Skip to content

Commit

Permalink
Feature: retrieve absorption and float voltage from Victron MPPTs (ho…
Browse files Browse the repository at this point in the history
…ylabs#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.
  • Loading branch information
SW-Niko authored Oct 25, 2024
1 parent cfb5c3f commit 3c1d3f7
Show file tree
Hide file tree
Showing 14 changed files with 227 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
std::optional<uint8_t> 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<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
129 changes: 105 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,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
}
}
}
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 3c1d3f7

Please sign in to comment.