Feature: Support for second Victron MPPT charge controller

this change adds support for a second Victron MPPT charge controller
using a second serial connection.

* Add device configuration for a second victron mppt
* Update VedirectView for second victron mppt
* Update MqttHandleVedirect for second victron mppt
* Update MqttHandleVedirectHass for second victron mppt
* Handle nonexisting victron controllers with optionals
* Add bool-function to Battery and inherited classes, if uart port 2 is
  being used
* Introduced a serial port manager. In order to prevent the battery and
  the Victron MPPT to use the same hw serial ports, this class keeps
  track of the used ports and their owners.
This commit is contained in:
Arman Vartan 2024-02-26 01:27:13 +01:00 committed by Bernhard Kirchen
parent 21c19f4b7f
commit 75541be248
28 changed files with 456 additions and 236 deletions

View File

@ -1,7 +1,6 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <stdint.h>
#include <memory>
#include <mutex>
#include <TaskSchedulerDeclarations.h>
@ -12,10 +11,10 @@ class BatteryProvider {
public:
// returns true if the provider is ready for use, false otherwise
virtual bool init(bool verboseLogging) = 0;
virtual void deinit() = 0;
virtual void loop() = 0;
virtual std::shared_ptr<BatteryStats> getStats() const = 0;
virtual bool usesHwPort2() = 0;
};
class BatteryClass {
@ -24,11 +23,11 @@ class BatteryClass {
void updateSettings();
std::shared_ptr<BatteryStats const> getStats() const;
private:
void loop();
Task _loopTask;
mutable std::mutex _mutex;
std::unique_ptr<BatteryProvider> _upProvider = nullptr;
};

View File

@ -30,6 +30,8 @@
#define DEV_MAX_MAPPING_NAME_STRLEN 63
#define VICTRON_MAX_COUNT 2
#define POWERMETER_MAX_PHASES 3
#define POWERMETER_MAX_HTTP_URL_STRLEN 1024
#define POWERMETER_MAX_USERNAME_STRLEN 64

View File

@ -19,6 +19,7 @@ class Controller : public BatteryProvider {
void deinit() final;
void loop() final;
std::shared_ptr<BatteryStats> getStats() const final { return _stats; }
bool usesHwPort2() override;
private:
enum class Status : unsigned {

View File

@ -12,6 +12,7 @@ class MqttBattery : public BatteryProvider {
void deinit() final;
void loop() final { return; } // this class is event-driven
std::shared_ptr<BatteryStats> getStats() const final { return _stats; }
bool usesHwPort2() override;
private:
bool _verboseLogging = false;

View File

@ -4,6 +4,7 @@
#include "VeDirectMpptController.h"
#include "Configuration.h"
#include <Arduino.h>
#include <map>
#include <TaskSchedulerDeclarations.h>
#ifndef VICTRON_PIN_RX
@ -20,7 +21,7 @@ public:
void forceUpdate();
private:
void loop();
VeDirectMpptController::veMpptStruct _kvFrame{};
std::map<std::string, VeDirectMpptController::veMpptStruct> _kvFrames;
Task _loopTask;
@ -31,6 +32,9 @@ private:
uint32_t _nextPublishFull = 1;
bool _PublishFull;
void publish_mppt_data(const VeDirectMpptController::spData_t &spMpptData,
VeDirectMpptController::veMpptStruct &frame) const;
};
extern MqttHandleVedirectClass MqttHandleVedirect;

View File

@ -14,9 +14,15 @@ public:
private:
void loop();
void publish(const String& subtopic, const String& payload);
void publishBinarySensor(const char* caption, const char* icon, const char* subTopic, const char* payload_on, const char* payload_off);
void publishSensor(const char* caption, const char* icon, const char* subTopic, const char* deviceClass = NULL, const char* stateClass = NULL, const char* unitOfMeasurement = NULL);
void createDeviceInfo(JsonObject& object);
void publishBinarySensor(const char *caption, const char *icon, const char *subTopic,
const char *payload_on, const char *payload_off,
const VeDirectMpptController::spData_t &spMpptData);
void publishSensor(const char *caption, const char *icon, const char *subTopic,
const char *deviceClass, const char *stateClass,
const char *unitOfMeasurement,
const VeDirectMpptController::spData_t &spMpptData);
void createDeviceInfo(JsonObject &object,
const VeDirectMpptController::spData_t &spMpptData);
Task _loopTask;

View File

@ -40,6 +40,8 @@ struct PinMapping_t {
uint8_t display_reset;
int8_t victron_tx;
int8_t victron_rx;
int8_t victron_tx2;
int8_t victron_rx2;
int8_t battery_rx;
int8_t battery_rxen;
int8_t battery_tx;

View File

@ -14,6 +14,7 @@ public:
void deinit() final;
void loop() final;
std::shared_ptr<BatteryStats> getStats() const final { return _stats; }
bool usesHwPort2() override;
private:
uint16_t readUnsignedInt16(uint8_t *data);

View File

@ -0,0 +1,27 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <map>
class SerialPortManager {
public:
bool allocateMpptPort(int port);
bool allocateBatteryPort(int port);
void invalidateBatteryPort();
void invalidateMpptPorts();
private:
enum Owner {
BATTERY,
MPPT
};
std::map<uint8_t, Owner> allocatedPorts;
bool allocatePort(uint8_t port, Owner owner);
void invalidate(Owner owner);
static const char* print(Owner owner);
};
extern SerialPortManager PortManager;

View File

@ -5,6 +5,7 @@
#include <memory>
#include "VeDirectMpptController.h"
#include "Configuration.h"
#include <TaskSchedulerDeclarations.h>
class VictronMpptClass {
@ -16,12 +17,13 @@ public:
void updateSettings();
bool isDataValid() const;
bool isDataValid(size_t idx) const;
// returns the data age of all controllers,
// i.e, the youngest data's age is returned.
uint32_t getDataAgeMillis() const;
VeDirectMpptController::spData_t getData(size_t idx = 0) const;
std::optional<VeDirectMpptController::spData_t> getData(size_t idx = 0) const;
// total output of all MPPT charge controllers in Watts
int32_t getPowerOutputWatts() const;
@ -50,6 +52,8 @@ private:
mutable std::mutex _mutex;
using controller_t = std::unique_ptr<VeDirectMpptController>;
std::vector<controller_t> _controllers;
bool initController(int8_t rx, int8_t tx, bool logging, int hwSerialPort);
};
extern VictronMpptClass VictronMppt;

View File

@ -9,6 +9,7 @@ public:
void deinit() final { }
void loop() final;
std::shared_ptr<BatteryStats> getStats() const final { return _stats; }
bool usesHwPort2() override;
private:
uint32_t _lastUpdate = 0;

View File

@ -2,6 +2,7 @@
#pragma once
#include "ArduinoJson.h"
#include "Configuration.h"
#include <ESPAsyncWebServer.h>
#include <TaskSchedulerDeclarations.h>
#include <VeDirectMpptController.h>
@ -14,6 +15,7 @@ public:
private:
void generateJsonResponse(JsonVariant& root);
static void populateJson(const JsonObject &root, const VeDirectMpptController::spData_t &spMpptData);
void onLivedataStatus(AsyncWebServerRequest* request);
void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len);
@ -22,7 +24,7 @@ private:
uint32_t _lastWsPublish = 0;
uint32_t _dataAgeMillis = 0;
static constexpr uint16_t _responseSize = 1024 + 128;
static constexpr uint16_t _responseSize = VICTRON_MAX_COUNT * (1024 + 128);
std::mutex _mutex;

View File

@ -1,9 +1,9 @@
#include <Arduino.h>
#include "VeDirectMpptController.h"
void VeDirectMpptController::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging)
void VeDirectMpptController::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort)
{
VeDirectFrameHandler::init(rx, tx, msgOut, verboseLogging, 1);
VeDirectFrameHandler::init(rx, tx, msgOut, verboseLogging, hwSerialPort);
_spData = std::make_shared<veMpptStruct>();
if (_verboseLogging) { _msgOut->println("Finished init MPPTController"); }
}

View File

@ -39,7 +39,7 @@ class VeDirectMpptController : public VeDirectFrameHandler {
public:
VeDirectMpptController() = default;
void init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging);
void init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort);
bool isDataValid() const; // return true if data valid and not outdated
struct veMpptStruct : veStruct {

View File

@ -5,6 +5,7 @@
#include "JkBmsController.h"
#include "VictronSmartShunt.h"
#include "MqttBattery.h"
#include "SerialPortManager.h"
BatteryClass Battery;
@ -38,6 +39,7 @@ void BatteryClass::updateSettings()
_upProvider->deinit();
_upProvider = nullptr;
}
PortManager.invalidateBatteryPort();
CONFIG_T& config = Configuration.get();
if (!config.Battery.Enabled) { return; }
@ -47,23 +49,32 @@ void BatteryClass::updateSettings()
switch (config.Battery.Provider) {
case 0:
_upProvider = std::make_unique<PylontechCanReceiver>();
if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; }
break;
case 1:
_upProvider = std::make_unique<JkBms::Controller>();
if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; }
break;
case 2:
_upProvider = std::make_unique<MqttBattery>();
if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; }
break;
case 3:
_upProvider = std::make_unique<VictronSmartShunt>();
if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; }
break;
default:
MessageOutput.printf("Unknown battery provider: %d\r\n", config.Battery.Provider);
break;
return;
}
if(_upProvider->usesHwPort2()) {
if (!PortManager.allocateBatteryPort(2)) {
MessageOutput.printf("[Battery] Serial port %d already in use. Initialization aborted!\r\n", 2);
_upProvider = nullptr;
return;
}
}
if (!_upProvider->init(verboseLogging)) {
PortManager.invalidateBatteryPort();
_upProvider = nullptr;
}
}

View File

@ -427,4 +427,8 @@ void Controller::processDataPoints(DataPointContainer const& dataPoints)
}
}
bool Controller::usesHwPort2() {
return true;
}
} /* namespace JkBms */

View File

@ -112,3 +112,7 @@ void MqttBattery::onMqttMessageVoltage(espMqttClientTypes::MessageProperties con
*voltage, topic);
}
}
bool MqttBattery::usesHwPort2() {
return false;
}

View File

@ -15,7 +15,7 @@ MqttHandleVedirectHassClass MqttHandleVedirectHass;
void MqttHandleVedirectHassClass::init(Scheduler& scheduler)
{
scheduler.addTask(_loopTask);
_loopTask.setCallback(std::bind(&MqttHandleVedirectHassClass::loop, this));
_loopTask.setCallback([this] { loop(); });
_loopTask.setIterations(TASK_FOREVER);
_loopTask.enable();
}
@ -55,43 +55,56 @@ void MqttHandleVedirectHassClass::publishConfig()
if (!MqttSettings.getConnected()) {
return;
}
// ensure data is revieved from victron
if (!VictronMppt.isDataValid()) {
return;
}
// device info
publishBinarySensor("MPPT load output state", "mdi:export", "LOAD", "ON", "OFF");
publishSensor("MPPT serial number", "mdi:counter", "SER");
publishSensor("MPPT firmware number", "mdi:counter", "FW");
publishSensor("MPPT state of operation", "mdi:wrench", "CS");
publishSensor("MPPT error code", "mdi:bell", "ERR");
publishSensor("MPPT off reason", "mdi:wrench", "OR");
publishSensor("MPPT tracker operation mode", "mdi:wrench", "MPPT");
publishSensor("MPPT Day sequence number (0...364)", "mdi:calendar-month-outline", "HSDS", NULL, "total", "d");
for (int idx = 0; idx < VICTRON_MAX_COUNT; ++idx) {
// ensure data is received from victron
if (!VictronMppt.isDataValid(idx)) {
continue;
}
std::optional<VeDirectMpptController::spData_t> spOptMpptData = VictronMppt.getData(idx);
if (!spOptMpptData.has_value()) {
continue;
}
VeDirectMpptController::spData_t &spMpptData = spOptMpptData.value();
publishBinarySensor("MPPT load output state", "mdi:export", "LOAD", "ON", "OFF", spMpptData);
publishSensor("MPPT serial number", "mdi:counter", "SER", nullptr, nullptr, nullptr, spMpptData);
publishSensor("MPPT firmware number", "mdi:counter", "FW", nullptr, nullptr, nullptr, spMpptData);
publishSensor("MPPT state of operation", "mdi:wrench", "CS", nullptr, nullptr, nullptr, spMpptData);
publishSensor("MPPT error code", "mdi:bell", "ERR", nullptr, nullptr, nullptr, spMpptData);
publishSensor("MPPT off reason", "mdi:wrench", "OR", nullptr, nullptr, nullptr, spMpptData);
publishSensor("MPPT tracker operation mode", "mdi:wrench", "MPPT", nullptr, nullptr, nullptr, spMpptData);
publishSensor("MPPT Day sequence number (0...364)", "mdi:calendar-month-outline", "HSDS", NULL, "total", "d", spMpptData);
// battery info
publishSensor("Battery voltage", NULL, "V", "voltage", "measurement", "V");
publishSensor("Battery current", NULL, "I", "current", "measurement", "A");
publishSensor("Battery power (calculated)", NULL, "P", "power", "measurement", "W");
publishSensor("Battery efficiency (calculated)", NULL, "E", NULL, "measurement", "%");
publishSensor("Battery voltage", NULL, "V", "voltage", "measurement", "V", spMpptData);
publishSensor("Battery current", NULL, "I", "current", "measurement", "A", spMpptData);
publishSensor("Battery power (calculated)", NULL, "P", "power", "measurement", "W", spMpptData);
publishSensor("Battery efficiency (calculated)", NULL, "E", NULL, "measurement", "%", spMpptData);
// panel info
publishSensor("Panel voltage", NULL, "VPV", "voltage", "measurement", "V");
publishSensor("Panel current (calculated)", NULL, "IPV", "current", "measurement", "A");
publishSensor("Panel power", NULL, "PPV", "power", "measurement", "W");
publishSensor("Panel yield total", NULL, "H19", "energy", "total_increasing", "kWh");
publishSensor("Panel yield today", NULL, "H20", "energy", "total", "kWh");
publishSensor("Panel maximum power today", NULL, "H21", "power", "measurement", "W");
publishSensor("Panel yield yesterday", NULL, "H22", "energy", "total", "kWh");
publishSensor("Panel maximum power yesterday", NULL, "H23", "power", "measurement", "W");
publishSensor("Panel voltage", NULL, "VPV", "voltage", "measurement", "V", spMpptData);
publishSensor("Panel current (calculated)", NULL, "IPV", "current", "measurement", "A", spMpptData);
publishSensor("Panel power", NULL, "PPV", "power", "measurement", "W", spMpptData);
publishSensor("Panel yield total", NULL, "H19", "energy", "total_increasing", "kWh", spMpptData);
publishSensor("Panel yield today", NULL, "H20", "energy", "total", "kWh", spMpptData);
publishSensor("Panel maximum power today", NULL, "H21", "power", "measurement", "W", spMpptData);
publishSensor("Panel yield yesterday", NULL, "H22", "energy", "total", "kWh", spMpptData);
publishSensor("Panel maximum power yesterday", NULL, "H23", "power", "measurement", "W", spMpptData);
}
yield();
}
void MqttHandleVedirectHassClass::publishSensor(const char* caption, const char* icon, const char* subTopic, const char* deviceClass, const char* stateClass, const char* unitOfMeasurement )
void MqttHandleVedirectHassClass::publishSensor(const char *caption, const char *icon, const char *subTopic,
const char *deviceClass, const char *stateClass,
const char *unitOfMeasurement,
const VeDirectMpptController::spData_t &spMpptData)
{
String serial = VictronMppt.getData()->SER;
String serial = spMpptData->SER;
String sensorId = caption;
sensorId.replace(" ", "_");
@ -126,7 +139,7 @@ void MqttHandleVedirectHassClass::publishSensor(const char* caption, const char*
}
JsonObject deviceObj = root.createNestedObject("dev");
createDeviceInfo(deviceObj);
createDeviceInfo(deviceObj, spMpptData);
if (Configuration.get().Mqtt.Hass.Expire) {
root["exp_aft"] = Configuration.get().Mqtt.PublishInterval * 3;
@ -143,9 +156,11 @@ void MqttHandleVedirectHassClass::publishSensor(const char* caption, const char*
publish(configTopic, buffer);
}
void MqttHandleVedirectHassClass::publishBinarySensor(const char* caption, const char* icon, const char* subTopic, const char* payload_on, const char* payload_off)
void MqttHandleVedirectHassClass::publishBinarySensor(const char *caption, const char *icon, const char *subTopic,
const char *payload_on, const char *payload_off,
const VeDirectMpptController::spData_t &spMpptData)
{
String serial = VictronMppt.getData()->SER;
String serial = spMpptData->SER;
String sensorId = caption;
sensorId.replace(" ", "_");
@ -178,16 +193,16 @@ void MqttHandleVedirectHassClass::publishBinarySensor(const char* caption, const
}
JsonObject deviceObj = root.createNestedObject("dev");
createDeviceInfo(deviceObj);
createDeviceInfo(deviceObj, spMpptData);
char buffer[512];
serializeJson(root, buffer);
publish(configTopic, buffer);
}
void MqttHandleVedirectHassClass::createDeviceInfo(JsonObject& object)
void MqttHandleVedirectHassClass::createDeviceInfo(JsonObject &object,
const VeDirectMpptController::spData_t &spMpptData)
{
auto spMpptData = VictronMppt.getData();
String serial = spMpptData->SER;
object["name"] = "Victron(" + serial + ")";
object["ids"] = serial;

View File

@ -17,7 +17,7 @@ MqttHandleVedirectClass MqttHandleVedirect;
void MqttHandleVedirectClass::init(Scheduler& scheduler)
{
scheduler.addTask(_loopTask);
_loopTask.setCallback(std::bind(&MqttHandleVedirectClass::loop, this));
_loopTask.setCallback([this] { loop(); });
_loopTask.setIterations(TASK_FOREVER);
_loopTask.enable();
@ -41,10 +41,6 @@ void MqttHandleVedirectClass::loop()
return;
}
if (!VictronMppt.isDataValid()) {
return;
}
if ((millis() >= _nextPublishFull) || (millis() >= _nextPublishUpdatesOnly)) {
// determine if this cycle should publish full values or updates only
if (_nextPublishFull <= _nextPublishUpdatesOnly) {
@ -62,82 +58,23 @@ void MqttHandleVedirectClass::loop()
}
#endif
auto spMpptData = VictronMppt.getData();
String value;
String topic = "victron/";
topic.concat(spMpptData->SER);
topic.concat("/");
for (int idx = 0; idx < VICTRON_MAX_COUNT; ++idx) {
if (!VictronMppt.isDataValid(idx)) {
continue;
}
if (_PublishFull || spMpptData->PID != _kvFrame.PID)
MqttSettings.publish(topic + "PID", spMpptData->getPidAsString().data());
if (_PublishFull || strcmp(spMpptData->SER, _kvFrame.SER) != 0)
MqttSettings.publish(topic + "SER", spMpptData->SER );
if (_PublishFull || strcmp(spMpptData->FW, _kvFrame.FW) != 0)
MqttSettings.publish(topic + "FW", spMpptData->FW);
if (_PublishFull || spMpptData->LOAD != _kvFrame.LOAD)
MqttSettings.publish(topic + "LOAD", spMpptData->LOAD == true ? "ON": "OFF");
if (_PublishFull || spMpptData->CS != _kvFrame.CS)
MqttSettings.publish(topic + "CS", spMpptData->getCsAsString().data());
if (_PublishFull || spMpptData->ERR != _kvFrame.ERR)
MqttSettings.publish(topic + "ERR", spMpptData->getErrAsString().data());
if (_PublishFull || spMpptData->OR != _kvFrame.OR)
MqttSettings.publish(topic + "OR", spMpptData->getOrAsString().data());
if (_PublishFull || spMpptData->MPPT != _kvFrame.MPPT)
MqttSettings.publish(topic + "MPPT", spMpptData->getMpptAsString().data());
if (_PublishFull || spMpptData->HSDS != _kvFrame.HSDS) {
value = spMpptData->HSDS;
MqttSettings.publish(topic + "HSDS", value);
}
if (_PublishFull || spMpptData->V != _kvFrame.V) {
value = spMpptData->V;
MqttSettings.publish(topic + "V", value);
}
if (_PublishFull || spMpptData->I != _kvFrame.I) {
value = spMpptData->I;
MqttSettings.publish(topic + "I", value);
}
if (_PublishFull || spMpptData->P != _kvFrame.P) {
value = spMpptData->P;
MqttSettings.publish(topic + "P", value);
}
if (_PublishFull || spMpptData->VPV != _kvFrame.VPV) {
value = spMpptData->VPV;
MqttSettings.publish(topic + "VPV", value);
}
if (_PublishFull || spMpptData->IPV != _kvFrame.IPV) {
value = spMpptData->IPV;
MqttSettings.publish(topic + "IPV", value);
}
if (_PublishFull || spMpptData->PPV != _kvFrame.PPV) {
value = spMpptData->PPV;
MqttSettings.publish(topic + "PPV", value);
}
if (_PublishFull || spMpptData->E != _kvFrame.E) {
value = spMpptData->E;
MqttSettings.publish(topic + "E", value);
}
if (_PublishFull || spMpptData->H19 != _kvFrame.H19) {
value = spMpptData->H19;
MqttSettings.publish(topic + "H19", value);
}
if (_PublishFull || spMpptData->H20 != _kvFrame.H20) {
value = spMpptData->H20;
MqttSettings.publish(topic + "H20", value);
}
if (_PublishFull || spMpptData->H21 != _kvFrame.H21) {
value = spMpptData->H21;
MqttSettings.publish(topic + "H21", value);
}
if (_PublishFull || spMpptData->H22 != _kvFrame.H22) {
value = spMpptData->H22;
MqttSettings.publish(topic + "H22", value);
}
if (_PublishFull || spMpptData->H23 != _kvFrame.H23) {
value = spMpptData->H23;
MqttSettings.publish(topic + "H23", value);
std::optional<VeDirectMpptController::spData_t> spOptMpptData = VictronMppt.getData(idx);
if (!spOptMpptData.has_value()) {
continue;
}
VeDirectMpptController::spData_t &spMpptData = spOptMpptData.value();
VeDirectMpptController::veMpptStruct _kvFrame = _kvFrames[spMpptData->SER];
publish_mppt_data(spMpptData, _kvFrame);
if (!_PublishFull) {
_kvFrame = *spMpptData;
_kvFrames[spMpptData->SER] = *spMpptData;
}
}
// now calculate next points of time to publish
@ -166,3 +103,80 @@ void MqttHandleVedirectClass::loop()
#endif
}
}
void MqttHandleVedirectClass::publish_mppt_data(const VeDirectMpptController::spData_t &spMpptData,
VeDirectMpptController::veMpptStruct &frame) const {
String value;
String topic = "victron/";
topic.concat(spMpptData->SER);
topic.concat("/");
if (_PublishFull || spMpptData->PID != frame.PID)
MqttSettings.publish(topic + "PID", spMpptData->getPidAsString().data());
if (_PublishFull || strcmp(spMpptData->SER, frame.SER) != 0)
MqttSettings.publish(topic + "SER", spMpptData->SER );
if (_PublishFull || strcmp(spMpptData->FW, frame.FW) != 0)
MqttSettings.publish(topic + "FW", spMpptData->FW);
if (_PublishFull || spMpptData->LOAD != frame.LOAD)
MqttSettings.publish(topic + "LOAD", spMpptData->LOAD ? "ON" : "OFF");
if (_PublishFull || spMpptData->CS != frame.CS)
MqttSettings.publish(topic + "CS", spMpptData->getCsAsString().data());
if (_PublishFull || spMpptData->ERR != frame.ERR)
MqttSettings.publish(topic + "ERR", spMpptData->getErrAsString().data());
if (_PublishFull || spMpptData->OR != frame.OR)
MqttSettings.publish(topic + "OR", spMpptData->getOrAsString().data());
if (_PublishFull || spMpptData->MPPT != frame.MPPT)
MqttSettings.publish(topic + "MPPT", spMpptData->getMpptAsString().data());
if (_PublishFull || spMpptData->HSDS != frame.HSDS) {
value = spMpptData->HSDS;
MqttSettings.publish(topic + "HSDS", value);
}
if (_PublishFull || spMpptData->V != frame.V) {
value = spMpptData->V;
MqttSettings.publish(topic + "V", value);
}
if (_PublishFull || spMpptData->I != frame.I) {
value = spMpptData->I;
MqttSettings.publish(topic + "I", value);
}
if (_PublishFull || spMpptData->P != frame.P) {
value = spMpptData->P;
MqttSettings.publish(topic + "P", value);
}
if (_PublishFull || spMpptData->VPV != frame.VPV) {
value = spMpptData->VPV;
MqttSettings.publish(topic + "VPV", value);
}
if (_PublishFull || spMpptData->IPV != frame.IPV) {
value = spMpptData->IPV;
MqttSettings.publish(topic + "IPV", value);
}
if (_PublishFull || spMpptData->PPV != frame.PPV) {
value = spMpptData->PPV;
MqttSettings.publish(topic + "PPV", value);
}
if (_PublishFull || spMpptData->E != frame.E) {
value = spMpptData->E;
MqttSettings.publish(topic + "E", value);
}
if (_PublishFull || spMpptData->H19 != frame.H19) {
value = spMpptData->H19;
MqttSettings.publish(topic + "H19", value);
}
if (_PublishFull || spMpptData->H20 != frame.H20) {
value = spMpptData->H20;
MqttSettings.publish(topic + "H20", value);
}
if (_PublishFull || spMpptData->H21 != frame.H21) {
value = spMpptData->H21;
MqttSettings.publish(topic + "H21", value);
}
if (_PublishFull || spMpptData->H22 != frame.H22) {
value = spMpptData->H22;
MqttSettings.publish(topic + "H22", value);
}
if (_PublishFull || spMpptData->H23 != frame.H23) {
value = spMpptData->H23;
MqttSettings.publish(topic + "H23", value);
}
}

View File

@ -182,8 +182,11 @@ PinMappingClass::PinMappingClass()
_pinMapping.display_cs = DISPLAY_CS;
_pinMapping.display_reset = DISPLAY_RESET;
_pinMapping.victron_tx = VICTRON_PIN_TX;
_pinMapping.victron_rx = VICTRON_PIN_RX;
_pinMapping.victron_tx = VICTRON_PIN_TX;
_pinMapping.victron_rx2 = VICTRON_PIN_RX;
_pinMapping.victron_tx2 = VICTRON_PIN_TX;
_pinMapping.battery_rx = BATTERY_PIN_RX;
_pinMapping.battery_rxen = BATTERY_PIN_RXEN;
@ -259,6 +262,8 @@ bool PinMappingClass::init(const String& deviceMapping)
_pinMapping.victron_rx = doc[i]["victron"]["rx"] | VICTRON_PIN_RX;
_pinMapping.victron_tx = doc[i]["victron"]["tx"] | VICTRON_PIN_TX;
_pinMapping.victron_rx2 = doc[i]["victron"]["rx2"] | VICTRON_PIN_RX;
_pinMapping.victron_tx2 = doc[i]["victron"]["tx2"] | VICTRON_PIN_TX;
_pinMapping.battery_rx = doc[i]["battery"]["rx"] | BATTERY_PIN_RX;
_pinMapping.battery_rxen = doc[i]["battery"]["rxen"] | BATTERY_PIN_RXEN;

View File

@ -266,6 +266,10 @@ bool PylontechCanReceiver::getBit(uint8_t value, uint8_t bit)
return (value & (1 << bit)) >> bit;
}
bool PylontechCanReceiver::usesHwPort2() {
return false;
}
#ifdef PYLONTECH_DUMMY
void PylontechCanReceiver::dummyData()
{

59
src/SerialPortManager.cpp Normal file
View File

@ -0,0 +1,59 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "SerialPortManager.h"
#include "MessageOutput.h"
#define MAX_CONTROLLERS 3
SerialPortManager PortManager;
bool SerialPortManager::allocateBatteryPort(int port)
{
return allocatePort(port, Owner::BATTERY);
}
bool SerialPortManager::allocateMpptPort(int port)
{
return allocatePort(port, Owner::MPPT);
}
bool SerialPortManager::allocatePort(uint8_t port, Owner owner)
{
if (port >= MAX_CONTROLLERS) {
MessageOutput.printf("[SerialPortManager] Invalid serial port = %d \r\n", port);
return false;
}
return allocatedPorts.insert({port, owner}).second;
}
void SerialPortManager::invalidateBatteryPort()
{
invalidate(Owner::BATTERY);
}
void SerialPortManager::invalidateMpptPorts()
{
invalidate(Owner::MPPT);
}
void SerialPortManager::invalidate(Owner owner)
{
for (auto it = allocatedPorts.begin(); it != allocatedPorts.end();) {
if (it->second == owner) {
MessageOutput.printf("[SerialPortManager] Removing port = %d, owner = %s \r\n", it->first, print(owner));
it = allocatedPorts.erase(it);
} else {
++it;
}
}
}
const char* SerialPortManager::print(Owner owner)
{
switch (owner) {
case BATTERY:
return "BATTERY";
case MPPT:
return "MPPT";
}
}

View File

@ -3,13 +3,14 @@
#include "Configuration.h"
#include "PinMapping.h"
#include "MessageOutput.h"
#include "SerialPortManager.h"
VictronMpptClass VictronMppt;
void VictronMpptClass::init(Scheduler& scheduler)
{
scheduler.addTask(_loopTask);
_loopTask.setCallback(std::bind(&VictronMpptClass::loop, this));
_loopTask.setCallback([this] { loop(); });
_loopTask.setIterations(TASK_FOREVER);
_loopTask.enable();
@ -21,24 +22,41 @@ void VictronMpptClass::updateSettings()
std::lock_guard<std::mutex> lock(_mutex);
_controllers.clear();
PortManager.invalidateMpptPorts();
CONFIG_T& config = Configuration.get();
if (!config.Vedirect.Enabled) { return; }
const PinMapping_t& pin = PinMapping.get();
int8_t rx = pin.victron_rx;
int8_t tx = pin.victron_tx;
MessageOutput.printf("[VictronMppt] rx = %d, tx = %d\r\n", rx, tx);
int hwSerialPort = 1;
bool initSuccess = initController(pin.victron_rx, pin.victron_tx, config.Vedirect.VerboseLogging, hwSerialPort);
if (initSuccess) {
hwSerialPort++;
}
initController(pin.victron_rx2, pin.victron_tx2, config.Vedirect.VerboseLogging, hwSerialPort);
}
bool VictronMpptClass::initController(int8_t rx, int8_t tx, bool logging, int hwSerialPort)
{
MessageOutput.printf("[VictronMppt] rx = %d, tx = %d, hwSerialPort = %d\r\n", rx, tx, hwSerialPort);
if (rx < 0) {
MessageOutput.println("[VictronMppt] invalid pin config");
return;
MessageOutput.printf("[VictronMppt] invalid pin config rx = %d, tx = %d\r\n", rx, tx);
return false;
}
if (!PortManager.allocateMpptPort(hwSerialPort)) {
MessageOutput.printf("[VictronMppt] Serial port %d already in use. Initialization aborted!\r\n",
hwSerialPort);
return false;
}
auto upController = std::make_unique<VeDirectMpptController>();
upController->init(rx, tx, &MessageOutput, config.Vedirect.VerboseLogging);
upController->init(rx, tx, &MessageOutput, logging, hwSerialPort);
_controllers.push_back(std::move(upController));
return true;
}
void VictronMpptClass::loop()
@ -61,6 +79,17 @@ bool VictronMpptClass::isDataValid() const
return !_controllers.empty();
}
bool VictronMpptClass::isDataValid(size_t idx) const
{
std::lock_guard<std::mutex> lock(_mutex);
if (_controllers.empty() || idx >= _controllers.size()) {
return false;
}
return _controllers[idx]->isDataValid();
}
uint32_t VictronMpptClass::getDataAgeMillis() const
{
std::lock_guard<std::mutex> lock(_mutex);
@ -81,17 +110,17 @@ uint32_t VictronMpptClass::getDataAgeMillis() const
return age;
}
VeDirectMpptController::spData_t VictronMpptClass::getData(size_t idx) const
std::optional<VeDirectMpptController::spData_t> VictronMpptClass::getData(size_t idx) const
{
std::lock_guard<std::mutex> 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::make_shared<VeDirectMpptController::veMpptStruct>();
return std::nullopt;
}
return _controllers[idx]->getData();
return std::optional<VeDirectMpptController::spData_t>{_controllers[idx]->getData()};
}
int32_t VictronMpptClass::getPowerOutputWatts() const

View File

@ -34,3 +34,7 @@ void VictronSmartShunt::loop()
_stats->updateFrom(VeDirectShunt.veFrame);
_lastUpdate = VeDirectShunt.getLastUpdate();
}
bool VictronSmartShunt::usesHwPort2() {
return true;
}

View File

@ -86,9 +86,11 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request)
led["brightness"] = config.Led_Single[i].Brightness;
}
JsonObject victronPinObj = curPin.createNestedObject("victron");
auto victronPinObj = curPin.createNestedObject("victron");
victronPinObj["rx"] = pin.victron_rx;
victronPinObj["tx"] = pin.victron_tx;
victronPinObj["rx2"] = pin.victron_rx2;
victronPinObj["tx2"] = pin.victron_tx2;
JsonObject batteryPinObj = curPin.createNestedObject("battery");
batteryPinObj["rx"] = pin.battery_rx;

View File

@ -61,7 +61,7 @@ void WebApiWsVedirectLiveClass::sendDataTaskCb()
// we assume this loop to be running at least twice for every
// update from a VE.Direct MPPT data producer, so _dataAgeMillis
// acutally grows in between updates.
// actually grows in between updates.
auto lastDataAgeMillis = _dataAgeMillis;
_dataAgeMillis = VictronMppt.getDataAgeMillis();
@ -99,15 +99,36 @@ void WebApiWsVedirectLiveClass::sendDataTaskCb()
void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root)
{
auto spMpptData = VictronMppt.getData();
root["vedirect"]["data_age"] = VictronMppt.getDataAgeMillis() / 1000;
const JsonArray &array = root["vedirect"].createNestedArray("devices");
for (int idx = 0; idx < VICTRON_MAX_COUNT; ++idx) {
std::optional<VeDirectMpptController::spData_t> spOptMpptData = VictronMppt.getData(idx);
if (!spOptMpptData.has_value()) {
continue;
}
VeDirectMpptController::spData_t &spMpptData = spOptMpptData.value();
const JsonObject &nested = array.createNestedObject();
nested["age_critical"] = !VictronMppt.isDataValid(idx);
populateJson(nested, spMpptData);
}
// power limiter state
root["dpl"]["PLSTATE"] = -1;
if (Configuration.get().PowerLimiter.Enabled)
root["dpl"]["PLSTATE"] = PowerLimiter.getPowerLimiterState();
root["dpl"]["PLLIMIT"] = PowerLimiter.getLastRequestedPowerLimit();
}
void
WebApiWsVedirectLiveClass::populateJson(const JsonObject &root, const VeDirectMpptController::spData_t &spMpptData) {
// device info
root["device"]["data_age"] = VictronMppt.getDataAgeMillis() / 1000;
root["device"]["age_critical"] = !VictronMppt.isDataValid();
root["device"]["PID"] = spMpptData->getPidAsString();
root["device"]["SER"] = spMpptData->SER;
root["device"]["FW"] = spMpptData->FW;
root["device"]["LOAD"] = spMpptData->LOAD == true ? "ON" : "OFF";
root["device"]["LOAD"] = spMpptData->LOAD ? "ON" : "OFF";
root["device"]["CS"] = spMpptData->getCsAsString();
root["device"]["ERR"] = spMpptData->getErrAsString();
root["device"]["OR"] = spMpptData->getOrAsString();
@ -154,12 +175,6 @@ void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root)
root["input"]["MaximumPowerYesterday"]["v"] = spMpptData->H23;
root["input"]["MaximumPowerYesterday"]["u"] = "W";
root["input"]["MaximumPowerYesterday"]["d"] = 0;
// power limiter state
root["dpl"]["PLSTATE"] = -1;
if (Configuration.get().PowerLimiter.Enabled)
root["dpl"]["PLSTATE"] = PowerLimiter.getPowerLimiterState();
root["dpl"]["PLLIMIT"] = PowerLimiter.getLastRequestedPowerLimit();
}
void WebApiWsVedirectLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len)

View File

@ -9,25 +9,25 @@
<template v-else>
<div class="row gy-3">
<div class="tab-content col-sm-12 col-md-12" id="v-pills-tabContent">
<div class="card">
<div class="card" v-for="item in vedirect.devices">
<div class="card-header d-flex justify-content-between align-items-center"
:class="{
'text-bg-danger': vedirectData.age_critical,
'text-bg-primary': !vedirectData.age_critical,
'text-bg-danger': item.age_critical,
'text-bg-primary': !item.age_critical,
}">
<div class="p-1 flex-grow-1">
<div class="d-flex flex-wrap">
<div style="padding-right: 2em;">
{{ vedirectData.PID }}
{{ item.device.PID }}
</div>
<div style="padding-right: 2em;">
{{ $t('vedirecthome.SerialNumber') }} {{ vedirectData.SER }}
{{ $t('vedirecthome.SerialNumber') }} {{ item.device.SER }}
</div>
<div style="padding-right: 2em;">
{{ $t('vedirecthome.FirmwareNumber') }} {{ vedirectData.FW }}
{{ $t('vedirecthome.FirmwareNumber') }} {{ item.device.FW }}
</div>
<div style="padding-right: 2em;">
{{ $t('vedirecthome.DataAge') }} {{ $t('vedirecthome.Seconds', {'val': vedirectData.data_age }) }}
{{ $t('vedirecthome.DataAge') }} {{ $t('vedirecthome.Seconds', {'val': vedirect.data_age }) }}
</div>
</div>
</div>
@ -71,33 +71,33 @@
<tbody>
<tr>
<th scope="row">{{ $t('vedirecthome.LoadOutputState') }}</th>
<td style="text-align: right">{{vedirectData.LOAD}}</td>
<td style="text-align: right">{{item.device.LOAD}}</td>
<td></td>
</tr>
<tr>
<th scope="row">{{ $t('vedirecthome.StateOfOperation') }}</th>
<td style="text-align: right">{{vedirectData.CS}}</td>
<td style="text-align: right">{{item.device.CS}}</td>
<td></td>
</tr>
<tr>
<th scope="row">{{ $t('vedirecthome.TrackerOperationMode') }}</th>
<td style="text-align: right">{{vedirectData.MPPT}}</td>
<td style="text-align: right">{{item.device.MPPT}}</td>
<td></td>
</tr>
<tr>
<th scope="row">{{ $t('vedirecthome.OffReason') }}</th>
<td style="text-align: right">{{vedirectData.OR}}</td>
<td style="text-align: right">{{item.device.OR}}</td>
<td></td>
</tr>
<tr>
<th scope="row">{{ $t('vedirecthome.ErrorCode') }}</th>
<td style="text-align: right">{{vedirectData.ERR}}</td>
<td style="text-align: right">{{item.device.ERR}}</td>
<td></td>
</tr>
<tr>
<th scope="row">{{ $t('vedirecthome.DaySequenceNumber') }}</th>
<td style="text-align: right">{{vedirectData.HSDS.v}}</td>
<td>{{vedirectData.HSDS.u}}</td>
<td style="text-align: right">{{item.device.HSDS.v}}</td>
<td>{{item.device.HSDS.u}}</td>
</tr>
</tbody>
</table>
@ -119,7 +119,7 @@
</tr>
</thead>
<tbody>
<tr v-for="(prop, key) in vedirectOutput" v-bind:key="key">
<tr v-for="(prop, key) in item.output" v-bind:key="key">
<th scope="row">{{ $t('vedirecthome.output.' + key) }}</th>
<td style="text-align: right">
{{ $n(prop.v, 'decimal', {
@ -149,7 +149,7 @@
</tr>
</thead>
<tbody>
<tr v-for="(prop, key) in vedirectInput" v-bind:key="key">
<tr v-for="(prop, key) in item.input" v-bind:key="key">
<th scope="row">{{ $t('vedirecthome.input.' + key) }}</th>
<td style="text-align: right">
{{ $n(prop.v, 'decimal', {
@ -178,7 +178,7 @@
<script lang="ts">
import { defineComponent } from 'vue';
import type { DynamicPowerLimiter, VedirectDevice, VedirectOutput, VedirectInput } from '@/types/VedirectLiveDataStatus';
import type { DynamicPowerLimiter, Vedirect } from '@/types/VedirectLiveDataStatus';
import { handleResponse, authHeader, authUrl } from '@/utils/authentication';
import {
BIconSun,
@ -202,9 +202,7 @@ export default defineComponent({
dataAgeInterval: 0,
dataLoading: true,
dplData: {} as DynamicPowerLimiter,
vedirectData: {} as VedirectDevice,
vedirectOutput: {} as VedirectOutput,
vedirectInput: {} as VedirectInput,
vedirect: {} as Vedirect,
isFirstFetchAfterConnect: true,
};
},
@ -224,9 +222,7 @@ export default defineComponent({
.then((response) => handleResponse(response, this.$emitter, this.$router))
.then((root) => {
this.dplData = root["dpl"];
this.vedirectData = root["device"];
this.vedirectOutput = root["output"];
this.vedirectInput = root["input"];
this.vedirect = root["vedirect"];
this.dataLoading = false;
});
},
@ -244,9 +240,7 @@ export default defineComponent({
console.log(event);
var root = JSON.parse(event.data);
this.dplData = root["dpl"];
this.vedirectData = root["device"];
this.vedirectOutput = root["output"];
this.vedirectInput = root["input"];
this.vedirect = root["vedirect"];
this.dataLoading = false;
this.heartCheck(); // Reset heartbeat detection
};
@ -263,8 +257,8 @@ export default defineComponent({
},
initDataAgeing() {
this.dataAgeInterval = setInterval(() => {
if (this.vedirectData) {
this.vedirectData.data_age++;
if (this.vedirect) {
this.vedirect.data_age++;
}
}, 1000);
},

View File

@ -5,12 +5,22 @@ export interface DynamicPowerLimiter {
PLLIMIT: number;
}
export interface Vedirect {
data_age: 0;
devices: Array<VedirectDevices>;
}
export interface VedirectDevices {
age_critical: boolean;
device: VedirectDevice;
input: VedirectInput;
output: VedirectOutput;
}
export interface VedirectDevice {
SER: string;
PID: string;
FW: string;
age_critical: boolean;
data_age: 0;
LOAD: ValueObject;
CS: ValueObject;
MPPT: ValueObject;