Skip to content

Commit

Permalink
Feature: retrieve absorption and float voltage from Victron MPPTs
Browse files Browse the repository at this point in the history
  • Loading branch information
SW-Niko authored and schlimmchen committed Oct 24, 2024
1 parent cfb5c3f commit 780ac51
Show file tree
Hide file tree
Showing 14 changed files with 240 additions and 35 deletions.
11 changes: 11 additions & 0 deletions include/VictronMppt.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
15 changes: 12 additions & 3 deletions lib/VeDirectFrameHandler/VeDirectData.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down Expand Up @@ -287,18 +287,27 @@ frozen::string const& VeDirectHexData::getResponseAsString() const
frozen::string const& VeDirectHexData::getRegisterAsString() const
{
using Register = VeDirectHexRegister;
static constexpr frozen::map<Register, frozen::string, 11> values = {
static constexpr frozen::map<Register, frozen::string, 20> 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);
Expand Down
13 changes: 12 additions & 1 deletion lib/VeDirectFrameHandler/VeDirectData.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ struct veMpptStruct : veStruct {
std::pair<uint32_t, int32_t> MpptTemperatureMilliCelsius;
std::pair<uint32_t, int32_t> SmartBatterySenseTemperatureMilliCelsius;
std::pair<uint32_t, uint32_t> NetworkTotalDcInputPowerMilliWatts;
std::pair<uint32_t, uint32_t> BatteryAbsorptionMilliVolt;
std::pair<uint32_t, uint32_t> BatteryFloatMilliVolt;
std::pair<uint32_t, uint8_t> NetworkInfo;
std::pair<uint32_t, uint8_t> NetworkMode;
std::pair<uint32_t, uint8_t> NetworkStatus;
Expand Down Expand Up @@ -121,15 +123,24 @@ enum class VeDirectHexRegister : uint16_t {
DeviceState = 0x0201,
RemoteControlUsed = 0x0202,
PanelVoltage = 0xEDBB,
PanelPower = 0xEDBC,
ChargerVoltage = 0xEDD5,
ChargerCurrent = 0xEDD7,
NetworkTotalDcInputPower = 0x2027,
ChargeControllerTemperature = 0xEDDB,
SmartBatterySenseTemperature = 0xEDEC,
NetworkInfo = 0x200D,
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 {
Expand Down
3 changes: 2 additions & 1 deletion lib/VeDirectFrameHandler/VeDirectFrameHandler.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
12 changes: 6 additions & 6 deletions lib/VeDirectFrameHandler/VeDirectFrameHexHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<typename T>
bool VeDirectFrameHandler<T>::disassembleHexData(VeDirectHexData &data) {
Expand Down Expand Up @@ -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<typename T>
bool VeDirectFrameHandler<T>::sendHexCommand(VeDirectHexCommand cmd, VeDirectHexRegister addr, uint32_t value, uint8_t valsize) {
Expand Down
130 changes: 106 additions & 24 deletions lib/VeDirectFrameHandler/VeDirectMpptController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -153,15 +143,20 @@ 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 &&
data.rsp != VeDirectHexResponse::ASYNC) { return false; }

auto regLog = static_cast<uint16_t>(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 =
Expand Down Expand Up @@ -215,6 +210,29 @@ bool VeDirectMpptController::hexDataHandler(VeDirectHexData const &data) {
return true;
break;

case VeDirectHexRegister::BatteryAbsorptionVoltage:
_tmpFrame.BatteryAbsorptionMilliVolt =
{ millis(), static_cast<uint32_t>(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<uint32_t>(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 =
Expand Down Expand Up @@ -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
}
}
}
19 changes: 19 additions & 0 deletions lib/VeDirectFrameHandler/VeDirectMpptController.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<veMpptStruct> {
public:
VeDirectMpptController() = default;
Expand All @@ -51,5 +57,18 @@ class VeDirectMpptController : public VeDirectFrameHandler<veMpptStruct> {
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<float, 5> _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<VeDirectHexQueue, 5> _hexQueue { VeDirectHexRegister::NetworkTotalDcInputPower, HIGH_PRIO_COMMAND, 0,
VeDirectHexRegister::ChargeControllerTemperature, 4, 0,
VeDirectHexRegister::SmartBatterySenseTemperature, 4, 0,
VeDirectHexRegister::BatteryFloatVoltage, 4, 0,
VeDirectHexRegister::BatteryAbsorptionVoltage, 4, 0 };
};
2 changes: 2 additions & 0 deletions src/MqttHandleVedirect.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
6 changes: 6 additions & 0 deletions src/MqttHandleVedirectHass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Loading

0 comments on commit 780ac51

Please sign in to comment.