diff --git a/include/BatteryGuard.h b/include/BatteryGuard.h new file mode 100644 index 000000000..3c4f6cdef --- /dev/null +++ b/include/BatteryGuard.h @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once +#include +#include +#include +#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 getOpenCircuitVoltage(void); + std::optional 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 _battVoltageAVG {5}; // average battery voltage [V] + WeightedAVG _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 _analyzedPeriod {20}; // measurement period [ms] + WeightedAVG _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 _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 _pFirstVolt = {0.0f,0.0f}; // first of two voltages and related current [V,A] + std::pair _pMaxVolt = {0.0f,0.0f}; // maximum voltage and related current [V,A] + std::pair _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; diff --git a/include/Configuration.h b/include/Configuration.h index fe9c7d7ef..b2df3b88d 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -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; @@ -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]; @@ -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); @@ -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); diff --git a/include/Statistic.h b/include/Statistic.h new file mode 100644 index 000000000..cfb505212 --- /dev/null +++ b/include/Statistic.h @@ -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 +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(_count) - 1) + num) / static_cast(_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/include/WebApi.h b/include/WebApi.h index 7cc95e807..991b72269 100644 --- a/include/WebApi.h +++ b/include/WebApi.h @@ -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" @@ -57,6 +58,7 @@ class WebApiClass { AsyncWebServer _server; WebApiBatteryClass _webApiBattery; + WebApiBatteryGuardClass _webApiBatteryGuard; WebApiDeviceClass _webApiDevice; WebApiDevInfoClass _webApiDevInfo; WebApiDtuClass _webApiDtu; diff --git a/include/WebApi_battery_guard.h b/include/WebApi_battery_guard.h new file mode 100644 index 000000000..312efc6a7 --- /dev/null +++ b/include/WebApi_battery_guard.h @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include + +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; +}; diff --git a/include/battery/Stats.h b/include/battery/Stats.h index c58af8c40..30bae127d 100644 --- a/include/battery/Stats.h +++ b/include/battery/Stats.h @@ -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; }; diff --git a/include/defaults.h b/include/defaults.h index 85bf5c30d..128967452 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -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 diff --git a/src/BatteryGuard.cpp b/src/BatteryGuard.cpp new file mode 100644 index 000000000..8d8b8dd40 --- /dev/null +++ b/src/BatteryGuard.cpp @@ -0,0 +1,431 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +/* Battery-Guard + * + * The Battery-Guard has several features. + * - Calculate the battery open circuit voltage + * - Calculate the battery internal resistance + * - Limit the power drawn from the battery, if the battery voltage is close to the stop threshold. + * - Periodically recharge the battery to 100% SoC (draft) + * + * Basic principe of the feature: "Battery open circuit voltage" + * As soon as we kow the battery internal resistance we calculate the open circuit voltage. + * open circuit voltage = battery voltage - battery current * resistance. + * + * Basic principe of the feature: "Battery internal resistance" + * In general, we try to use steady state values to calculate the battery internal resistance. + * We always search for 2 consecutive values and build an average for voltage and current. + * First, we search for a start value. + * Second, we search for a sufficient current change (trigger). + * Third, we search for min and max values in a time frame of 15 seconds + * After the time, we calculate the resistance from the min and max difference. + * Note: We need high load changes to get sufficient calculation results. About 100W on 24VDC or 180W on 48VDC. + * The resistance on LifePO4 batteries is not a fixed value, it depends from temperature, charge and time + * after a load change. + * + * 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 for the sun to fully charge the battery. + * When we reach 100% SoC we remove all restrictions and start a new period. + * Especially usefull during winter to support the SoC calibration of the BMS + * + * Notes: + * Some function are still under development. + * These functions were developed for the battery provider "Smart Shunt", but should also work with other providers + * + * 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" + * 14.02.2025 - 0.4 - works now with all battery providers, accept different time stamps for voltage and current + */ + +#include +#include +#include +#include +#include +#include + +#undef TAG +static const char* TAG = "battery"; +static const char* SUBTAG = "Battery Guard"; + +BatteryGuardClass BatteryGuard; + +/* + * Initialize the battery guard + */ +void BatteryGuardClass::init(Scheduler& scheduler) { + // init the fast loop + scheduler.addTask(_fastLoopTask); + _fastLoopTask.setCallback(std::bind(&BatteryGuardClass::loop, this)); + _fastLoopTask.setIterations(TASK_FOREVER); + _fastLoopTask.enable(); + + // init the slow loop + scheduler.addTask(_slowLoopTask); + _slowLoopTask.setCallback(std::bind(&BatteryGuardClass::slowLoop, this)); + _slowLoopTask.setIterations(TASK_FOREVER); + _slowLoopTask.setInterval(60*1000); + _slowLoopTask.enable(); + + _analyzedResolutionV = 0.10f; + _analyzedResolutionI = 1.0f; + _analyzedPeriod.reset(10000); + _analyzedUIDelay.reset(5000); + updateSettings(); +} + + +/* + * Update some settings of the battery guard + */ +void BatteryGuardClass::updateSettings(void) { + const auto& config = Configuration.get(); + + // Check if BatteryGuard is enabled and battery provider is configured + _useBatteryGuard = config.BatteryGuard.Enabled && config.Battery.Enabled; + _resistanceFromConfig = config.BatteryGuard.InternalResistance / 1000.0f; // mOhm -> Ohm +} + +/* + * Normal loop, fetches the battery values (voltage, current and measurement time stamp) from the battery provider + */ +void BatteryGuardClass::loop(void) { + const auto& config = Configuration.get(); + + if (!_useBatteryGuard + || !config.Battery.Enabled) { + // not active or no battery, we abort + return; + } + + if (!Battery.getStats()->isVoltageValid() + || !Battery.getStats()->isCurrentValid()) { + // stats not valid, we abort + return; + } + + auto const& u2Value = Battery.getStats()->getVoltage(); + auto const& u2Time = Battery.getStats()->getLastVoltageUpdate(); + auto const& i2Value = Battery.getStats()->getChargeCurrent(); + auto const& i2Time = Battery.getStats()->getLastCurrentUpdate(); + + if ( (_u1Data.timeStamp == u2Time) && (_i1Data.timeStamp == i2Time) ) { return; } // same time stamp again, we abort + + if (u2Time == i2Time) { + // the simple handling: voltage und current have the same time stamp + _i1Data = { i2Value, i2Time, true }; + _u1Data = { u2Value, u2Time, true }; + _analyzedUIDelay.addNumber(0.0f); + updateBatteryValues(u2Value, i2Value, i2Time); + return; + } + + // the special handling: voltage and current time stamp are different + // Note: In worst case, this will add a delay of 1 measurement period + if (i2Time != _i1Data.timeStamp) { + + // check if new U1 data is available and check if we have to use U1 or U2 + Data uxData = _u1Data; + if ((u2Time != _u1Data.timeStamp) && ((millis() - u2Time) > (millis() - i2Time))) { + uxData = { u2Value, u2Time, true }; + } + + if (uxData.valid && _i1Data.valid) { + + // check if Ux time stamp is closer to I1 or I2 time stamp + if ((i2Time - uxData.timeStamp) < (uxData.timeStamp - _i1Data.timeStamp)) { + _analyzedUIDelay.addNumber(static_cast(i2Time - uxData.timeStamp)); + updateBatteryValues(uxData.value, i2Value, uxData.timeStamp); // we use the older time stamp + } else { + _analyzedUIDelay.addNumber(-static_cast(uxData.timeStamp - _i1Data.timeStamp)); + updateBatteryValues(uxData.value, _i1Data.value, _i1Data.timeStamp); // we use the older time stamp + } + _u1Data.valid = false; + } + + _i1Data = { i2Value, i2Time, true }; // store the next I1 data + } + + if (u2Time != _u1Data.timeStamp) { _u1Data = { u2Value, u2Time, true }; } // store the next U1 data +} + + +/* + * Slow periodical tasks, will be called once a minute + */ +void BatteryGuardClass::slowLoop(void) { + const auto& config = Configuration.get(); + if (!_useBatteryGuard + || !config.Battery.Enabled + || !DTU_LOG_IS_VERBOSE) { + // not active or no battery or no verbose logging, we abort + return; + } + + DTU_LOGV(""); + DTU_LOGV("------------- Battery Guard Report (every minute) -------------"); + DTU_LOGV(""); + + // "Open circuit voltage" + printOpenCircuitVoltageReport(); + + DTU_LOGV("-----------------------------------------------------------"); + DTU_LOGV(""); +} + + +/* + * Update the battery guard with new values. (voltage[V], current[A], millisStamp[ms]) + * Note: Just call the function if new values are available. Current into the battery must be positive. + */ +void BatteryGuardClass::updateBatteryValues(float const volt, float const current, uint32_t const millisStamp) { + if (volt <= 0.0f) { return; } + + // analyse the measurement period + if ((_battMillis != 0) && (millisStamp > _battMillis)) { + _analyzedPeriod.addNumber(millisStamp - _battMillis); + } + + // analyse the voltage and current resolution + auto resolution = std::abs(volt - _battVoltage); + if ((resolution >= 0.001f) && (resolution < _analyzedResolutionV)) { _analyzedResolutionV = resolution; } + resolution = std::abs(current - _battCurrent); + if ((resolution >= 0.001f) && (resolution < _analyzedResolutionI)) { _analyzedResolutionI = resolution; } + + // store the values + _battMillis = millisStamp; + _battVoltage = volt; + _battCurrent = current; + _battVoltageAVG.addNumber(_battVoltage); + calculateInternalResistance(_battVoltage, _battCurrent); + calculateOpenCircuitVoltage(_battVoltage, _battCurrent); +} + + +/* + * Calculate the battery open circuit voltage. + */ +void BatteryGuardClass::calculateOpenCircuitVoltage(float const nowVoltage, float const nowCurrent) { + // resistance must be available and current flow into the battery must be positive + auto oResistor = getInternalResistance(); + if (oResistor.has_value()) { + _openCircuitVoltageAVG.addNumber(nowVoltage - nowCurrent * oResistor.value()); + } +} + + +/* + * The battery internal resistance, calculated or configured or nullopt if neither value is valid + */ +std::optional BatteryGuardClass::getInternalResistance(void) const { + // we use the calculated value if we have 5 calculations minimum + if (_useBatteryGuard) { + if (_resistanceFromCalcAVG.getCounts() >= MINIMUM_RESISTANCE_CALC) { return _resistanceFromCalcAVG.getAverage(); } + if (_resistanceFromConfig != 0.0f) { return _resistanceFromConfig; } + } + return std::nullopt; +} + + +/* + * True if we use the calculated resistor and not the configured one + */ +bool BatteryGuardClass::isInternalResistanceCalculated(void) const { + return (_resistanceFromCalcAVG.getCounts() >= MINIMUM_RESISTANCE_CALC); +} + + +/* + * The battery open circuit voltage or nullopt if value is not valid or not enabled + */ +std::optional BatteryGuardClass::getOpenCircuitVoltage(void) { + if ((_useBatteryGuard && _openCircuitVoltageAVG.getCounts() > 0) && isDataValid()) { + return _openCircuitVoltageAVG.getAverage(); + } + _notAvailableCounter++; + return std::nullopt; +} + + +/* + * True if the measurement resolution, period and time delay between voltage and current is sufficient + * Requirement: Voltage <= 20mV, Current <= 200mA, Period <= 4s, Time delay <= 1s + */ +bool BatteryGuardClass::isResolutionOK(void) const { + return (_analyzedResolutionV <= MAXIMUM_VOLTAGE_RESOLUTION) + && (_analyzedResolutionI <= MAXIMUM_CURRENT_RESOLUTION) + && (_analyzedPeriod.getAverage() <= MAXIMUM_MEASUREMENT_TIME_PERIOD) + && (std::abs(_analyzedUIDelay.getAverage()) <= MAXIMUM_V_I_TIME_STAMP_DELAY); +} + + +/* + * Calculate the battery DC-Pulse-Resistance based on the voltage measurement position of the battery provider. + * Note: The calculation will not work, if the performance of the battery provider is not good enough + * or if the power difference is not big enough or too slow. + */ +void BatteryGuardClass::calculateInternalResistance(float const nowVoltage, float const nowCurrent) { + + // lambda function to avoid nested if-else statements and code doubleing + auto cleanExit = [&](RState state) -> void { + if (_rStateLast == state) { return; } // no change, we abort without logging + _rStateLast = state; + DTU_LOGV("Resistance calculation state: %s", getResistanceStateText(state).data()); + if (state > _rStateMax) { _rStateMax = state; } + }; + + // check the resolution and the calculation frequency + if (!isResolutionOK()) { return cleanExit(RState::RESOLUTION); } + if ((millis() - _lastDataInMillis) < 900 ) { return cleanExit(RState::TIME); } + _lastDataInMillis = millis(); + if (!_minMaxAvailable) { _rState = RState::IDLE; } + + // check if we get the SoC from the provider and if we are in a useable SoC range + auto const& soc = Battery.getStats(); + if (!soc->isSoCValid() || (soc->getSoCAgeSeconds() > 30)) { return cleanExit(RState::SOC_NOT_VALID); } + if ((soc->getSoC() < 20.0f) || soc->getSoC() > 90.0f) { return cleanExit(RState::SOC_RANGE); } + + // check for the trigger event (sufficient current change) + auto const minDiffCurrent = 4.0f; // seems to be a good value for all battery providers + if (!_triggerEvent && _minMaxAvailable && (std::abs(nowCurrent - _pMinVolt.second) > (minDiffCurrent/2.0f))) { + _lastTriggerMillis = millis(); + _triggerEvent = true; + _rState = RState::TRIGGER; + } + + // we evaluate min and max values in a time duration of 15 sec after the trigger event + if (!_triggerEvent || (_triggerEvent && (millis() - _lastTriggerMillis) < 15*1000)) { + + // we use the measurement resolution to decide if two consecutive values are almost identical + auto minVoltage = (_triggerEvent) ? 0.2f : std::max(_analyzedResolutionV * 3.0f, 0.01f); + auto minCurrent = std::max(_analyzedResolutionI * 3.0f, 0.2f); + + // after the first-pair-after-the-trigger, we check if the current is stable + // if the current is not stable we break the calculation because we have again a power transition + // which influences the quality of the calculation + if (_pairAfterTriggerAvailable && (std::abs(_checkCurrent - nowCurrent) > minCurrent)) { + _firstOfTwoAvailable = false; + _minMaxAvailable = false; + _triggerEvent = false; + _pairAfterTriggerAvailable = false; + return cleanExit(RState::SECOND_BREAK); + } + + // we must avoid to use measurement values during any power transitions + // to solve this problem, we check whether two consecutive measurements are almost identical + if (_firstOfTwoAvailable && (std::abs(_pFirstVolt.first - nowVoltage) <= minVoltage) && + (std::abs(_pFirstVolt.second - nowCurrent) <= minCurrent)) { + + auto avgVolt = std::make_pair((nowVoltage+_pFirstVolt.first)/2.0f, (nowCurrent+_pFirstVolt.second)/2.0f); + if (!_minMaxAvailable || !_triggerEvent) { + _pMinVolt = _pMaxVolt = avgVolt; + _minMaxAvailable = true; + _rState = RState::FIRST_PAIR; // we have the first pair (before the trigger event) + } else { + if (avgVolt.first < _pMinVolt.first) { _pMinVolt = avgVolt; } + if (avgVolt.first > _pMaxVolt.first) { _pMaxVolt = avgVolt; } + _pairAfterTriggerAvailable = true; + _checkCurrent = nowCurrent; + _rState = RState::SECOND_PAIR; // we have the second pair (after the trigger event) + } + } + _pFirstVolt = { nowVoltage, nowCurrent }; // preparation for the next two consecutive values + _firstOfTwoAvailable = true; + return cleanExit(_rState); + } + + // reset conditions for the next calculation + _firstOfTwoAvailable = false; + _minMaxAvailable = false; + _triggerEvent = false; + _pairAfterTriggerAvailable = false; + + // now we have minimum and maximum values and we can try to calculate the resistance + // we need a minimum power difference to get a sufficiently good result (failure < 20%) + // SmartShunt: 40mV and 4A (about 100W on VDC=24V, Ri=12mOhm) + auto minDiffVoltage = std::max(_analyzedResolutionV * 5.0f, 0.04f); + auto diffVolt = _pMaxVolt.first - _pMinVolt.first; + auto diffCurrent = std::abs(_pMaxVolt.second - _pMinVolt.second); // can be negative + if ((diffVolt >= minDiffVoltage) && (diffCurrent >= minDiffCurrent)) { + float resistor = diffVolt / diffCurrent; + auto reference = (_resistanceFromConfig != 0.0f) ?_resistanceFromConfig : _resistanceFromCalcAVG.getAverage(); + if ((reference != 0.0f) && ((resistor > reference * 2.0f) || (resistor < reference / 2.0f))) { + _rState = RState::TOO_BAD; // safety feature: we try to keep out bad values from the average + } else { + _resistanceFromCalcAVG.addNumber(resistor); + _rState = RState::CALCULATED; + } + } else { + _rState = RState::DELTA_POWER; + } + + return cleanExit(_rState); +} + + +/* + * Prints the "Battery open circuit voltage" report + */ +void BatteryGuardClass::printOpenCircuitVoltageReport(void) +{ + DTU_LOGV("1) Open circuit voltage calculation. Battery data %s", (isResolutionOK()) ? "sufficient" : "not sufficient"); + DTU_LOGV("Open circuit voltage: %0.3fV (Actual battery voltage: %0.3fV)", _openCircuitVoltageAVG.getAverage(), _battVoltage); + + auto oResistance = getInternalResistance(); + if (!oResistance.has_value()) { + DTU_LOGV("Resistance neither calculated (5 times) nor configured"); + } else { + auto resCalc = (_resistanceFromCalcAVG.getCounts() > 4) ? _resistanceFromCalcAVG.getAverage() * 1000.0f : 0.0f; + DTU_LOGV("Resistance in use: %0.1fmOhm (Calc.: %0.1fmOhm, Config.: %0.1fmOhm)", + oResistance.value() * 1000.0f, resCalc, _resistanceFromConfig * 1000.0f); + } + + DTU_LOGV("Resistance calc.: %0.1fmOhm (Min: %0.1f, Max: %0.1f, Amount: %i)", + _resistanceFromCalcAVG.getAverage()*1000.0f, _resistanceFromCalcAVG.getMin()*1000.0f, + _resistanceFromCalcAVG.getMax()*1000.0f, _resistanceFromCalcAVG.getCounts()); + + DTU_LOGV("Resistance calculation state: %s", getResistanceStateText(_rStateMax).data()); + + DTU_LOGV("Voltage resolution: %0.0fmV, Current resolution: %0.0fmA", + _analyzedResolutionV * 1000.0f, _analyzedResolutionI * 1000.0f); + + DTU_LOGV("Measurement period: %0.0fms, V-I time stamp delay: %0.0fms", + _analyzedPeriod.getAverage(), _analyzedUIDelay.getAverage()); + + DTU_LOGV("Open circuit voltage not available counter: %i", _notAvailableCounter); +} + + +/* + * Returns a string according to resistance calculation state + */ +frozen::string const& BatteryGuardClass::getResistanceStateText(BatteryGuardClass::RState tNr) const +{ + static const frozen::string missing = "programmer error: missing status text"; + + static const frozen::map texts = { + { RState::IDLE, "Idle" }, + { RState::RESOLUTION, "Battery data insufficient" }, + { RState::SOC_NOT_VALID, "SoC not available" }, + { RState::SOC_RANGE, "SoC out of range 20%-90%" }, + { RState::TIME, "Measurement time to fast" }, + { RState::FIRST_PAIR, "Start data available" }, + { RState::TRIGGER, "Trigger event" }, + { RState::SECOND_PAIR, "Collecting data after trigger" }, + { RState::SECOND_BREAK, "Second power change after trigger" }, + { RState::DELTA_POWER, "Power difference not high enough" }, + { RState::TOO_BAD, "Resistance out of safety range" }, + { RState::CALCULATED, "Resistance calculated" } + }; + + auto iter = texts.find(tNr); + if (iter == texts.end()) { return missing; } + + return iter->second; +} diff --git a/src/Configuration.cpp b/src/Configuration.cpp index d1fb639fe..7fd9f6434 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -259,6 +259,12 @@ void ConfigurationClass::serializeGridChargerHuaweiConfig(GridChargerHuaweiConfi target["fan_offline_full_speed"] = source.FanOfflineFullSpeed; } +void ConfigurationClass::serializeBatteryGuardConfig(BatteryGuardConfig const& source, JsonObject& target) +{ + target["enabled"] = source.Enabled; + target["internal_resistance"] = source.InternalResistance; +} + bool ConfigurationClass::write() { File f = LittleFS.open(CONFIG_FILENAME, "w"); @@ -447,6 +453,9 @@ bool ConfigurationClass::write() JsonObject gridcharger_huawei = gridcharger["huawei"].to(); serializeGridChargerHuaweiConfig(config.GridCharger.Huawei, gridcharger_huawei); + JsonObject batteryGuard = doc["batteryguard"].to(); + serializeBatteryGuardConfig(config.BatteryGuard, batteryGuard); + if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) { return false; } @@ -678,6 +687,12 @@ void ConfigurationClass::deserializeGridChargerHuaweiConfig(JsonObject const& so target.FanOfflineFullSpeed = source["fan_offline_full_speed"] | GRIDCHARGER_HUAWEI_FAN_OFFLINE_FULL_SPEED; } +void ConfigurationClass::deserializeBatteryGuardConfig(JsonObject const& source, BatteryGuardConfig& target) +{ + target.Enabled = source["enabled"] | BATTERYGUARD_ENABLED; + target.InternalResistance = source["internal_resistance"] | BATTERYGUARD_INTERNAL_RESISTANCE; +} + bool ConfigurationClass::read() { File f = LittleFS.open(CONFIG_FILENAME, "r", false); @@ -892,6 +907,8 @@ bool ConfigurationClass::read() deserializeGridChargerCanConfig(gridcharger["can"], config.GridCharger.Can); deserializeGridChargerHuaweiConfig(gridcharger["huawei"], config.GridCharger.Huawei); + deserializeBatteryGuardConfig(doc["batteryguard"], config.BatteryGuard); + f.close(); // Check for default DTU serial diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index 6a2ccc8d3..3e421cdcb 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -18,6 +18,7 @@ #include #include "SunPosition.h" #include +#include #undef TAG static const char* TAG = "dynamicPowerLimiter"; @@ -291,6 +292,12 @@ void PowerLimiterClass::loop() }; auto getLoadCorrectedVoltage = [this,&config]() -> float { + // use "open circuit voltage" from the Battery Guard if available + auto oOpenCV = BatteryGuard.getOpenCircuitVoltage(); + if (oOpenCV.has_value()) { + return *oOpenCV; + } + // TODO(schlimmchen): use the battery's data if available, // i.e., the current drawn from the battery as reported by the battery. float acPower = getBatteryInvertersOutputAcWatts(); diff --git a/src/WebApi.cpp b/src/WebApi.cpp index a17b6acfa..47e16c8e1 100644 --- a/src/WebApi.cpp +++ b/src/WebApi.cpp @@ -40,6 +40,7 @@ void WebApiClass::init(Scheduler& scheduler) _webApiWsConsole.init(_server, scheduler); _webApiWsLive.init(_server, scheduler); _webApiBattery.init(_server, scheduler); + _webApiBatteryGuard.init(_server, scheduler); _webApiPowerMeter.init(_server, scheduler); _webApiPowerLimiter.init(_server, scheduler); _webApiWsSolarChargerLive.init(_server, scheduler); diff --git a/src/WebApi_battery_guard.cpp b/src/WebApi_battery_guard.cpp new file mode 100644 index 000000000..e952d97ae --- /dev/null +++ b/src/WebApi_battery_guard.cpp @@ -0,0 +1,126 @@ +#include "WebApi_battery_guard.h" +#include "Configuration.h" +#include "BatteryGuard.h" +#include "defaults.h" +#include "WebApi.h" +#include +#include + +void WebApiBatteryGuardClass::init(AsyncWebServer& server, Scheduler& scheduler) +{ + using std::placeholders::_1; + + _server = &server; + + _server->on("/api/batteryguard/status", HTTP_GET, std::bind(&WebApiBatteryGuardClass::onStatus, this, _1)); + _server->on("/api/batteryguard/config", HTTP_GET, std::bind(&WebApiBatteryGuardClass::onAdminGet, this, _1)); + _server->on("/api/batteryguard/config", HTTP_POST, std::bind(&WebApiBatteryGuardClass::onAdminPost, this, _1)); + _server->on("/api/batteryguard/metadata", HTTP_GET, std::bind(&WebApiBatteryGuardClass::onMetaData, this, _1)); +} + +void WebApiBatteryGuardClass::onStatus(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentialsReadonly(request)) { + return; + } + + AsyncJsonResponse* response = new AsyncJsonResponse(); + auto root = response->getRoot().as(); + auto const& config = Configuration.get(); + + // Basic configuration status + root["enabled"] = config.BatteryGuard.Enabled; + + auto const& limits = root["limits"]; + limits["max_voltage_resolution"] = BatteryGuard.MAXIMUM_VOLTAGE_RESOLUTION * 1000.0f; // mV + limits["max_current_resolution"] = BatteryGuard.MAXIMUM_CURRENT_RESOLUTION * 1000.0f; // mA + limits["max_measurement_time_period"] = BatteryGuard.MAXIMUM_MEASUREMENT_TIME_PERIOD; // In milliseconds + limits["max_v_i_time_stamp_delay"] = BatteryGuard.MAXIMUM_V_I_TIME_STAMP_DELAY; // In milliseconds + limits["min_resistance_calculation_count"] = BatteryGuard.MINIMUM_RESISTANCE_CALC; + + auto const& values = root["values"]; + + // Open circuit voltage + auto oOpenCircuitVoltage = BatteryGuard.getOpenCircuitVoltage(); + values["open_circuit_voltage_calculated"] = oOpenCircuitVoltage.has_value(); + if (oOpenCircuitVoltage) { + values["open_circuit_voltage"] = *oOpenCircuitVoltage; + } else { + values["open_circuit_voltage"] = 0; + } + + values["uncompensated_voltage"] = Battery.getStats()->getVoltage(); + + // Internal resistance (configured and calculated) + auto oInternalResistance = BatteryGuard.getInternalResistance(); + values["internal_resistance_calculated"] = oInternalResistance.has_value() && BatteryGuard.isInternalResistanceCalculated(); + + if (oInternalResistance && BatteryGuard.isInternalResistanceCalculated()) { + values["resistance_calculated"] = *oInternalResistance * 1000.0f; // mOhm + } + + values["resistance_configured"] = config.BatteryGuard.InternalResistance; // mOhm + + // Get resistance calculation details + values["resistance_calculation_count"] = BatteryGuard.getResistanceCalculationCount(); + values["resistance_calculation_state"] = BatteryGuard.getResistanceCalculationState(); + + // Resolution and timing information + values["voltage_resolution"] = BatteryGuard.getVoltageResolution() * 1000.0f; // mV + values["current_resolution"] = BatteryGuard.getCurrentResolution() * 1000.0f; // mA + values["measurement_time_period"] = BatteryGuard.getMeasurementPeriod(); // In milliseconds + values["v_i_time_stamp_delay"] = BatteryGuard.getVIStampDelay(); // In milliseconds + + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); +} + +void WebApiBatteryGuardClass::onMetaData(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentials(request)) { return; } + + auto const& config = Configuration.get(); + + AsyncJsonResponse* response = new AsyncJsonResponse(); + auto& root = response->getRoot(); + + root["battery_enabled"] = config.Battery.Enabled; + + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); +} + +void WebApiBatteryGuardClass::onAdminGet(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentials(request)) { return; } + + AsyncJsonResponse* response = new AsyncJsonResponse(); + auto root = response->getRoot().as(); + auto const& config = Configuration.get(); + ConfigurationClass::serializeBatteryGuardConfig(config.BatteryGuard, root); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); +} + +void WebApiBatteryGuardClass::onAdminPost(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentials(request)) { return; } + + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { + return; + } + + auto& retMsg = response->getRoot(); + + { + auto guard = Configuration.getWriteGuard(); + auto& config = guard.getConfig(); + ConfigurationClass::deserializeBatteryGuardConfig(root.as(), config.BatteryGuard); + } + + WebApi.writeConfig(retMsg); + + response->setLength(); + request->send(response); + + BatteryGuard.updateSettings(); +} diff --git a/src/battery/Stats.cpp b/src/battery/Stats.cpp index da105bb91..39987128c 100644 --- a/src/battery/Stats.cpp +++ b/src/battery/Stats.cpp @@ -3,6 +3,7 @@ #include #include #include +#include namespace Batteries { @@ -52,6 +53,10 @@ void Stats::getLiveViewData(JsonVariant& root) const addLiveViewValue(root, "voltage", _voltage, "V", 2); } + if (BatteryGuard.getOpenCircuitVoltage().has_value()) { + addLiveViewValue(root, "openCircuitVoltage", BatteryGuard.getOpenCircuitVoltage().value(), "V", 2); + } + if (isCurrentValid()) { addLiveViewValue(root, "current", _current, "A", _currentPrecision); } @@ -64,6 +69,14 @@ void Stats::getLiveViewData(JsonVariant& root) const addLiveViewValue(root, "chargeCurrentLimitation", _chargeCurrentLimit, "A", 1); } + if (BatteryGuard.getInternalResistance().has_value()) { + if (BatteryGuard.isInternalResistanceCalculated()) { + addLiveViewValue(root, "resistorCalculated", BatteryGuard.getInternalResistance().value() * 1000.0f, "mOhm", 1); + } else { + addLiveViewValue(root, "resistorConfigured", BatteryGuard.getInternalResistance().value() * 1000.0f, "mOhm", 1); + } + } + root["showIssues"] = supportsAlarmsAndWarnings(); } diff --git a/src/main.cpp b/src/main.cpp index c4faf034f..4823237fc 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -36,6 +36,7 @@ #include #include #include +#include "BatteryGuard.h" #undef TAG static const char* TAG = "main"; @@ -151,6 +152,7 @@ void setup() PowerLimiter.init(scheduler); GridCharger.init(scheduler); Battery.init(scheduler); + BatteryGuard.init(scheduler); ESP_LOGI(TAG, "Startup complete"); } diff --git a/webapp/src/components/NavBar.vue b/webapp/src/components/NavBar.vue index f96fd0ca8..5136537e6 100644 --- a/webapp/src/components/NavBar.vue +++ b/webapp/src/components/NavBar.vue @@ -96,6 +96,11 @@ $t('menu.BatterySettings') }} +
  • + + {{ $t('menu.BatteryGuardSettings') }} + +
  • {{ $t('menu.AcChargerSettings') @@ -158,6 +163,11 @@ $t('menu.MQTT') }}
  • +
  • + {{ + $t('menu.BatteryGuard') + }} +
  • diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index b6680e011..4a3accc1a 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -13,6 +13,7 @@ "SolarChargerSettings": "Solarladeregler", "PowerMeterSettings": "Stromzähler", "BatterySettings": "Batterie", + "BatteryGuardSettings": "Battery Guard", "AcChargerSettings": "AC-Ladegerät", "ConfigManagement": "Konfigurationsverwaltung", "FirmwareUpgrade": "Firmware-Aktualisierung", @@ -1131,6 +1132,9 @@ "capacity": "Gesamtkapazität", "availableCapacity": "Verfügbare Kapazität", "temperature": "Temperatur", + "resistorConfigured": "Widerstand (konfiguriert)", + "resistorCalculated": "Widerstand (berechnet)", + "openCircuitVoltage": "Leerlaufspannung", "bmsTemp": "BMS-Temperatur", "chargeVoltage": "Gewünschte Ladespannung (BMS)", "chargeCurrentLimitation": "Ladestromlimit", @@ -1279,5 +1283,40 @@ "invalid": "@:battery.invalid" } } + }, + "batteryguardadmin": { + "BatteryGuardSettings": "Battery Guard Einstellungen", + "ConfigAlertMessage": "Eine oder mehrere Voraussetzungen zum Betrieb des Battery Guard sind nicht erfüllt.", + "ConfigHints": "Konfigurationshinweise", + "ConfigHintsIntro": "Folgende Hinweise zur Konfiguration des Battery Guard sollten beachtet werden:", + "ConfigHintRequirement": "Erforderlich", + "ConfigHintOptional": "Optional", + "ConfigHintBatteryRequired": "Battery Guard kann nur mit konfigurierter Batteriekommunikationsschnittstelle genutzt werden.", + "BatteryGuardConfiguration": "Battery Guard Konfiguration", + "EnableBatteryGuard": "Aktiviere Battery Guard", + "InternalResistance": "DC-Pulse Widerstand", + "InternalResistanceHint": "Das System versucht den Wert aus Laständerungen zu berechnen. Bis ein berechneter Wert zur Verfügung steht wird der hier konfigurierte Wert verwendet. Der berechnete Wert wird unter Info/BatteryGuard angezeigt." + }, + "batteryguardinfo": { + "BatteryGuardInformation": "Battery Guard Informationen", + "General": "Allgemein", + "Status": "Status", + "BatteryVoltage": "Batterie Ausgangsspannung", + "OpenCircuitVoltage": "Berechnete Leerlaufspannung", + "UncompensatedVoltage": "Unkompensierte Spannung", + "BatteryDCPulseResistance": "DC-Puls Widerstand", + "ResistanceConfigured": "Konfigurierter Widerstand", + "ResistanceCalculated": "Berechneter Widerstand", + "MeasurementData": "Detailinformationen zur Widerstandsberechnung", + "Property": "Eigenschaft", + "Limit": "Grenzwert", + "Value": "Wert", + "VoltageResolution": "Spannungsauflösung", + "CurrentResolution": "Stromauflösung", + "MeasurementTimePeriod": "Messperiode", + "VIStampDelay": "Zeitversatz Spannung-Strom", + "ResistanceCalculationCount": "Anzahl der Widerstandsberechnungen", + "ResistanceCalculationState": "Maximale Stufe der Widerstandsberechnung", + "CalculationNotPossible": "Berechnung nicht möglich" } } diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 44de8cc57..3f32ab019 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -13,6 +13,7 @@ "SolarChargerSettings": "Solar Charger", "PowerMeterSettings": "Power Meter", "BatterySettings": "Battery", + "BatteryGuardSettings": "Battery Guard", "AcChargerSettings": "AC Charger", "ConfigManagement": "Config Management", "FirmwareUpgrade": "Firmware Upgrade", @@ -22,6 +23,7 @@ "Network": "Network", "NTP": "NTP", "MQTT": "MQTT", + "BatteryGuard": "Battery Guard", "Console": "Console", "About": "About", "Logout": "Logout", @@ -1133,6 +1135,9 @@ "capacity": "Total capacity", "availableCapacity": "Available capacity", "temperature": "Temperature", + "resistorConfigured": "Resistance (configured)", + "resistorCalculated": "Resistance (calculated)", + "openCircuitVoltage": "Open circuit voltage", "bmsTemp": "BMS temperature", "chargeVoltage": "Requested charge voltage", "chargeCurrentLimitation": "Charge current limit", @@ -1281,5 +1286,40 @@ "invalid": "@:battery.invalid" } } + }, + "batteryguardadmin": { + "BatteryGuardSettings": "Battery Guard Settings", + "ConfigAlertMessage": "One or more prerequisites for operating the Battery Guard are not met.", + "ConfigHints": "Configuration Hints", + "ConfigHintsIntro": "The following hints should be considered when configuring the Battery Guard:", + "ConfigHintRequirement": "Required", + "ConfigHintOptional": "Optional", + "ConfigHintBatteryRequired": "Battery Guard can only be used with a configured battery communication interface.", + "BatteryGuardConfiguration": "Battery Guard Configuration", + "EnableBatteryGuard": "Enable Battery Guard", + "InternalResistance": "DC-Pulse Resistance", + "InternalResistanceHint": "The system tries to calculate the value from load changes. Until a calculated value is available, the value configured here will be used. The calculated value is shown in Info/BatteryGuard." + }, + "batteryguardinfo": { + "BatteryGuardInformation": "Battery Guard Information", + "General": "General", + "Status": "Status", + "BatteryVoltage": "Battery voltage", + "OpenCircuitVoltage": "Calculated open circuit voltage", + "UncompensatedVoltage": "Uncompensated voltage", + "BatteryDCPulseResistance": "DC-Puls resistance", + "ResistanceConfigured": "Configured resistance", + "ResistanceCalculated": "Calculated resistance", + "MeasurementData": "Resistance calculation details", + "Property": "Property", + "Limit": "Limit", + "Value": "Value", + "VoltageResolution": "Voltage resolution", + "CurrentResolution": "Current resolution", + "MeasurementTimePeriod": "Measurement period", + "VIStampDelay": "Voltage-Current delay", + "ResistanceCalculationCount": "Resistance calculation count", + "ResistanceCalculationState": "Maximum resistance calculation state", + "CalculationNotPossible": "Calculation not possible" } } diff --git a/webapp/src/router/index.ts b/webapp/src/router/index.ts index e538b935c..df713b522 100644 --- a/webapp/src/router/index.ts +++ b/webapp/src/router/index.ts @@ -24,6 +24,8 @@ import NtpInfoView from '@/views/NtpInfoView.vue'; import SecurityAdminView from '@/views/SecurityAdminView.vue'; import SystemInfoView from '@/views/SystemInfoView.vue'; import WaitRestartView from '@/views/WaitRestartView.vue'; +import BatteryGuardAdminView from '@/views/BatteryGuardAdminView.vue'; +import BatteryGuardInfoView from '@/views/BatteryGuardInfoView.vue'; import { createRouter, createWebHistory } from 'vue-router'; const router = createRouter({ @@ -82,6 +84,11 @@ const router = createRouter({ name: 'Web Console', component: ConsoleInfoView, }, + { + path: '/info/batteryguard', + name: 'Battery Guard', + component: BatteryGuardInfoView, + }, { path: '/settings/network', name: 'Network Settings', @@ -167,6 +174,14 @@ const router = createRouter({ name: 'Wait Restart', component: WaitRestartView, }, + { + path: '/settings/batteryguard', + name: 'Battery Guard Settings', + component: BatteryGuardAdminView, + meta: { + requiresAuth: true, + }, + }, ], }); diff --git a/webapp/src/types/BatteryGuardConfig.ts b/webapp/src/types/BatteryGuardConfig.ts new file mode 100644 index 000000000..df0aa3151 --- /dev/null +++ b/webapp/src/types/BatteryGuardConfig.ts @@ -0,0 +1,10 @@ +export interface BatteryGuardConfig { + enabled: boolean; + internal_resistance: number; +} + +// meta-data not directly part of the BatteryGuard settings, +// to control visibility of BatteryGuard settings +export interface BatteryGuardMetaData { + battery_enabled: boolean; +} diff --git a/webapp/src/types/BatteryGuardStatus.ts b/webapp/src/types/BatteryGuardStatus.ts new file mode 100644 index 000000000..9bdf66e0f --- /dev/null +++ b/webapp/src/types/BatteryGuardStatus.ts @@ -0,0 +1,28 @@ +export interface Values { + voltage_resolution: number; + current_resolution: number; + open_circuit_voltage_calculated: boolean; + open_circuit_voltage: number; + uncompensated_voltage: number; + internal_resistance_calculated: boolean; + resistance_configured: number; + resistance_calculated: number; + resistance_calculation_count: number; + resistance_calculation_state: string; + measurement_time_period: number; + v_i_time_stamp_delay: number; +} + +export interface Limits { + max_voltage_resolution: number; + max_current_resolution: number; + max_measurement_time_period: number; + max_v_i_time_stamp_delay: number; + min_resistance_calculation_count: number; +} + +export interface BatteryGuardStatus { + enabled: boolean; + values: Values; + limits: Limits; +} diff --git a/webapp/src/views/BatteryGuardAdminView.vue b/webapp/src/views/BatteryGuardAdminView.vue new file mode 100644 index 000000000..a6657f163 --- /dev/null +++ b/webapp/src/views/BatteryGuardAdminView.vue @@ -0,0 +1,143 @@ + + + diff --git a/webapp/src/views/BatteryGuardInfoView.vue b/webapp/src/views/BatteryGuardInfoView.vue new file mode 100644 index 000000000..0a2cdaa03 --- /dev/null +++ b/webapp/src/views/BatteryGuardInfoView.vue @@ -0,0 +1,358 @@ + + +