// SPDX-License-Identifier: GPL-2.0-or-later #include "VictronMppt.h" #include "Configuration.h" #include "PinMapping.h" #include "MessageOutput.h" #include "SerialPortManager.h" VictronMpptClass VictronMppt; void VictronMpptClass::init(Scheduler& scheduler) { scheduler.addTask(_loopTask); _loopTask.setCallback([this] { loop(); }); _loopTask.setIterations(TASK_FOREVER); _loopTask.enable(); this->updateSettings(); } void VictronMpptClass::updateSettings() { std::lock_guard lock(_mutex); _controllers.clear(); SerialPortManager.invalidateMpptPorts(); CONFIG_T& config = Configuration.get(); if (!config.Vedirect.Enabled) { return; } const PinMapping_t& pin = PinMapping.get(); // HW UART 1 has always been the designated UART to connect a Victron MPPT if (!initController(pin.victron_rx, pin.victron_tx, config.Vedirect.VerboseLogging, 1, 1)) { return; } // HW UART 2 conflicts with the SDM power meter and the battery interface if (!initController(pin.victron_rx2, pin.victron_tx2, config.Vedirect.VerboseLogging, 2, 2)) { return; } // HW UART 0 is only available on ESP32-S3 with logging over USB CDC, and // furthermore still conflicts with the battery interface in that case initController(pin.victron_rx3, pin.victron_tx3, config.Vedirect.VerboseLogging, 3, 0); } bool VictronMpptClass::initController(int8_t rx, int8_t tx, bool logging, uint8_t instance, uint8_t hwSerialPort) { MessageOutput.printf("[VictronMppt Instance %d] rx = %d, tx = %d, " "hwSerialPort = %d\r\n", instance, rx, tx, hwSerialPort); if (rx < 0) { MessageOutput.printf("[VictronMppt Instance %d] invalid pin config\r\n", instance); return false; } if (!SerialPortManager.allocateMpptPort(hwSerialPort)) { return false; } auto upController = std::make_unique(); upController->init(rx, tx, &MessageOutput, logging, hwSerialPort); _controllers.push_back(std::move(upController)); return true; } void VictronMpptClass::loop() { std::lock_guard lock(_mutex); for (auto const& upController : _controllers) { upController->loop(); } } bool VictronMpptClass::isDataValid() const { std::lock_guard lock(_mutex); for (auto const& upController: _controllers) { if (!upController->isDataValid()) { return false; } } return !_controllers.empty(); } bool VictronMpptClass::isDataValid(size_t idx) const { std::lock_guard lock(_mutex); if (_controllers.empty() || idx >= _controllers.size()) { return false; } return _controllers[idx]->isDataValid(); } uint32_t VictronMpptClass::getDataAgeMillis() const { std::lock_guard lock(_mutex); if (_controllers.empty()) { return 0; } auto now = millis(); auto iter = _controllers.cbegin(); uint32_t age = now - (*iter)->getLastUpdate(); ++iter; while (iter != _controllers.end()) { age = std::min(age, now - (*iter)->getLastUpdate()); ++iter; } return age; } uint32_t VictronMpptClass::getDataAgeMillis(size_t idx) const { std::lock_guard lock(_mutex); if (_controllers.empty() || idx >= _controllers.size()) { return 0; } return millis() - _controllers[idx]->getLastUpdate(); } std::optional VictronMpptClass::getData(size_t idx) const { std::lock_guard lock(_mutex); if (_controllers.empty() || idx >= _controllers.size()) { MessageOutput.printf("ERROR: MPPT controller index %d is out of bounds (%d controllers)\r\n", idx, _controllers.size()); return std::nullopt; } if (!_controllers[idx]->isDataValid()) { return std::nullopt; } return _controllers[idx]->getData(); } int32_t VictronMpptClass::getPowerOutputWatts() const { int32_t sum = 0; for (const auto& upController : _controllers) { if (!upController->isDataValid()) { continue; } // if any charge controller is part of a VE.Smart network, and if the // charge controller is connected in a way that allows to send // requests, we should have the "network total DC input power" // available. if so, to estimate the output power, we multiply by // the calculated efficiency of the connected charge controller. auto networkPower = upController->getData().NetworkTotalDcInputPowerMilliWatts; if (networkPower.first > 0) { return static_cast(networkPower.second / 1000.0 * upController->getData().mpptEfficiency_Percent / 100); } sum += upController->getData().batteryOutputPower_W; } return sum; } int32_t VictronMpptClass::getPanelPowerWatts() const { int32_t sum = 0; for (const auto& upController : _controllers) { if (!upController->isDataValid()) { continue; } // if any charge controller is part of a VE.Smart network, and if the // charge controller is connected in a way that allows to send // requests, we should have the "network total DC input power" available. auto networkPower = upController->getData().NetworkTotalDcInputPowerMilliWatts; if (networkPower.first > 0) { return static_cast(networkPower.second / 1000.0); } sum += upController->getData().panelPower_PPV_W; } return sum; } float VictronMpptClass::getYieldTotal() const { float sum = 0; for (const auto& upController : _controllers) { if (!upController->isDataValid()) { continue; } sum += upController->getData().yieldTotal_H19_Wh / 1000.0; } return sum; } float VictronMpptClass::getYieldDay() const { float sum = 0; for (const auto& upController : _controllers) { if (!upController->isDataValid()) { continue; } sum += upController->getData().yieldToday_H20_Wh / 1000.0; } return sum; } float VictronMpptClass::getOutputVoltage() const { float min = -1; for (const auto& upController : _controllers) { if (!upController->isDataValid()) { continue; } float volts = upController->getData().batteryVoltage_V_mV / 1000.0; if (min == -1) { min = volts; } min = std::min(min, volts); } return min; }