From 3c1d3f72073ddb66d3c7a62ce7e65b7f91a0572d Mon Sep 17 00:00:00 2001 From: Niko <129541740+SW-Niko@users.noreply.github.com> Date: Fri, 25 Oct 2024 21:42:26 +0200 Subject: [PATCH] Feature: retrieve absorption and float voltage from Victron MPPTs (#1140) the absorption and float voltage setting is retrieved from connected Victron Ve.Direct MPPTs using the HEX protocol. the values are displayed in the live view, published to MQTT, and added to Home Assistent auto-discovery. --- include/VictronMppt.h | 11 ++ lib/VeDirectFrameHandler/VeDirectData.cpp | 15 +- lib/VeDirectFrameHandler/VeDirectData.h | 13 +- .../VeDirectFrameHandler.h | 3 +- .../VeDirectFrameHexHandler.cpp | 12 +- .../VeDirectMpptController.cpp | 129 ++++++++++++++---- .../VeDirectMpptController.h | 19 +++ src/MqttHandleVedirect.cpp | 2 + src/MqttHandleVedirectHass.cpp | 6 + src/VictronMppt.cpp | 36 +++++ src/WebApi_ws_vedirect_live.cpp | 10 ++ webapp/src/locales/de.json | 2 + webapp/src/locales/en.json | 2 + webapp/src/locales/fr.json | 2 + 14 files changed, 227 insertions(+), 35 deletions(-) diff --git a/include/VictronMppt.h b/include/VictronMppt.h index 963a46cd9..e79564e87 100644 --- a/include/VictronMppt.h +++ b/include/VictronMppt.h @@ -42,6 +42,17 @@ class VictronMpptClass { // minimum of all MPPT charge controllers' output voltages in V float getOutputVoltage() const; + // returns the state of operation from the first available controller + std::optional getStateOfOperation() const; + + // returns the requested value from the first available controller in mV + enum class MPPTVoltage : uint8_t { + ABSORPTION = 0, + FLOAT = 1, + BATTERY = 2 + }; + std::optional getVoltage(MPPTVoltage kindOf) const; + private: void loop(); VictronMpptClass(VictronMpptClass const& other) = delete; diff --git a/lib/VeDirectFrameHandler/VeDirectData.cpp b/lib/VeDirectFrameHandler/VeDirectData.cpp index a67175fde..74e4bc484 100644 --- a/lib/VeDirectFrameHandler/VeDirectData.cpp +++ b/lib/VeDirectFrameHandler/VeDirectData.cpp @@ -191,7 +191,7 @@ frozen::string const& veMpptStruct::getCsAsString() const { 0, "OFF" }, { 2, "Fault" }, { 3, "Bulk" }, - { 4, "Absorbtion" }, + { 4, "Absorption" }, { 5, "Float" }, { 7, "Equalize (manual)" }, { 245, "Starting-up" }, @@ -287,18 +287,27 @@ frozen::string const& VeDirectHexData::getResponseAsString() const frozen::string const& VeDirectHexData::getRegisterAsString() const { using Register = VeDirectHexRegister; - static constexpr frozen::map values = { + static constexpr frozen::map values = { { Register::DeviceMode, "Device Mode" }, { Register::DeviceState, "Device State" }, { Register::RemoteControlUsed, "Remote Control Used" }, { Register::PanelVoltage, "Panel Voltage" }, + { Register::PanelPower, "Panel Power" }, { Register::ChargerVoltage, "Charger Voltage" }, + { Register::ChargerCurrent, "Charger Current" }, { Register::NetworkTotalDcInputPower, "Network Total DC Input Power" }, { Register::ChargeControllerTemperature, "Charger Controller Temperature" }, { Register::SmartBatterySenseTemperature, "Smart Battery Sense Temperature" }, { Register::NetworkInfo, "Network Info" }, { Register::NetworkMode, "Network Mode" }, - { Register::NetworkStatus, "Network Status" } + { Register::NetworkStatus, "Network Status" }, + { Register::BatteryAbsorptionVoltage, "Battery Absorption Voltage" }, + { Register::BatteryFloatVoltage, "Battery Float Voltage" }, + { Register::TotalChargeCurrent, "Total Charge Current" }, + { Register::ChargeStateElapsedTime, "Charge State Elapsed Time" }, + { Register::BatteryVoltageSense, "Battery Voltage Sense" }, + { Register::LoadCurrent, "Load current" }, + { Register::LoadOutputVoltage, "Load Output Voltage" } }; return getAsString(values, addr); diff --git a/lib/VeDirectFrameHandler/VeDirectData.h b/lib/VeDirectFrameHandler/VeDirectData.h index 34c6b8991..9cd97ad61 100644 --- a/lib/VeDirectFrameHandler/VeDirectData.h +++ b/lib/VeDirectFrameHandler/VeDirectData.h @@ -45,6 +45,8 @@ struct veMpptStruct : veStruct { std::pair MpptTemperatureMilliCelsius; std::pair SmartBatterySenseTemperatureMilliCelsius; std::pair NetworkTotalDcInputPowerMilliWatts; + std::pair BatteryAbsorptionMilliVolt; + std::pair BatteryFloatMilliVolt; std::pair NetworkInfo; std::pair NetworkMode; std::pair NetworkStatus; @@ -121,7 +123,9 @@ enum class VeDirectHexRegister : uint16_t { DeviceState = 0x0201, RemoteControlUsed = 0x0202, PanelVoltage = 0xEDBB, + PanelPower = 0xEDBC, ChargerVoltage = 0xEDD5, + ChargerCurrent = 0xEDD7, NetworkTotalDcInputPower = 0x2027, ChargeControllerTemperature = 0xEDDB, SmartBatterySenseTemperature = 0xEDEC, @@ -129,7 +133,14 @@ enum class VeDirectHexRegister : uint16_t { NetworkMode = 0x200E, NetworkStatus = 0x200F, HistoryTotal = 0x104F, - HistoryMPPTD30 = 0x10BE + HistoryMPPTD30 = 0x10BE, + BatteryAbsorptionVoltage = 0xEDF7, + BatteryFloatVoltage = 0xEDF6, + TotalChargeCurrent = 0x2013, + ChargeStateElapsedTime= 0x2007, + BatteryVoltageSense = 0x2002, + LoadCurrent = 0xEDAD, + LoadOutputVoltage = 0xEDA9 }; struct VeDirectHexData { diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHandler.h b/lib/VeDirectFrameHandler/VeDirectFrameHandler.h index 244caf3f3..86636e87c 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHandler.h +++ b/lib/VeDirectFrameHandler/VeDirectFrameHandler.h @@ -27,12 +27,13 @@ class VeDirectFrameHandler { bool isDataValid() const; // return true if data valid and not outdated T const& getData() const { return _tmpFrame; } bool sendHexCommand(VeDirectHexCommand cmd, VeDirectHexRegister addr, uint32_t value = 0, uint8_t valsize = 0); + bool isStateIdle() const { return (_state == State::IDLE); } protected: VeDirectFrameHandler(); void init(char const* who, int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint8_t hwSerialPort); - virtual bool hexDataHandler(VeDirectHexData const &data) { return false; } // handles the disassembeled hex response + virtual bool hexDataHandler(VeDirectHexData const &data) { return false; } // handles the disassembled hex response bool _verboseLogging; Print* _msgOut; diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHexHandler.cpp b/lib/VeDirectFrameHandler/VeDirectFrameHexHandler.cpp index 392d2f8a9..8256f1eae 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHexHandler.cpp +++ b/lib/VeDirectFrameHandler/VeDirectFrameHexHandler.cpp @@ -8,7 +8,7 @@ HexHandler.cpp * 1. Use sendHexCommand() to send hex messages. Use the Victron documentation to find the parameter. * 2. The from class "VeDirectFrameHandler" derived class X must overwrite the function * void VeDirectFrameHandler::hexDataHandler(VeDirectHexData const &data) - * to handle the received hex messages. All hex messages will be forwarted to function hexDataHandler() + * to handle the received hex messages. All hex messages will be forwarded to function hexDataHandler() * 3. Analyse the content of data (struct VeDirectHexData) to check if a message fits. * * 2024.03.08 - 0.4 - adds the ability to send hex commands and to parse hex messages @@ -63,9 +63,9 @@ static uint32_t AsciiHexLE2Int(const char *ascii, const uint8_t anz) { * disassembleHexData() * analysis the hex message and extract: response, address, flags and value/text * buffer: pointer to message (ascii hex little endian format) - * data: disassembeled message - * return: true = successful disassembeld, false = hex sum fault or message - * do not aligin with VE.Diekt syntax + * data: disassembled message + * return: true = successful disassembled, false = hex sum fault or message + * do not align with VE.Direct syntax */ template bool VeDirectFrameHandler::disassembleHexData(VeDirectHexData &data) { @@ -164,14 +164,14 @@ static String Int2HexLEString(uint32_t value, uint8_t anz) { * addr: register address, default 0 * value: value to write into a register, default 0 * valsize: size of the value, 8, 16 or 32 bit, default 0 - * return: true = message assembeld and send, false = it was not possible to put the message together + * return: true = message assembled and send, false = it was not possible to put the message together * SAMPLE: ping command: sendHexCommand(PING), * read total DC input power sendHexCommand(GET, 0xEDEC) * set Charge current limit 10A sendHexCommand(SET, 0x2015, 64, 16) * * WARNING: some values are stored in non-volatile memory. Continuous writing, for example from a control loop, will * lead to early failure. - * On MPPT for example 0xEDE0 - 0xEDFF. Check the Vivtron doc "BlueSolar-HEX-protocol.pdf" + * On MPPT for example 0xEDE0 - 0xEDFF. Check the Victron doc "BlueSolar-HEX-protocol.pdf" */ template bool VeDirectFrameHandler::sendHexCommand(VeDirectHexCommand cmd, VeDirectHexRegister addr, uint32_t value, uint8_t valsize) { diff --git a/lib/VeDirectFrameHandler/VeDirectMpptController.cpp b/lib/VeDirectFrameHandler/VeDirectMpptController.cpp index 6630a4cea..6c7af8902 100644 --- a/lib/VeDirectFrameHandler/VeDirectMpptController.cpp +++ b/lib/VeDirectFrameHandler/VeDirectMpptController.cpp @@ -104,35 +104,23 @@ void VeDirectMpptController::frameValidEvent() { } else { _tmpFrame.mpptEfficiency_Percent = 0.0f; } - - if (!_canSend) { return; } - - // Copy from the "VE.Direct Protocol" documentation - // For firmware version v1.52 and below, when no VE.Direct queries are sent to the device, the - // charger periodically sends human readable (TEXT) data to the serial port. For firmware - // versions v1.53 and above, the charger always periodically sends TEXT data to the serial port. - // --> We just use hex commandes for firmware >= 1.53 to keep text messages alive - if (_tmpFrame.getFwVersionAsInteger() < 153) { return; } - - using Command = VeDirectHexCommand; - using Register = VeDirectHexRegister; - - sendHexCommand(Command::GET, Register::ChargeControllerTemperature); - sendHexCommand(Command::GET, Register::SmartBatterySenseTemperature); - sendHexCommand(Command::GET, Register::NetworkTotalDcInputPower); - -#ifdef PROCESS_NETWORK_STATE - sendHexCommand(Command::GET, Register::NetworkInfo); - sendHexCommand(Command::GET, Register::NetworkMode); - sendHexCommand(Command::GET, Register::NetworkStatus); -#endif // PROCESS_NETWORK_STATE } void VeDirectMpptController::loop() { + // First we send HEX-Commands (timing improvement) + if (isHexCommandPossible()) { + sendNextHexCommandFromQueue(); + } + + // Second we read Text- and HEX-Messages VeDirectFrameHandler::loop(); + // Third we check if HEX-Data is outdated + // Note: Room for improvement, longer data valid time for slow changing values? + if (!isHexCommandPossible()) { return; } + auto resetTimestamp = [this](auto& pair) { if (pair.first > 0 && (millis() - pair.first) > (10 * 1000)) { pair.first = 0; @@ -142,6 +130,8 @@ void VeDirectMpptController::loop() resetTimestamp(_tmpFrame.MpptTemperatureMilliCelsius); resetTimestamp(_tmpFrame.SmartBatterySenseTemperatureMilliCelsius); resetTimestamp(_tmpFrame.NetworkTotalDcInputPowerMilliWatts); + resetTimestamp(_tmpFrame.BatteryFloatMilliVolt); + resetTimestamp(_tmpFrame.BatteryAbsorptionMilliVolt); #ifdef PROCESS_NETWORK_STATE resetTimestamp(_tmpFrame.NetworkInfo); @@ -153,8 +143,8 @@ void VeDirectMpptController::loop() /* * hexDataHandler() - * analyse the content of VE.Direct hex messages - * Handels the received hex data from the MPPT + * analyze the content of VE.Direct hex messages + * handles the received hex data from the MPPT */ bool VeDirectMpptController::hexDataHandler(VeDirectHexData const &data) { if (data.rsp != VeDirectHexResponse::GET && @@ -162,6 +152,11 @@ bool VeDirectMpptController::hexDataHandler(VeDirectHexData const &data) { auto regLog = static_cast(data.addr); + // we check whether the answer matches a previously asked query + if ((data.rsp == VeDirectHexResponse::GET) && (data.addr == _hexQueue[_sendQueueNr]._hexRegister)) { + _sendTimeout = 0; + } + switch (data.addr) { case VeDirectHexRegister::ChargeControllerTemperature: _tmpFrame.MpptTemperatureMilliCelsius = @@ -215,6 +210,29 @@ bool VeDirectMpptController::hexDataHandler(VeDirectHexData const &data) { return true; break; + case VeDirectHexRegister::BatteryAbsorptionVoltage: + _tmpFrame.BatteryAbsorptionMilliVolt = + { millis(), static_cast(data.value) * 10 }; + if (_verboseLogging) { + _msgOut->printf("%s Hex Data: MPPT Absorption Voltage (0x%04X): %.2fV\r\n", + _logId, regLog, + _tmpFrame.BatteryAbsorptionMilliVolt.second / 1000.0); + } + return true; + break; + + case VeDirectHexRegister::BatteryFloatVoltage: + _tmpFrame.BatteryFloatMilliVolt = + { millis(), static_cast(data.value) * 10 }; + + if (_verboseLogging) { + _msgOut->printf("%s Hex Data: MPPT Float Voltage (0x%04X): %.2fV\r\n", + _logId, regLog, + _tmpFrame.BatteryFloatMilliVolt.second / 1000.0); + } + return true; + break; + #ifdef PROCESS_NETWORK_STATE case VeDirectHexRegister::NetworkInfo: _tmpFrame.NetworkInfo = @@ -257,3 +275,66 @@ bool VeDirectMpptController::hexDataHandler(VeDirectHexData const &data) { return false; } + + +/* + * isHexCommandPossible() + * return: true = yes we can use Hex-Commands + */ +bool VeDirectMpptController::isHexCommandPossible(void) { + // Copy from the "VE.Direct Protocol" documentation + // For firmware version v1.52 and below, when no VE.Direct queries are sent to the device, the + // charger periodically sends human readable (TEXT) data to the serial port. For firmware + // versions v1.53 and above, the charger always periodically sends TEXT data to the serial port. + // --> We just use hex commands for firmware >= 1.53 to keep text messages alive + return (_canSend && (_tmpFrame.getFwVersionAsInteger() >= 153)); +} + + +/* + * sendNextHexCommandFromQueue() + * send one Hex Commands from the Hex Command Queue + * handles the received hex data from the MPPT + */ +void VeDirectMpptController::sendNextHexCommandFromQueue(void) { + // It seems some commands get lost if we send to fast the next command. + // maybe we produce an overflow on the MPPT receive buffer or we have to + // wait for the MPPT answer before we can send the next command. We only + // send a new query in VE.Direct idle state and if no query is pending. In + // case we do not get an answer we send the next query from the queue after + // a timeout of 500ms. NOTE: _sendTimeout will be set to 0 after receiving + // an answer, see function hexDataHandler(). + auto millisTime = millis(); + if (isStateIdle() && ((millisTime - _hexQueue[_sendQueueNr]._lastSendTime) > _sendTimeout)) { + + // we do 2 loops, first for high prio commands and second for low prio commands + bool prio = true; + for (auto idy = 0; idy < 2; ++idy) { + + // we start searching the queue with the next queue index + auto idx = _sendQueueNr + 1; + if (idx >= _hexQueue.size()) { idx = 0; } + + do { + // we check if it is time to send the command again + if (((prio && (_hexQueue[idx]._readPeriod == HIGH_PRIO_COMMAND)) || + (!prio && (_hexQueue[idx]._readPeriod != HIGH_PRIO_COMMAND))) && + (millisTime - _hexQueue[idx]._lastSendTime) > (_hexQueue[idx]._readPeriod * 1000)) { + + sendHexCommand(VeDirectHexCommand::GET, _hexQueue[idx]._hexRegister); + _hexQueue[idx]._lastSendTime = millisTime; + + // we need this information to check if we get an answer, see hexDataHandler() + _sendTimeout = 500; + _sendQueueNr = idx; + return; + } + + ++idx; + if (idx == _hexQueue.size()) { idx = 0; } + } while (idx != _sendQueueNr); + + prio = false; // second loop for low prio commands + } + } +} \ No newline at end of file diff --git a/lib/VeDirectFrameHandler/VeDirectMpptController.h b/lib/VeDirectFrameHandler/VeDirectMpptController.h index b8bd4c72f..ac3b9122a 100644 --- a/lib/VeDirectFrameHandler/VeDirectMpptController.h +++ b/lib/VeDirectFrameHandler/VeDirectMpptController.h @@ -36,6 +36,12 @@ class MovingAverage { size_t _count; }; +struct VeDirectHexQueue { + VeDirectHexRegister _hexRegister; // hex register + uint8_t _readPeriod; // time period in sec until we send the command again + uint32_t _lastSendTime; // time stamp in milli sec of last send +}; + class VeDirectMpptController : public VeDirectFrameHandler { public: VeDirectMpptController() = default; @@ -51,5 +57,18 @@ class VeDirectMpptController : public VeDirectFrameHandler { bool hexDataHandler(VeDirectHexData const &data) final; bool processTextDataDerived(std::string const& name, std::string const& value) final; void frameValidEvent() final; + void sendNextHexCommandFromQueue(void); + bool isHexCommandPossible(void); MovingAverage _efficiency; + + uint32_t _sendTimeout = 0; // timeout until we send the next command from the queue + size_t _sendQueueNr = 0; // actual queue position; + + // for slow changing values we use a send time period of 4 sec + #define HIGH_PRIO_COMMAND 1 + std::array _hexQueue { VeDirectHexRegister::NetworkTotalDcInputPower, HIGH_PRIO_COMMAND, 0, + VeDirectHexRegister::ChargeControllerTemperature, 4, 0, + VeDirectHexRegister::SmartBatterySenseTemperature, 4, 0, + VeDirectHexRegister::BatteryFloatVoltage, 4, 0, + VeDirectHexRegister::BatteryAbsorptionVoltage, 4, 0 }; }; diff --git a/src/MqttHandleVedirect.cpp b/src/MqttHandleVedirect.cpp index c244afc68..009519dd7 100644 --- a/src/MqttHandleVedirect.cpp +++ b/src/MqttHandleVedirect.cpp @@ -138,6 +138,8 @@ void MqttHandleVedirectClass::publish_mppt_data(const VeDirectMpptController::da PUBLISH_OPT(NetworkTotalDcInputPowerMilliWatts, "NetworkTotalDcInputPower", currentData.NetworkTotalDcInputPowerMilliWatts.second / 1000.0); PUBLISH_OPT(MpptTemperatureMilliCelsius, "MpptTemperature", currentData.MpptTemperatureMilliCelsius.second / 1000.0); + PUBLISH_OPT(BatteryAbsorptionMilliVolt, "BatteryAbsorption", currentData.BatteryAbsorptionMilliVolt.second / 1000.0); + PUBLISH_OPT(BatteryFloatMilliVolt, "BatteryFloat", currentData.BatteryFloatMilliVolt.second / 1000.0); PUBLISH_OPT(SmartBatterySenseTemperatureMilliCelsius, "SmartBatterySenseTemperature", currentData.SmartBatterySenseTemperatureMilliCelsius.second / 1000.0); #undef PUBLILSH_OPT } diff --git a/src/MqttHandleVedirectHass.cpp b/src/MqttHandleVedirectHass.cpp index 883705ad9..1ea1c8066 100644 --- a/src/MqttHandleVedirectHass.cpp +++ b/src/MqttHandleVedirectHass.cpp @@ -95,6 +95,12 @@ void MqttHandleVedirectHassClass::publishConfig() if (optMpptData->MpptTemperatureMilliCelsius.first != 0) { publishSensor("MPPT temperature", "mdi:temperature-celsius", "MpptTemperature", "temperature", "measurement", "°C", *optMpptData); } + if (optMpptData->BatteryAbsorptionMilliVolt.first != 0) { + publishSensor("Battery absorption voltage", "mdi:battery-charging-90", "BatteryAbsorption", "voltage", "measurement", "V", *optMpptData); + } + if (optMpptData->BatteryFloatMilliVolt.first != 0) { + publishSensor("Battery float voltage", "mdi:battery-charging-100", "BatteryFloat", "voltage", "measurement", "V", *optMpptData); + } if (optMpptData->SmartBatterySenseTemperatureMilliCelsius.first != 0) { publishSensor("Smart Battery Sense temperature", "mdi:temperature-celsius", "SmartBatterySenseTemperature", "temperature", "measurement", "°C", *optMpptData); } diff --git a/src/VictronMppt.cpp b/src/VictronMppt.cpp index a994e20d6..4596e6dcb 100644 --- a/src/VictronMppt.cpp +++ b/src/VictronMppt.cpp @@ -226,3 +226,39 @@ float VictronMpptClass::getOutputVoltage() const return min; } + +std::optional VictronMpptClass::getStateOfOperation() const +{ + for (const auto& upController : _controllers) { + if (upController->isDataValid()) { + return upController->getData().currentState_CS; + } + } + + return std::nullopt; +} + +std::optional VictronMpptClass::getVoltage(MPPTVoltage kindOf) const +{ + for (const auto& upController : _controllers) { + switch (kindOf) { + case MPPTVoltage::ABSORPTION: { + auto const& absorptionVoltage = upController->getData().BatteryAbsorptionMilliVolt; + if (absorptionVoltage.first > 0) { return absorptionVoltage.second; } + break; + } + case MPPTVoltage::FLOAT: { + auto const& floatVoltage = upController->getData().BatteryFloatMilliVolt; + if (floatVoltage.first > 0) { return floatVoltage.second; } + break; + } + case MPPTVoltage::BATTERY: { + auto const& batteryVoltage = upController->getData().batteryVoltage_V_mV; + if (upController->isDataValid()) { return batteryVoltage; } + break; + } + } + } + + return std::nullopt; +} diff --git a/src/WebApi_ws_vedirect_live.cpp b/src/WebApi_ws_vedirect_live.cpp index 2a539af79..75b332a81 100644 --- a/src/WebApi_ws_vedirect_live.cpp +++ b/src/WebApi_ws_vedirect_live.cpp @@ -195,6 +195,16 @@ void WebApiWsVedirectLiveClass::populateJson(const JsonObject &root, const VeDir output["SBSTemperature"]["u"] = "°C"; output["SBSTemperature"]["d"] = "0"; } + if (mpptData.BatteryAbsorptionMilliVolt.first > 0) { + output["AbsorptionVoltage"]["v"] = mpptData.BatteryAbsorptionMilliVolt.second / 1000.0; + output["AbsorptionVoltage"]["u"] = "V"; + output["AbsorptionVoltage"]["d"] = "2"; + } + if (mpptData.BatteryFloatMilliVolt.first > 0) { + output["FloatVoltage"]["v"] = mpptData.BatteryFloatMilliVolt.second / 1000.0; + output["FloatVoltage"]["u"] = "V"; + output["FloatVoltage"]["d"] = "2"; + } const JsonObject input = values["input"].to(); if (mpptData.NetworkTotalDcInputPowerMilliWatts.first > 0) { diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 6bf5f9776..ae13e2c11 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -188,6 +188,8 @@ "V": "Spannung", "I": "Strom", "E": "Effizienz (berechnet)", + "AbsorptionVoltage": "Absorptionsspannung", + "FloatVoltage": "Erhaltungsspannung", "SBSTemperature": "SBS Temperatur" }, "section_input": "Eingang (Solarpanele)", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 2ade82335..5e76c52ff 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -188,6 +188,8 @@ "V": "Voltage", "I": "Current", "E": "Efficiency (calculated)", + "AbsorptionVoltage": "Absorption voltage", + "FloatVoltage": "Float voltage", "SBSTemperature": "SBS temperature" }, "section_input": "Input (Solar Panels)", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index 58e157962..37e107ac1 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -188,6 +188,8 @@ "V": "Voltage", "I": "Current", "E": "Efficiency (calculated)", + "AbsorptionVoltage": "Absorption voltage", + "FloatVoltage": "Float voltage", "SBSTemperature": "SBS temperature" }, "section_input": "Input (Solar Panels)",