/* VeDirectMpptController.cpp * * * 2020.08.20 - 0.0 - ??? * 2024.03.18 - 0.1 - add of: - temperature from "Smart Battery Sense" connected over VE.Smart network * - temperature from internal MPPT sensor * - "total DC input power" from MPPT's connected over VE.Smart network */ #include #include "VeDirectMpptController.h" //#define PROCESS_NETWORK_STATE void VeDirectMpptController::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint8_t hwSerialPort) { VeDirectFrameHandler::init("MPPT", rx, tx, msgOut, verboseLogging, hwSerialPort); } bool VeDirectMpptController::processTextDataDerived(std::string const& name, std::string const& value) { if (name == "IL") { _tmpFrame.loadCurrent_IL_mA = atol(value.c_str()); return true; } if (name == "LOAD") { _tmpFrame.loadOutputState_LOAD = (value == "ON"); return true; } if (name == "CS") { _tmpFrame.currentState_CS = atoi(value.c_str()); return true; } if (name == "ERR") { _tmpFrame.errorCode_ERR = atoi(value.c_str()); return true; } if (name == "OR") { _tmpFrame.offReason_OR = strtol(value.c_str(), nullptr, 0); return true; } if (name == "MPPT") { _tmpFrame.stateOfTracker_MPPT = atoi(value.c_str()); return true; } if (name == "HSDS") { _tmpFrame.daySequenceNr_HSDS = atoi(value.c_str()); return true; } if (name == "VPV") { _tmpFrame.panelVoltage_VPV_mV = atol(value.c_str()); return true; } if (name == "PPV") { _tmpFrame.panelPower_PPV_W = atoi(value.c_str()); return true; } if (name == "H19") { _tmpFrame.yieldTotal_H19_Wh = atol(value.c_str()) * 10; return true; } if (name == "H20") { _tmpFrame.yieldToday_H20_Wh = atol(value.c_str()) * 10; return true; } if (name == "H21") { _tmpFrame.maxPowerToday_H21_W = atoi(value.c_str()); return true; } if (name == "H22") { _tmpFrame.yieldYesterday_H22_Wh = atol(value.c_str()) * 10; return true; } if (name == "H23") { _tmpFrame.maxPowerYesterday_H23_W = atoi(value.c_str()); return true; } return false; } /* * frameValidEvent * This function is called at the end of the received frame. */ void VeDirectMpptController::frameValidEvent() { // power into the battery, (+) means charging, (-) means discharging _tmpFrame.batteryOutputPower_W = static_cast((_tmpFrame.batteryVoltage_V_mV / 1000.0f) * (_tmpFrame.batteryCurrent_I_mA / 1000.0f)); // calculation of the panel current if ((_tmpFrame.panelVoltage_VPV_mV > 0) && (_tmpFrame.panelPower_PPV_W >= 1)) { _tmpFrame.panelCurrent_mA = static_cast(_tmpFrame.panelPower_PPV_W * 1000000.0f / _tmpFrame.panelVoltage_VPV_mV); } else { _tmpFrame.panelCurrent_mA = 0; } // calculation of the MPPT efficiency float totalPower_W = (_tmpFrame.loadCurrent_IL_mA / 1000.0f + _tmpFrame.batteryCurrent_I_mA / 1000.0f) * _tmpFrame.batteryVoltage_V_mV /1000.0f; if (_tmpFrame.panelPower_PPV_W > 0) { _efficiency.addNumber(totalPower_W * 100.0f / _tmpFrame.panelPower_PPV_W); _tmpFrame.mpptEfficiency_Percent = _efficiency.getAverage(); } else { _tmpFrame.mpptEfficiency_Percent = 0.0f; } } 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; } }; resetTimestamp(_tmpFrame.MpptTemperatureMilliCelsius); resetTimestamp(_tmpFrame.SmartBatterySenseTemperatureMilliCelsius); resetTimestamp(_tmpFrame.NetworkTotalDcInputPowerMilliWatts); resetTimestamp(_tmpFrame.BatteryFloatMilliVolt); resetTimestamp(_tmpFrame.BatteryAbsorptionMilliVolt); #ifdef PROCESS_NETWORK_STATE resetTimestamp(_tmpFrame.NetworkInfo); resetTimestamp(_tmpFrame.NetworkMode); resetTimestamp(_tmpFrame.NetworkStatus); #endif // PROCESS_NETWORK_STATE } /* * hexDataHandler() * 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(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 = { millis(), static_cast(data.value) * 10 }; if (_verboseLogging) { _msgOut->printf("%s Hex Data: MPPT Temperature (0x%04X): %.2f°C\r\n", _logId, regLog, _tmpFrame.MpptTemperatureMilliCelsius.second / 1000.0); } return true; break; case VeDirectHexRegister::SmartBatterySenseTemperature: if (data.value == 0xFFFF) { if (_verboseLogging) { _msgOut->printf("%s Hex Data: Smart Battery Sense Temperature is not available\r\n", _logId); } return true; // we know what to do with it, and we decided to ignore the value } _tmpFrame.SmartBatterySenseTemperatureMilliCelsius = { millis(), static_cast(data.value) * 10 - 273150 }; if (_verboseLogging) { _msgOut->printf("%s Hex Data: Smart Battery Sense Temperature (0x%04X): %.2f°C\r\n", _logId, regLog, _tmpFrame.SmartBatterySenseTemperatureMilliCelsius.second / 1000.0); } return true; break; case VeDirectHexRegister::NetworkTotalDcInputPower: if (data.value == 0xFFFFFFFF) { if (_verboseLogging) { _msgOut->printf("%s Hex Data: Network total DC power value " "indicates non-networked controller\r\n", _logId); } _tmpFrame.NetworkTotalDcInputPowerMilliWatts = { 0, 0 }; return true; // we know what to do with it, and we decided to ignore the value } _tmpFrame.NetworkTotalDcInputPowerMilliWatts = { millis(), data.value * 10 }; if (_verboseLogging) { _msgOut->printf("%s Hex Data: Network Total DC Power (0x%04X): %.2fW\r\n", _logId, regLog, _tmpFrame.NetworkTotalDcInputPowerMilliWatts.second / 1000.0); } 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 = { millis(), static_cast(data.value) }; if (_verboseLogging) { _msgOut->printf("%s Hex Data: Network Info (0x%04X): 0x%X\r\n", _logId, regLog, data.value); } return true; break; case VeDirectHexRegister::NetworkMode: _tmpFrame.NetworkMode = { millis(), static_cast(data.value) }; if (_verboseLogging) { _msgOut->printf("%s Hex Data: Network Mode (0x%04X): 0x%X\r\n", _logId, regLog, data.value); } return true; break; case VeDirectHexRegister::NetworkStatus: _tmpFrame.NetworkStatus = { millis(), static_cast(data.value) }; if (_verboseLogging) { _msgOut->printf("%s Hex Data: Network Status (0x%04X): 0x%X\r\n", _logId, regLog, data.value); } return true; break; #endif // PROCESS_NETWORK_STATE default: return false; break; } 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 } } }