diff --git a/include/VictronMppt.h b/include/VictronMppt.h index 963a46cd9..c43fbb3cc 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 + int16_t getStateOfOperation() const; + + // the configured value from the first available controller in V + enum class MPPTVoltage : uint8_t { + ABSORPTION = 0, + FLOAT = 1, + BATTERY = 2 + }; + float 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..f9bd90490 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,67 @@ 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 + * handel's 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..7f57b9140 100644 --- a/src/VictronMppt.cpp +++ b/src/VictronMppt.cpp @@ -226,3 +226,51 @@ float VictronMpptClass::getOutputVoltage() const return min; } + +/* + * getStateOfOperation() + * return: the state from the first available controller or + * -1 if data is not available + */ +int16_t VictronMpptClass::getStateOfOperation() const +{ + for (const auto& upController : _controllers) { + if (upController->isDataValid()) { + return static_cast(upController->getData().currentState_CS); + } + } + + return -1; +} + +/* + * getVoltage() + * return: the configured value from the first available controller in V or + * -1V if data is not available + */ +float VictronMpptClass::getVoltage(MPPTVoltage kindOf) const +{ + std::pair voltX {0,0}; + + for (const auto& upController : _controllers) { + switch (kindOf) { + case MPPTVoltage::ABSORPTION: + voltX = upController->getData().BatteryAbsorptionMilliVolt; + break; + case MPPTVoltage::FLOAT: + voltX = upController->getData().BatteryFloatMilliVolt; + break; + case MPPTVoltage::BATTERY: + if (upController->isDataValid()) { + voltX.first = 1; + voltX.second = upController->getData().batteryVoltage_V_mV; + } + break; + } + if (voltX.first > 0) { + return static_cast(voltX.second / 1000.0); + } + } + + return -1.0f; +} 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)",