VE.Direct: Fix design issues and prepare support for multiple instances (#505)

* introduce VictronMpptClass

this solves a design issue where the loop() method of a static instance
of VeDirectMpptController, which is part of library code, is called as
part of the main loop() implementation. that is a problem because the
call to this loop() must be handled differently from all other calls:
the lib does not know whether or not the feature is enabled at all.
also, the instance would not be initialized when enabling the feature
during normal operation. that would even lead to a nullptr exception
since the pointer to the serial implementation is still uninitialized.

this new intermediate class is implemented with the support for multiple
Victron charge controllers in mind. adding support for more charge
controllers should be more viable than ever.

fixes #481.

related to #397 #129.

* VE.Direct: move get.*AsString methods to respective structs

those structs, which hold the data to be translated into strings, know
best how to translate them. this change also simplifies access to those
translation, as no parameter must be handed to the respective methods:
they now act upon the data of the instance they are called for. adds
constness to those methods.

* VE.Direct: simplify and clean up get.*AsString methods

use a map, which is much easier to maintain and which reads much easier.
move the strings to flash memory to save RAM.

* DPL: use VictronMpptClass::getPowerOutputWatts method

remove redundant calculation of output power from DPL. consider
separation of concern: VictronMpptClass will provide the total solar
output power. the DPL shall not concern itself about how that value is
calculated and it certainly should be unaware about how many MPPT charge
controllers there actually are.

* VE.Direct: avoid shadowing struct member "P"

P was part of the base struct for both MPPT and SmartShunt controller.
however, P was also part of the SmartShunt controller data struct,
shadowing the member in the base struct.

since P has slightly different meaning in MPPT versus SmartShunt, and
since P is calculated for MPPT controllers but read from SmartShunts, P
now lives in both derived structs, but not in the base struct.

* VE.Direct: isDataValid(): avoid copying data structs

pass a const reference to the base class implementation of isDataValid()
rather than a copy of the whole struct.

* VE.Direct: unify logging of text events

* VE.Direct: stop processing text event if handled by base

in case the base class processed a text event, do not try to match it
against values that are only valid in the derived class -- none will
match.

* VE.Direct MPPT: manage data in a shared_ptr

instead of handing out a reference to a struct which is part of a class
instance that may disappear, e.g., on a config change, we now manage the
lifetime of said data structure using a shared_ptr and hand out copies
of that shared_ptr. this makes sure that users have a valid copy of the
data as long as they hold the shared_ptr.

* VE.Direct MPPT: implement getDataAgeMillis()

this works even if millis() wraps around.

* VE.Direct: process frame end event only for valid frames

save a parameters, save a level of indention, save a function call for
invalid frames.
This commit is contained in:
Bernhard Kirchen 2023-10-19 16:15:29 +02:00 committed by GitHub
parent 917242909b
commit d23b991f5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 507 additions and 623 deletions

View File

@ -62,7 +62,6 @@ public:
bool isValidNrf24Config();
bool isValidCmt2300Config();
bool isValidEthConfig();
bool isValidVictronConfig();
bool isValidHuaweiConfig();
private:

39
include/VictronMppt.h Normal file
View File

@ -0,0 +1,39 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <mutex>
#include <memory>
#include "VeDirectMpptController.h"
class VictronMpptClass {
public:
VictronMpptClass() = default;
~VictronMpptClass() = default;
void init();
void loop();
bool isDataValid() 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;
// total output of all MPPT charge controllers in Watts
int32_t getPowerOutputWatts() const;
private:
VictronMpptClass(VictronMpptClass const& other) = delete;
VictronMpptClass(VictronMpptClass&& other) = delete;
VictronMpptClass& operator=(VictronMpptClass const& other) = delete;
VictronMpptClass& operator=(VictronMpptClass&& other) = delete;
mutable std::mutex _mutex;
using controller_t = std::unique_ptr<VeDirectMpptController>;
std::vector<controller_t> _controllers;
};
extern VictronMpptClass VictronMppt;

View File

@ -20,8 +20,7 @@ private:
AsyncWebSocket _ws;
uint32_t _lastWsPublish = 0;
uint32_t _lastVedirectUpdateCheck = 0;
uint32_t _lastWsCleanup = 0;
uint32_t _newestVedirectTimestamp = 0;
uint32_t _dataAgeMillis = 0;
static constexpr uint16_t _responseSize = 1024 + 128;
};

View File

@ -72,19 +72,14 @@ VeDirectFrameHandler::VeDirectFrameHandler() :
{
}
void VeDirectFrameHandler::setVerboseLogging(bool verboseLogging)
{
_verboseLogging = verboseLogging;
if (!_verboseLogging) { _debugIn = 0; }
}
void VeDirectFrameHandler::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort)
{
_vedirectSerial = std::make_unique<HardwareSerial>(hwSerialPort);
_vedirectSerial->begin(19200, SERIAL_8N1, rx, tx);
_vedirectSerial->flush();
_msgOut = msgOut;
setVerboseLogging(verboseLogging);
_verboseLogging = verboseLogging;
_debugIn = 0;
}
void VeDirectFrameHandler::dumpDebugBuffer() {
@ -211,7 +206,7 @@ void VeDirectFrameHandler::rxData(uint8_t inbyte)
if (_verboseLogging) { dumpDebugBuffer(); }
_checksum = 0;
_state = IDLE;
frameEndEvent(valid);
if (valid) { frameValidEvent(); }
break;
}
case RECORD_HEX:
@ -224,22 +219,38 @@ void VeDirectFrameHandler::rxData(uint8_t inbyte)
* textRxEvent
* This function is called every time a new name/value is successfully parsed. It writes the values to the temporary buffer.
*/
void VeDirectFrameHandler::textRxEvent(char * name, char * value, veStruct& frame) {
bool VeDirectFrameHandler::textRxEvent(std::string const& who, char* name, char* value, veStruct& frame) {
if (_verboseLogging) {
_msgOut->printf("[Victron %s] Text Event %s: Value: %s\r\n",
who.c_str(), name, value );
}
if (strcmp(name, "PID") == 0) {
frame.PID = strtol(value, nullptr, 0);
return true;
}
else if (strcmp(name, "SER") == 0) {
if (strcmp(name, "SER") == 0) {
strcpy(frame.SER, value);
return true;
}
else if (strcmp(name, "FW") == 0) {
if (strcmp(name, "FW") == 0) {
strcpy(frame.FW, value);
return true;
}
else if (strcmp(name, "V") == 0) {
if (strcmp(name, "V") == 0) {
frame.V = round(atof(value) / 10.0) / 100.0;
return true;
}
else if (strcmp(name, "I") == 0) {
if (strcmp(name, "I") == 0) {
frame.I = round(atof(value) / 10.0) / 100.0;
return true;
}
return false;
}
@ -269,7 +280,7 @@ int VeDirectFrameHandler::hexRxEvent(uint8_t inbyte) {
return ret;
}
bool VeDirectFrameHandler::isDataValid(veStruct frame) {
bool VeDirectFrameHandler::isDataValid(veStruct const& frame) const {
if (_lastUpdate == 0) {
return false;
}
@ -279,330 +290,112 @@ bool VeDirectFrameHandler::isDataValid(veStruct frame) {
return true;
}
unsigned long VeDirectFrameHandler::getLastUpdate()
uint32_t VeDirectFrameHandler::getLastUpdate() const
{
return _lastUpdate;
}
template<typename T>
String const& VeDirectFrameHandler::getAsString(std::map<T, String> const& values, T val)
{
auto pos = values.find(val);
if (pos == values.end()) {
static String dummy;
dummy = val;
return dummy;
}
return pos->second;
}
template String const& VeDirectFrameHandler::getAsString(std::map<uint8_t, String> const& values, uint8_t val);
template String const& VeDirectFrameHandler::getAsString(std::map<uint16_t, String> const& values, uint16_t val);
template String const& VeDirectFrameHandler::getAsString(std::map<uint32_t, String> const& values, uint32_t val);
/*
* getPidAsString
* This function returns the product id (PID) as readable text.
*/
String VeDirectFrameHandler::getPidAsString(uint16_t pid)
String VeDirectFrameHandler::veStruct::getPidAsString() const
{
String strPID ="";
static const std::map<uint16_t, String> values = {
{ 0x0300, F("BlueSolar MPPT 70|15") },
{ 0xA040, F("BlueSolar MPPT 75|50") },
{ 0xA041, F("BlueSolar MPPT 150|35") },
{ 0xA042, F("BlueSolar MPPT 75|15") },
{ 0xA043, F("BlueSolar MPPT 100|15") },
{ 0xA044, F("BlueSolar MPPT 100|30") },
{ 0xA045, F("BlueSolar MPPT 100|50") },
{ 0xA046, F("BlueSolar MPPT 100|70") },
{ 0xA047, F("BlueSolar MPPT 150|100") },
{ 0xA049, F("BlueSolar MPPT 100|50 rev2") },
{ 0xA04A, F("BlueSolar MPPT 100|30 rev2") },
{ 0xA04B, F("BlueSolar MPPT 150|35 rev2") },
{ 0xA04C, F("BlueSolar MPPT 75|10") },
{ 0xA04D, F("BlueSolar MPPT 150|45") },
{ 0xA04E, F("BlueSolar MPPT 150|60") },
{ 0xA04F, F("BlueSolar MPPT 150|85") },
{ 0xA050, F("SmartSolar MPPT 250|100") },
{ 0xA051, F("SmartSolar MPPT 150|100") },
{ 0xA052, F("SmartSolar MPPT 150|85") },
{ 0xA053, F("SmartSolar MPPT 75|15") },
{ 0xA054, F("SmartSolar MPPT 75|10") },
{ 0xA055, F("SmartSolar MPPT 100|15") },
{ 0xA056, F("SmartSolar MPPT 100|30") },
{ 0xA057, F("SmartSolar MPPT 100|50") },
{ 0xA058, F("SmartSolar MPPT 150|35") },
{ 0xA059, F("SmartSolar MPPT 150|10 rev2") },
{ 0xA05A, F("SmartSolar MPPT 150|85 rev2") },
{ 0xA05B, F("SmartSolar MPPT 250|70") },
{ 0xA05C, F("SmartSolar MPPT 250|85") },
{ 0xA05D, F("SmartSolar MPPT 250|60") },
{ 0xA05E, F("SmartSolar MPPT 250|45") },
{ 0xA05F, F("SmartSolar MPPT 100|20") },
{ 0xA060, F("SmartSolar MPPT 100|20 48V") },
{ 0xA061, F("SmartSolar MPPT 150|45") },
{ 0xA062, F("SmartSolar MPPT 150|60") },
{ 0xA063, F("SmartSolar MPPT 150|70") },
{ 0xA064, F("SmartSolar MPPT 250|85 rev2") },
{ 0xA065, F("SmartSolar MPPT 250|100 rev2") },
{ 0xA066, F("BlueSolar MPPT 100|20") },
{ 0xA067, F("BlueSolar MPPT 100|20 48V") },
{ 0xA068, F("SmartSolar MPPT 250|60 rev2") },
{ 0xA069, F("SmartSolar MPPT 250|70 rev2") },
{ 0xA06A, F("SmartSolar MPPT 150|45 rev2") },
{ 0xA06B, F("SmartSolar MPPT 150|60 rev2") },
{ 0xA06C, F("SmartSolar MPPT 150|70 rev2") },
{ 0xA06D, F("SmartSolar MPPT 150|85 rev3") },
{ 0xA06E, F("SmartSolar MPPT 150|100 rev3") },
{ 0xA06F, F("BlueSolar MPPT 150|45 rev2") },
{ 0xA070, F("BlueSolar MPPT 150|60 rev2") },
{ 0xA071, F("BlueSolar MPPT 150|70 rev2") },
{ 0xA102, F("SmartSolar MPPT VE.Can 150|70") },
{ 0xA103, F("SmartSolar MPPT VE.Can 150|45") },
{ 0xA104, F("SmartSolar MPPT VE.Can 150|60") },
{ 0xA105, F("SmartSolar MPPT VE.Can 150|85") },
{ 0xA106, F("SmartSolar MPPT VE.Can 150|100") },
{ 0xA107, F("SmartSolar MPPT VE.Can 250|45") },
{ 0xA108, F("SmartSolar MPPT VE.Can 250|60") },
{ 0xA109, F("SmartSolar MPPT VE.Can 250|80") },
{ 0xA10A, F("SmartSolar MPPT VE.Can 250|85") },
{ 0xA10B, F("SmartSolar MPPT VE.Can 250|100") },
{ 0xA10C, F("SmartSolar MPPT VE.Can 150|70 rev2") },
{ 0xA10D, F("SmartSolar MPPT VE.Can 150|85 rev2") },
{ 0xA10E, F("SmartSolar MPPT VE.Can 150|100 rev2") },
{ 0xA10F, F("BlueSolar MPPT VE.Can 150|100") },
{ 0xA110, F("SmartSolar MPPT RS 450|100") },
{ 0xA112, F("BlueSolar MPPT VE.Can 250|70") },
{ 0xA113, F("BlueSolar MPPT VE.Can 250|100") },
{ 0xA114, F("SmartSolar MPPT VE.Can 250|70 rev2") },
{ 0xA115, F("SmartSolar MPPT VE.Can 250|100 rev2") },
{ 0xA116, F("SmartSolar MPPT VE.Can 250|85 rev2") },
{ 0xA381, F("BMV-712 Smart") },
{ 0xA382, F("BMV-710H Smart") },
{ 0xA383, F("BMV-712 Smart Rev2") },
{ 0xA389, F("SmartShunt 500A/50mV") },
{ 0xA38A, F("SmartShunt 1000A/50mV") },
{ 0xA38B, F("SmartShunt 2000A/50mV") },
{ 0xA3F0, F("SmartShunt 2000A/50mV" ) }
};
switch(pid) {
case 0x0300:
strPID = "BlueSolar MPPT 70|15";
break;
case 0xA040:
strPID = "BlueSolar MPPT 75|50";
break;
case 0xA041:
strPID = "BlueSolar MPPT 150|35";
break;
case 0xA042:
strPID = "BlueSolar MPPT 75|15";
break;
case 0xA043:
strPID = "BlueSolar MPPT 100|15";
break;
case 0xA044:
strPID = "BlueSolar MPPT 100|30";
break;
case 0xA045:
strPID = "BlueSolar MPPT 100|50";
break;
case 0xA046:
strPID = "BlueSolar MPPT 100|70";
break;
case 0xA047:
strPID = "BlueSolar MPPT 150|100";
break;
case 0xA049:
strPID = "BlueSolar MPPT 100|50 rev2";
break;
case 0xA04A:
strPID = "BlueSolar MPPT 100|30 rev2";
break;
case 0xA04B:
strPID = "BlueSolar MPPT 150|35 rev2";
break;
case 0XA04C:
strPID = "BlueSolar MPPT 75|10";
break;
case 0XA04D:
strPID = "BlueSolar MPPT 150|45";
break;
case 0XA04E:
strPID = "BlueSolar MPPT 150|60";
break;
case 0XA04F:
strPID = "BlueSolar MPPT 150|85";
break;
case 0XA050:
strPID = "SmartSolar MPPT 250|100";
break;
case 0XA051:
strPID = "SmartSolar MPPT 150|100";
break;
case 0XA052:
strPID = "SmartSolar MPPT 150|85";
break;
case 0XA053:
strPID = "SmartSolar MPPT 75|15";
break;
case 0XA054:
strPID = "SmartSolar MPPT 75|10";
break;
case 0XA055:
strPID = "SmartSolar MPPT 100|15";
break;
case 0XA056:
strPID = "SmartSolar MPPT 100|30";
break;
case 0XA057:
strPID = "SmartSolar MPPT 100|50";
break;
case 0XA058:
strPID = "SmartSolar MPPT 150|35";
break;
case 0XA059:
strPID = "SmartSolar MPPT 150|10 rev2";
break;
case 0XA05A:
strPID = "SmartSolar MPPT 150|85 rev2";
break;
case 0XA05B:
strPID = "SmartSolar MPPT 250|70";
break;
case 0XA05C:
strPID = "SmartSolar MPPT 250|85";
break;
case 0XA05D:
strPID = "SmartSolar MPPT 250|60";
break;
case 0XA05E:
strPID = "SmartSolar MPPT 250|45";
break;
case 0XA05F:
strPID = "SmartSolar MPPT 100|20";
break;
case 0XA060:
strPID = "SmartSolar MPPT 100|20 48V";
break;
case 0XA061:
strPID = "SmartSolar MPPT 150|45";
break;
case 0XA062:
strPID = "SmartSolar MPPT 150|60";
break;
case 0XA063:
strPID = "SmartSolar MPPT 150|70";
break;
case 0XA064:
strPID = "SmartSolar MPPT 250|85 rev2";
break;
case 0XA065:
strPID = "SmartSolar MPPT 250|100 rev2";
break;
case 0XA066:
strPID = "BlueSolar MPPT 100|20";
break;
case 0XA067:
strPID = "BlueSolar MPPT 100|20 48V";
break;
case 0XA068:
strPID = "SmartSolar MPPT 250|60 rev2";
break;
case 0XA069:
strPID = "SmartSolar MPPT 250|70 rev2";
break;
case 0XA06A:
strPID = "SmartSolar MPPT 150|45 rev2";
break;
case 0XA06B:
strPID = "SmartSolar MPPT 150|60 rev2";
break;
case 0XA06C:
strPID = "SmartSolar MPPT 150|70 rev2";
break;
case 0XA06D:
strPID = "SmartSolar MPPT 150|85 rev3";
break;
case 0XA06E:
strPID = "SmartSolar MPPT 150|100 rev3";
break;
case 0XA06F:
strPID = "BlueSolar MPPT 150|45 rev2";
break;
case 0XA070:
strPID = "BlueSolar MPPT 150|60 rev2";
break;
case 0XA071:
strPID = "BlueSolar MPPT 150|70 rev2";
break;
case 0XA102:
strPID = "SmartSolar MPPT VE.Can 150|70";
break;
case 0XA103:
strPID = "SmartSolar MPPT VE.Can 150|45";
break;
case 0XA104:
strPID = "SmartSolar MPPT VE.Can 150|60";
break;
case 0XA105:
strPID = "SmartSolar MPPT VE.Can 150|85";
break;
case 0XA106:
strPID = "SmartSolar MPPT VE.Can 150|100";
break;
case 0XA107:
strPID = "SmartSolar MPPT VE.Can 250|45";
break;
case 0XA108:
strPID = "SmartSolar MPPT VE.Can 250|60";
break;
case 0XA109:
strPID = "SmartSolar MPPT VE.Can 250|80";
break;
case 0XA10A:
strPID = "SmartSolar MPPT VE.Can 250|85";
break;
case 0XA10B:
strPID = "SmartSolar MPPT VE.Can 250|100";
break;
case 0XA10C:
strPID = "SmartSolar MPPT VE.Can 150|70 rev2";
break;
case 0XA10D:
strPID = "SmartSolar MPPT VE.Can 150|85 rev2";
break;
case 0XA10E:
strPID = "SmartSolar MPPT VE.Can 150|100 rev2";
break;
case 0XA10F:
strPID = "BlueSolar MPPT VE.Can 150|100";
break;
case 0XA110:
strPID = "SmartSolar MPPT RS 450|100";
break;
case 0XA112:
strPID = "BlueSolar MPPT VE.Can 250|70";
break;
case 0XA113:
strPID = "BlueSolar MPPT VE.Can 250|100";
break;
case 0XA114:
strPID = "SmartSolar MPPT VE.Can 250|70 rev2";
break;
case 0XA115:
strPID = "SmartSolar MPPT VE.Can 250|100 rev2";
break;
case 0XA116:
strPID = "SmartSolar MPPT VE.Can 250|85 rev2";
break;
case 0xA381:
strPID = "BMV-712 Smart";
break;
case 0xA382:
strPID = "BMV-710H Smart";
break;
case 0xA383:
strPID = "BMV-712 Smart Rev2";
break;
case 0xA389:
strPID = "SmartShunt 500A/50mV";
break;
case 0xA38A:
strPID = "SmartShunt 1000A/50mV";
break;
case 0xA38B:
strPID = "SmartShunt 2000A/50mV";
break;
case 0xA3F0:
strPID = "SmartShunt 2000A/50mV" ;
break;
default:
strPID = pid;
}
return strPID;
}
/*
* getErrAsString
* This function returns error state (ERR) as readable text.
*/
String VeDirectFrameHandler::getErrAsString(uint8_t err)
{
String strERR ="";
switch(err) {
case 0:
strERR = "No error";
break;
case 2:
strERR = "Battery voltage too high";
break;
case 17:
strERR = "Charger temperature too high";
break;
case 18:
strERR = "Charger over current";
break;
case 19:
strERR = "Charger current reversed";
break;
case 20:
strERR = "Bulk time limit exceeded";
break;
case 21:
strERR = "Current sensor issue(sensor bias/sensor broken)";
break;
case 26:
strERR = "Terminals overheated";
break;
case 28:
strERR = "Converter issue (dual converter models only)";
break;
case 33:
strERR = "Input voltage too high (solar panel)";
break;
case 34:
strERR = "Input current too high (solar panel)";
break;
case 38:
strERR = "Input shutdown (due to excessive battery voltage)";
break;
case 39:
strERR = "Input shutdown (due to current flow during off mode)";
break;
case 40:
strERR = "Input";
break;
case 65:
strERR = "Lost communication with one of devices";
break;
case 67:
strERR = "Synchronisedcharging device configuration issue";
break;
case 68:
strERR = "BMS connection lost";
break;
case 116:
strERR = "Factory calibration data lost";
break;
case 117:
strERR = "Invalid/incompatible firmware";
break;
case 118:
strERR = "User settings invalid";
break;
default:
strERR = err;
}
return strERR;
return getAsString(values, PID);
}

View File

@ -13,45 +13,47 @@
#include <Arduino.h>
#include <array>
#include <map>
#include <memory>
#define VE_MAX_VALUE_LEN 33 // VE.Direct Protocol: max value size is 33 including /0
#define VE_MAX_HEX_LEN 100 // Maximum size of hex frame - max payload 34 byte (=68 char) + safe buffer
typedef struct {
uint16_t PID = 0; // product id
char SER[VE_MAX_VALUE_LEN]; // serial number
char FW[VE_MAX_VALUE_LEN]; // firmware release number
int32_t P = 0; // battery output power in W (calculated)
double V = 0; // battery voltage in V
double I = 0; // battery current in A
double E = 0; // efficiency in percent (calculated, moving average)
} veStruct;
class VeDirectFrameHandler {
public:
VeDirectFrameHandler();
void setVerboseLogging(bool verboseLogging);
virtual void init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort);
void loop(); // main loop to read ve.direct data
unsigned long getLastUpdate(); // timestamp of last successful frame read
bool isDataValid(veStruct frame); // return true if data valid and not outdated
String getPidAsString(uint16_t pid); // product id as string
String getErrAsString(uint8_t err); // errer state as string
uint32_t getLastUpdate() const; // timestamp of last successful frame read
protected:
void textRxEvent(char *, char *, veStruct& );
bool _verboseLogging;
Print* _msgOut;
uint32_t _lastUpdate;
typedef struct {
uint16_t PID = 0; // product id
char SER[VE_MAX_VALUE_LEN]; // serial number
char FW[VE_MAX_VALUE_LEN]; // firmware release number
double V = 0; // battery voltage in V
double I = 0; // battery current in A
double E = 0; // efficiency in percent (calculated, moving average)
String getPidAsString() const; // product id as string
} veStruct;
bool textRxEvent(std::string const& who, char* name, char* value, veStruct& frame);
bool isDataValid(veStruct const& frame) const; // return true if data valid and not outdated
template<typename T>
static String const& getAsString(std::map<T, String> const& values, T val);
private:
void setLastUpdate(); // set timestampt after successful frame read
void dumpDebugBuffer();
void rxData(uint8_t inbyte); // byte of serial data
virtual void textRxEvent(char *, char *) = 0;
virtual void frameEndEvent(bool) = 0; // copy temp struct to public struct
virtual void frameValidEvent() = 0;
int hexRxEvent(uint8_t);
std::unique_ptr<HardwareSerial> _vedirectSerial;

View File

@ -1,25 +1,24 @@
#include <Arduino.h>
#include <map>
#include "VeDirectMpptController.h"
VeDirectMpptController VeDirectMppt;
VeDirectMpptController::VeDirectMpptController()
{
}
void VeDirectMpptController::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging)
{
VeDirectFrameHandler::init(rx, tx, msgOut, verboseLogging, 1);
_spData = std::make_shared<veMpptStruct>();
if (_verboseLogging) { _msgOut->println("Finished init MPPTController"); }
}
bool VeDirectMpptController::isDataValid() {
return VeDirectFrameHandler::isDataValid(veFrame);
bool VeDirectMpptController::isDataValid() const {
return VeDirectFrameHandler::isDataValid(*_spData);
}
void VeDirectMpptController::textRxEvent(char * name, char * value) {
if (_verboseLogging) { _msgOut->printf("[Victron MPPT] Received Text Event %s: Value: %s\r\n", name, value ); }
VeDirectFrameHandler::textRxEvent(name, value, _tmpFrame);
void VeDirectMpptController::textRxEvent(char* name, char* value)
{
if (VeDirectFrameHandler::textRxEvent("MPPT", name, value, _tmpFrame)) {
return;
}
if (strcmp(name, "LOAD") == 0) {
if (strcmp(value, "ON") == 0)
_tmpFrame.LOAD = true;
@ -65,13 +64,10 @@ void VeDirectMpptController::textRxEvent(char * name, char * value) {
}
/*
* frameEndEvent
* This function is called at the end of the received frame. If the checksum is valid, the temp buffer is read line by line.
* If the name exists in the public buffer, the new value is copied to the public buffer. If not, a new name/value entry
* is created in the public buffer.
* frameValidEvent
* This function is called at the end of the received frame.
*/
void VeDirectMpptController::frameEndEvent(bool valid) {
if (valid) {
void VeDirectMpptController::frameValidEvent() {
_tmpFrame.P = _tmpFrame.V * _tmpFrame.I;
_tmpFrame.IPV = 0;
@ -85,119 +81,97 @@ void VeDirectMpptController::frameEndEvent(bool valid) {
_tmpFrame.E = _efficiency.getAverage();
}
veFrame = _tmpFrame;
_spData = std::make_shared<veMpptStruct>(_tmpFrame);
_tmpFrame = {};
_lastUpdate = millis();
}
}
/*
* getCsAsString
* This function returns the state of operations (CS) as readable text.
*/
String VeDirectMpptController::getCsAsString(uint8_t cs)
String VeDirectMpptController::veMpptStruct::getCsAsString() const
{
String strCS ="";
static const std::map<uint8_t, String> values = {
{ 0, F("OFF") },
{ 2, F("Fault") },
{ 3, F("Bulk") },
{ 4, F("Absorbtion") },
{ 5, F("Float") },
{ 7, F("Equalize (manual)") },
{ 245, F("Starting-up") },
{ 247, F("Auto equalize / Recondition") },
{ 252, F("External Control") }
};
switch(cs) {
case 0:
strCS = "OFF";
break;
case 2:
strCS = "Fault";
break;
case 3:
strCS = "Bulk";
break;
case 4:
strCS = "Absorbtion";
break;
case 5:
strCS = "Float";
break;
case 7:
strCS = "Equalize (manual)";
break;
case 245:
strCS = "Starting-up";
break;
case 247:
strCS = "Auto equalize / Recondition";
break;
case 252:
strCS = "External Control";
break;
default:
strCS = cs;
}
return strCS;
return getAsString(values, CS);
}
/*
* getMpptAsString
* This function returns the state of MPPT (MPPT) as readable text.
*/
String VeDirectMpptController::getMpptAsString(uint8_t mppt)
String VeDirectMpptController::veMpptStruct::getMpptAsString() const
{
String strMPPT ="";
static const std::map<uint8_t, String> values = {
{ 0, F("OFF") },
{ 1, F("Voltage or current limited") },
{ 2, F("MPP Tracker active") }
};
switch(mppt) {
case 0:
strMPPT = "OFF";
break;
case 1:
strMPPT = "Voltage or current limited";
break;
case 2:
strMPPT = "MPP Tracker active";
break;
default:
strMPPT = mppt;
}
return strMPPT;
return getAsString(values, MPPT);
}
/*
* getErrAsString
* This function returns error state (ERR) as readable text.
*/
String VeDirectMpptController::veMpptStruct::getErrAsString() const
{
static const std::map<uint8_t, String> values = {
{ 0, F("No error") },
{ 2, F("Battery voltage too high") },
{ 17, F("Charger temperature too high") },
{ 18, F("Charger over current") },
{ 19, F("Charger current reversed") },
{ 20, F("Bulk time limit exceeded") },
{ 21, F("Current sensor issue(sensor bias/sensor broken)") },
{ 26, F("Terminals overheated") },
{ 28, F("Converter issue (dual converter models only)") },
{ 33, F("Input voltage too high (solar panel)") },
{ 34, F("Input current too high (solar panel)") },
{ 38, F("Input shutdown (due to excessive battery voltage)") },
{ 39, F("Input shutdown (due to current flow during off mode)") },
{ 40, F("Input") },
{ 65, F("Lost communication with one of devices") },
{ 67, F("Synchronisedcharging device configuration issue") },
{ 68, F("BMS connection lost") },
{ 116, F("Factory calibration data lost") },
{ 117, F("Invalid/incompatible firmware") },
{ 118, F("User settings invalid") }
};
return getAsString(values, ERR);
}
/*
* getOrAsString
* This function returns the off reason (OR) as readable text.
*/
String VeDirectMpptController::getOrAsString(uint32_t offReason)
String VeDirectMpptController::veMpptStruct::getOrAsString() const
{
String strOR ="";
static const std::map<uint32_t, String> values = {
{ 0x00000000, F("Not off") },
{ 0x00000001, F("No input power") },
{ 0x00000002, F("Switched off (power switch)") },
{ 0x00000004, F("Switched off (device moderegister)") },
{ 0x00000008, F("Remote input") },
{ 0x00000010, F("Protection active") },
{ 0x00000020, F("Paygo") },
{ 0x00000040, F("BMS") },
{ 0x00000080, F("Engine shutdown detection") },
{ 0x00000100, F("Analysing input voltage") }
};
switch(offReason) {
case 0x00000000:
strOR = "Not off";
break;
case 0x00000001:
strOR = "No input power";
break;
case 0x00000002:
strOR = "Switched off (power switch)";
break;
case 0x00000004:
strOR = "Switched off (device moderegister)";
break;
case 0x00000008:
strOR = "Remote input";
break;
case 0x00000010:
strOR = "Protection active";
break;
case 0x00000020:
strOR = "Paygo";
break;
case 0x00000040:
strOR = "BMS";
break;
case 0x00000080:
strOR = "Engine shutdown detection";
break;
case 0x00000100:
strOR = "Analysing input voltage";
break;
default:
strOR = offReason;
}
return strOR;
return getAsString(values, OR);
}

View File

@ -37,17 +37,15 @@ private:
class VeDirectMpptController : public VeDirectFrameHandler {
public:
VeDirectMpptController();
VeDirectMpptController() = default;
void init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging);
String getMpptAsString(uint8_t mppt); // state of mppt as string
String getCsAsString(uint8_t cs); // current state as string
String getOrAsString(uint32_t offReason); // off reason as string
bool isDataValid(); // return true if data valid and not outdated
bool isDataValid() const; // return true if data valid and not outdated
struct veMpptStruct : veStruct {
uint8_t MPPT; // state of MPP tracker
int32_t PPV; // panel power in W
int32_t P; // battery output power in W (calculated)
double VPV; // panel voltage in V
double IPV; // panel current in A (calculated)
bool LOAD; // virtual load output state (on if battery voltage reaches upper limit, off if battery reaches lower limit)
@ -60,15 +58,20 @@ public:
int32_t H21; // maximum power today W
double H22; // yield yesterday kWh
int32_t H23; // maximum power yesterday W
String getMpptAsString() const; // state of mppt as string
String getCsAsString() const; // current state as string
String getErrAsString() const; // error state as string
String getOrAsString() const; // off reason as string
};
veMpptStruct veFrame{};
using spData_t = std::shared_ptr<veMpptStruct const>;
spData_t getData() const { return _spData; }
private:
void textRxEvent(char * name, char * value) final;
void frameEndEvent(bool) final; // copy temp struct to public struct
void textRxEvent(char* name, char* value) final;
void frameValidEvent() final;
spData_t _spData = nullptr;
veMpptStruct _tmpFrame{}; // private struct for received name and value pairs
MovingAverage<double, 5> _efficiency;
};
extern VeDirectMpptController VeDirectMppt;

View File

@ -17,10 +17,10 @@ void VeDirectShuntController::init(int8_t rx, int8_t tx, Print* msgOut, bool ver
void VeDirectShuntController::textRxEvent(char* name, char* value)
{
VeDirectFrameHandler::textRxEvent(name, value, _tmpFrame);
if (_verboseLogging) {
_msgOut->printf("[Victron SmartShunt] Received Text Event %s: Value: %s\r\n", name, value );
if (VeDirectFrameHandler::textRxEvent("SmartShunt", name, value, _tmpFrame)) {
return;
}
if (strcmp(name, "T") == 0) {
_tmpFrame.T = atoi(value);
}
@ -96,18 +96,16 @@ void VeDirectShuntController::textRxEvent(char* name, char* value)
}
/*
* frameEndEvent
* This function is called at the end of the received frame. If the checksum is valid, the temp buffer is read line by line.
* If the name exists in the public buffer, the new value is copied to the public buffer. If not, a new name/value entry
* is created in the public buffer.
* frameValidEvent
* This function is called at the end of the received frame.
*/
void VeDirectShuntController::frameEndEvent(bool valid) {
void VeDirectShuntController::frameValidEvent() {
// other than in the MPPT controller, the SmartShunt seems to split all data
// into two seperate messagesas. Thus we update veFrame only every second message
// after a value for PID has been received
if (valid && _tmpFrame.PID != 0) {
if (_tmpFrame.PID == 0) { return; }
veFrame = _tmpFrame;
_tmpFrame = {};
_lastUpdate = millis();
}
}

View File

@ -41,7 +41,7 @@ public:
private:
void textRxEvent(char * name, char * value) final;
void frameEndEvent(bool) final; // copy temp struct to public struct
void frameValidEvent() final;
veShuntStruct _tmpFrame{}; // private struct for received name and value pairs
};

View File

@ -208,7 +208,7 @@ void VictronSmartShuntStats::updateFrom(VeDirectShuntController::veShuntStruct c
_SoC = shuntData.SOC / 10;
_voltage = shuntData.V;
_current = shuntData.I;
_modelName = VeDirectShunt.getPidAsString(shuntData.PID);
_modelName = shuntData.getPidAsString();
_chargeCycles = shuntData.H4;
_timeToGo = shuntData.TTG / 60;
_chargedEnergy = shuntData.H18 / 100;

View File

@ -7,6 +7,7 @@
#include "MqttSettings.h"
#include "NetworkSettings.h"
#include "MessageOutput.h"
#include "VictronMppt.h"
MqttHandleVedirectHassClass MqttHandleVedirectHass;
@ -50,7 +51,7 @@ void MqttHandleVedirectHassClass::publishConfig()
return;
}
// ensure data is revieved from victron
if (!VeDirectMppt.isDataValid()) {
if (!VictronMppt.isDataValid()) {
return;
}
@ -82,7 +83,7 @@ void MqttHandleVedirectHassClass::publishConfig()
void MqttHandleVedirectHassClass::publishSensor(const char* caption, const char* icon, const char* subTopic, const char* deviceClass, const char* stateClass, const char* unitOfMeasurement )
{
String serial = VeDirectMppt.veFrame.SER;
String serial = VictronMppt.getData()->SER;
String sensorId = caption;
sensorId.replace(" ", "_");
@ -96,7 +97,7 @@ void MqttHandleVedirectHassClass::publishSensor(const char* caption, const char*
+ "/config";
String statTopic = MqttSettings.getPrefix() + "victron/";
statTopic.concat(VeDirectMppt.veFrame.SER);
statTopic.concat(serial);
statTopic.concat("/");
statTopic.concat(subTopic);
@ -133,7 +134,7 @@ void MqttHandleVedirectHassClass::publishSensor(const char* caption, const char*
}
void MqttHandleVedirectHassClass::publishBinarySensor(const char* caption, const char* icon, const char* subTopic, const char* payload_on, const char* payload_off)
{
String serial = VeDirectMppt.veFrame.SER;
String serial = VictronMppt.getData()->SER;
String sensorId = caption;
sensorId.replace(" ", "_");
@ -147,7 +148,7 @@ void MqttHandleVedirectHassClass::publishBinarySensor(const char* caption, const
+ "/config";
String statTopic = MqttSettings.getPrefix() + "victron/";
statTopic.concat(VeDirectMppt.veFrame.SER);
statTopic.concat(serial);
statTopic.concat("/");
statTopic.concat(subTopic);
@ -172,12 +173,13 @@ void MqttHandleVedirectHassClass::publishBinarySensor(const char* caption, const
void MqttHandleVedirectHassClass::createDeviceInfo(JsonObject& object)
{
String serial = VeDirectMppt.veFrame.SER;
auto spMpptData = VictronMppt.getData();
String serial = spMpptData->SER;
object[F("name")] = "Victron(" + serial + ")";
object[F("ids")] = serial;
object[F("cu")] = String(F("http://")) + NetworkSettings.localIP().toString();
object[F("mf")] = F("OpenDTU");
object[F("mdl")] = VeDirectMppt.getPidAsString(VeDirectMppt.veFrame.PID);
object[F("mdl")] = spMpptData->getPidAsString();
object[F("sw")] = AUTO_GIT_HASH;
}

View File

@ -2,7 +2,7 @@
/*
* Copyright (C) 2022 Helge Erbe and others
*/
#include "VeDirectMpptController.h"
#include "VictronMppt.h"
#include "MqttHandleVedirect.h"
#include "MqttSettings.h"
#include "MessageOutput.h"
@ -29,7 +29,7 @@ void MqttHandleVedirectClass::loop()
return;
}
if (!VeDirectMppt.isDataValid()) {
if (!VictronMppt.isDataValid()) {
return;
}
@ -50,81 +50,82 @@ void MqttHandleVedirectClass::loop()
}
#endif
auto spMpptData = VictronMppt.getData();
String value;
String topic = "victron/";
topic.concat(VeDirectMppt.veFrame.SER);
topic.concat(spMpptData->SER);
topic.concat("/");
if (_PublishFull || VeDirectMppt.veFrame.PID != _kvFrame.PID)
MqttSettings.publish(topic + "PID", VeDirectMppt.getPidAsString(VeDirectMppt.veFrame.PID));
if (_PublishFull || strcmp(VeDirectMppt.veFrame.SER, _kvFrame.SER) != 0)
MqttSettings.publish(topic + "SER", VeDirectMppt.veFrame.SER );
if (_PublishFull || strcmp(VeDirectMppt.veFrame.FW, _kvFrame.FW) != 0)
MqttSettings.publish(topic + "FW", VeDirectMppt.veFrame.FW);
if (_PublishFull || VeDirectMppt.veFrame.LOAD != _kvFrame.LOAD)
MqttSettings.publish(topic + "LOAD", VeDirectMppt.veFrame.LOAD == true ? "ON": "OFF");
if (_PublishFull || VeDirectMppt.veFrame.CS != _kvFrame.CS)
MqttSettings.publish(topic + "CS", VeDirectMppt.getCsAsString(VeDirectMppt.veFrame.CS));
if (_PublishFull || VeDirectMppt.veFrame.ERR != _kvFrame.ERR)
MqttSettings.publish(topic + "ERR", VeDirectMppt.getErrAsString(VeDirectMppt.veFrame.ERR));
if (_PublishFull || VeDirectMppt.veFrame.OR != _kvFrame.OR)
MqttSettings.publish(topic + "OR", VeDirectMppt.getOrAsString(VeDirectMppt.veFrame.OR));
if (_PublishFull || VeDirectMppt.veFrame.MPPT != _kvFrame.MPPT)
MqttSettings.publish(topic + "MPPT", VeDirectMppt.getMpptAsString(VeDirectMppt.veFrame.MPPT));
if (_PublishFull || VeDirectMppt.veFrame.HSDS != _kvFrame.HSDS) {
value = VeDirectMppt.veFrame.HSDS;
if (_PublishFull || spMpptData->PID != _kvFrame.PID)
MqttSettings.publish(topic + "PID", spMpptData->getPidAsString());
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());
if (_PublishFull || spMpptData->ERR != _kvFrame.ERR)
MqttSettings.publish(topic + "ERR", spMpptData->getErrAsString());
if (_PublishFull || spMpptData->OR != _kvFrame.OR)
MqttSettings.publish(topic + "OR", spMpptData->getOrAsString());
if (_PublishFull || spMpptData->MPPT != _kvFrame.MPPT)
MqttSettings.publish(topic + "MPPT", spMpptData->getMpptAsString());
if (_PublishFull || spMpptData->HSDS != _kvFrame.HSDS) {
value = spMpptData->HSDS;
MqttSettings.publish(topic + "HSDS", value);
}
if (_PublishFull || VeDirectMppt.veFrame.V != _kvFrame.V) {
value = VeDirectMppt.veFrame.V;
if (_PublishFull || spMpptData->V != _kvFrame.V) {
value = spMpptData->V;
MqttSettings.publish(topic + "V", value);
}
if (_PublishFull || VeDirectMppt.veFrame.I != _kvFrame.I) {
value = VeDirectMppt.veFrame.I;
if (_PublishFull || spMpptData->I != _kvFrame.I) {
value = spMpptData->I;
MqttSettings.publish(topic + "I", value);
}
if (_PublishFull || VeDirectMppt.veFrame.P != _kvFrame.P) {
value = VeDirectMppt.veFrame.P;
if (_PublishFull || spMpptData->P != _kvFrame.P) {
value = spMpptData->P;
MqttSettings.publish(topic + "P", value);
}
if (_PublishFull || VeDirectMppt.veFrame.VPV != _kvFrame.VPV) {
value = VeDirectMppt.veFrame.VPV;
if (_PublishFull || spMpptData->VPV != _kvFrame.VPV) {
value = spMpptData->VPV;
MqttSettings.publish(topic + "VPV", value);
}
if (_PublishFull || VeDirectMppt.veFrame.IPV != _kvFrame.IPV) {
value = VeDirectMppt.veFrame.IPV;
if (_PublishFull || spMpptData->IPV != _kvFrame.IPV) {
value = spMpptData->IPV;
MqttSettings.publish(topic + "IPV", value);
}
if (_PublishFull || VeDirectMppt.veFrame.PPV != _kvFrame.PPV) {
value = VeDirectMppt.veFrame.PPV;
if (_PublishFull || spMpptData->PPV != _kvFrame.PPV) {
value = spMpptData->PPV;
MqttSettings.publish(topic + "PPV", value);
}
if (_PublishFull || VeDirectMppt.veFrame.E != _kvFrame.E) {
value = VeDirectMppt.veFrame.E;
if (_PublishFull || spMpptData->E != _kvFrame.E) {
value = spMpptData->E;
MqttSettings.publish(topic + "E", value);
}
if (_PublishFull || VeDirectMppt.veFrame.H19 != _kvFrame.H19) {
value = VeDirectMppt.veFrame.H19;
if (_PublishFull || spMpptData->H19 != _kvFrame.H19) {
value = spMpptData->H19;
MqttSettings.publish(topic + "H19", value);
}
if (_PublishFull || VeDirectMppt.veFrame.H20 != _kvFrame.H20) {
value = VeDirectMppt.veFrame.H20;
if (_PublishFull || spMpptData->H20 != _kvFrame.H20) {
value = spMpptData->H20;
MqttSettings.publish(topic + "H20", value);
}
if (_PublishFull || VeDirectMppt.veFrame.H21 != _kvFrame.H21) {
value = VeDirectMppt.veFrame.H21;
if (_PublishFull || spMpptData->H21 != _kvFrame.H21) {
value = spMpptData->H21;
MqttSettings.publish(topic + "H21", value);
}
if (_PublishFull || VeDirectMppt.veFrame.H22 != _kvFrame.H22) {
value = VeDirectMppt.veFrame.H22;
if (_PublishFull || spMpptData->H22 != _kvFrame.H22) {
value = spMpptData->H22;
MqttSettings.publish(topic + "H22", value);
}
if (_PublishFull || VeDirectMppt.veFrame.H23 != _kvFrame.H23) {
value = VeDirectMppt.veFrame.H23;
if (_PublishFull || spMpptData->H23 != _kvFrame.H23) {
value = spMpptData->H23;
MqttSettings.publish(topic + "H23", value);
}
if (!_PublishFull) {
_kvFrame= VeDirectMppt.veFrame;
_kvFrame = *spMpptData;
}
// now calculate next points of time to publish

View File

@ -305,11 +305,6 @@ bool PinMappingClass::isValidEthConfig()
return _pinMapping.eth_enabled;
}
bool PinMappingClass::isValidVictronConfig()
{
return _pinMapping.victron_rx >= 0;
}
bool PinMappingClass::isValidHuaweiConfig()
{
return _pinMapping.huawei_miso >= 0

View File

@ -10,7 +10,7 @@
#include "MqttSettings.h"
#include "NetworkSettings.h"
#include "Huawei_can.h"
#include <VeDirectMpptController.h>
#include <VictronMppt.h>
#include "MessageOutput.h"
#include <ctime>
#include <cmath>
@ -364,14 +364,12 @@ int32_t PowerLimiterClass::inverterPowerDcToAc(std::shared_ptr<InverterAbstract>
*/
void PowerLimiterClass::unconditionalSolarPassthrough(std::shared_ptr<InverterAbstract> inverter)
{
CONFIG_T& config = Configuration.get();
if (!config.Vedirect_Enabled || !VeDirectMppt.isDataValid()) {
if (!VictronMppt.isDataValid()) {
shutdown(Status::NoVeDirect);
return;
}
int32_t solarPower = VeDirectMppt.veFrame.V * VeDirectMppt.veFrame.I;
int32_t solarPower = VictronMppt.getPowerOutputWatts();
setNewPowerLimit(inverter, inverterPowerDcToAc(inverter, solarPower));
announceStatus(Status::UnconditionalSolarPassthrough);
}
@ -406,12 +404,11 @@ bool PowerLimiterClass::canUseDirectSolarPower()
if (!config.PowerLimiter_SolarPassThroughEnabled
|| isBelowStopThreshold()
|| !config.Vedirect_Enabled
|| !VeDirectMppt.isDataValid()) {
|| !VictronMppt.isDataValid()) {
return false;
}
return VeDirectMppt.veFrame.PPV >= 20; // enough power?
return VictronMppt.getPowerOutputWatts() >= 20; // enough power?
}
@ -576,7 +573,7 @@ int32_t PowerLimiterClass::getSolarChargePower()
return 0;
}
return VeDirectMppt.veFrame.V * VeDirectMppt.veFrame.I;
return VictronMppt.getPowerOutputWatts();
}
float PowerLimiterClass::getLoadCorrectedVoltage()

96
src/VictronMppt.cpp Normal file
View File

@ -0,0 +1,96 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "VictronMppt.h"
#include "Configuration.h"
#include "PinMapping.h"
#include "MessageOutput.h"
VictronMpptClass VictronMppt;
void VictronMpptClass::init()
{
std::lock_guard<std::mutex> lock(_mutex);
_controllers.clear();
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);
if (rx < 0) {
MessageOutput.println(F("[VictronMppt] invalid pin config"));
return;
}
auto upController = std::make_unique<VeDirectMpptController>();
upController->init(rx, tx, &MessageOutput, config.Vedirect_VerboseLogging);
_controllers.push_back(std::move(upController));
}
void VictronMpptClass::loop()
{
std::lock_guard<std::mutex> lock(_mutex);
for (auto const& upController : _controllers) {
upController->loop();
}
}
bool VictronMpptClass::isDataValid() const
{
std::lock_guard<std::mutex> lock(_mutex);
for (auto const& upController : _controllers) {
if (!upController->isDataValid()) { return false; }
}
return !_controllers.empty();
}
uint32_t VictronMpptClass::getDataAgeMillis() const
{
std::lock_guard<std::mutex> 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<uint32_t>(age, now - (*iter)->getLastUpdate());
++iter;
}
return age;
}
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 VeDirectMpptController::spData_t{};
}
return _controllers[idx]->getData();
}
int32_t VictronMpptClass::getPowerOutputWatts() const
{
int32_t sum = 0;
for (const auto& upController : _controllers) {
sum += upController->getData()->P;
}
return sum;
}

View File

@ -3,7 +3,7 @@
* Copyright (C) 2022 Thomas Basler and others
*/
#include "WebApi_vedirect.h"
#include "VeDirectMpptController.h"
#include "VictronMppt.h"
#include "ArduinoJson.h"
#include "AsyncJson.h"
#include "Configuration.h"
@ -117,7 +117,7 @@ void WebApiVedirectClass::onVedirectAdminPost(AsyncWebServerRequest* request)
config.Vedirect_UpdatesOnly = root[F("vedirect_updatesonly")].as<bool>();
Configuration.write();
VeDirectMppt.setVerboseLogging(config.Vedirect_VerboseLogging);
VictronMppt.init();
retMsg[F("type")] = F("success");
retMsg[F("message")] = F("Settings saved!");

View File

@ -10,7 +10,7 @@
#include "Battery.h"
#include "Huawei_can.h"
#include "PowerMeter.h"
#include "VeDirectMpptController.h"
#include "VictronMppt.h"
#include "defaults.h"
#include <AsyncJson.h>
@ -191,9 +191,11 @@ void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root)
JsonObject vedirectObj = root.createNestedObject("vedirect");
vedirectObj[F("enabled")] = Configuration.get().Vedirect_Enabled;
JsonObject totalVeObj = vedirectObj.createNestedObject("total");
addTotalField(totalVeObj, "Power", VeDirectMppt.veFrame.PPV, "W", 1);
addTotalField(totalVeObj, "YieldDay", VeDirectMppt.veFrame.H20 * 1000, "Wh", 0);
addTotalField(totalVeObj, "YieldTotal", VeDirectMppt.veFrame.H19, "kWh", 2);
auto spMpptData = VictronMppt.getData();
addTotalField(totalVeObj, "Power", spMpptData->PPV, "W", 1);
addTotalField(totalVeObj, "YieldDay", spMpptData->H20 * 1000, "Wh", 0);
addTotalField(totalVeObj, "YieldTotal", spMpptData->H19, "kWh", 2);
JsonObject huaweiObj = root.createNestedObject("huawei");
huaweiObj[F("enabled")] = Configuration.get().Huawei_Enabled;

View File

@ -9,6 +9,7 @@
#include "WebApi.h"
#include "defaults.h"
#include "PowerLimiter.h"
#include "VictronMppt.h"
WebApiWsVedirectLiveClass::WebApiWsVedirectLiveClass()
: _ws("/vedirectlivedata")
@ -44,18 +45,14 @@ void WebApiWsVedirectLiveClass::loop()
return;
}
if (millis() - _lastVedirectUpdateCheck < 1000) {
return;
}
_lastVedirectUpdateCheck = millis();
uint32_t maxTimeStamp = 0;
if (VeDirectMppt.getLastUpdate() > maxTimeStamp) {
maxTimeStamp = VeDirectMppt.getLastUpdate();
}
// 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.
auto lastDataAgeMillis = _dataAgeMillis;
_dataAgeMillis = VictronMppt.getDataAgeMillis();
// Update on ve.direct change or at least after 10 seconds
if (millis() - _lastWsPublish > (10 * 1000) || (maxTimeStamp != _newestVedirectTimestamp)) {
if (millis() - _lastWsPublish > (10 * 1000) || lastDataAgeMillis > _dataAgeMillis) {
try {
String buffer;
@ -87,57 +84,59 @@ void WebApiWsVedirectLiveClass::loop()
void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root)
{
auto spMpptData = VictronMppt.getData();
// device info
root["device"]["data_age"] = (millis() - VeDirectMppt.getLastUpdate() ) / 1000;
root["device"]["age_critical"] = !VeDirectMppt.isDataValid();
root["device"]["PID"] = VeDirectMppt.getPidAsString(VeDirectMppt.veFrame.PID);
root["device"]["SER"] = VeDirectMppt.veFrame.SER;
root["device"]["FW"] = VeDirectMppt.veFrame.FW;
root["device"]["LOAD"] = VeDirectMppt.veFrame.LOAD == true ? "ON" : "OFF";
root["device"]["CS"] = VeDirectMppt.getCsAsString(VeDirectMppt.veFrame.CS);
root["device"]["ERR"] = VeDirectMppt.getErrAsString(VeDirectMppt.veFrame.ERR);
root["device"]["OR"] = VeDirectMppt.getOrAsString(VeDirectMppt.veFrame.OR);
root["device"]["MPPT"] = VeDirectMppt.getMpptAsString(VeDirectMppt.veFrame.MPPT);
root["device"]["HSDS"]["v"] = VeDirectMppt.veFrame.HSDS;
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"]["CS"] = spMpptData->getCsAsString();
root["device"]["ERR"] = spMpptData->getErrAsString();
root["device"]["OR"] = spMpptData->getOrAsString();
root["device"]["MPPT"] = spMpptData->getMpptAsString();
root["device"]["HSDS"]["v"] = spMpptData->HSDS;
root["device"]["HSDS"]["u"] = "d";
// battery info
root["output"]["P"]["v"] = VeDirectMppt.veFrame.P;
root["output"]["P"]["v"] = spMpptData->P;
root["output"]["P"]["u"] = "W";
root["output"]["P"]["d"] = 0;
root["output"]["V"]["v"] = VeDirectMppt.veFrame.V;
root["output"]["V"]["v"] = spMpptData->V;
root["output"]["V"]["u"] = "V";
root["output"]["V"]["d"] = 2;
root["output"]["I"]["v"] = VeDirectMppt.veFrame.I;
root["output"]["I"]["v"] = spMpptData->I;
root["output"]["I"]["u"] = "A";
root["output"]["I"]["d"] = 2;
root["output"]["E"]["v"] = VeDirectMppt.veFrame.E;
root["output"]["E"]["v"] = spMpptData->E;
root["output"]["E"]["u"] = "%";
root["output"]["E"]["d"] = 1;
// panel info
root["input"]["PPV"]["v"] = VeDirectMppt.veFrame.PPV;
root["input"]["PPV"]["v"] = spMpptData->PPV;
root["input"]["PPV"]["u"] = "W";
root["input"]["PPV"]["d"] = 0;
root["input"]["VPV"]["v"] = VeDirectMppt.veFrame.VPV;
root["input"]["VPV"]["v"] = spMpptData->VPV;
root["input"]["VPV"]["u"] = "V";
root["input"]["VPV"]["d"] = 2;
root["input"]["IPV"]["v"] = VeDirectMppt.veFrame.IPV;
root["input"]["IPV"]["v"] = spMpptData->IPV;
root["input"]["IPV"]["u"] = "A";
root["input"]["IPV"]["d"] = 2;
root["input"]["YieldToday"]["v"] = VeDirectMppt.veFrame.H20;
root["input"]["YieldToday"]["v"] = spMpptData->H20;
root["input"]["YieldToday"]["u"] = "kWh";
root["input"]["YieldToday"]["d"] = 3;
root["input"]["YieldYesterday"]["v"] = VeDirectMppt.veFrame.H22;
root["input"]["YieldYesterday"]["v"] = spMpptData->H22;
root["input"]["YieldYesterday"]["u"] = "kWh";
root["input"]["YieldYesterday"]["d"] = 3;
root["input"]["YieldTotal"]["v"] = VeDirectMppt.veFrame.H19;
root["input"]["YieldTotal"]["v"] = spMpptData->H19;
root["input"]["YieldTotal"]["u"] = "kWh";
root["input"]["YieldTotal"]["d"] = 3;
root["input"]["MaximumPowerToday"]["v"] = VeDirectMppt.veFrame.H21;
root["input"]["MaximumPowerToday"]["v"] = spMpptData->H21;
root["input"]["MaximumPowerToday"]["u"] = "W";
root["input"]["MaximumPowerToday"]["d"] = 0;
root["input"]["MaximumPowerYesterday"]["v"] = VeDirectMppt.veFrame.H23;
root["input"]["MaximumPowerYesterday"]["v"] = spMpptData->H23;
root["input"]["MaximumPowerYesterday"]["u"] = "W";
root["input"]["MaximumPowerYesterday"]["d"] = 0;
@ -146,10 +145,6 @@ void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root)
if (Configuration.get().PowerLimiter_Enabled)
root["dpl"]["PLSTATE"] = PowerLimiter.getPowerLimiterState();
root["dpl"]["PLLIMIT"] = PowerLimiter.getLastRequestedPowerLimit();
if (VeDirectMppt.getLastUpdate() > _newestVedirectTimestamp) {
_newestVedirectTimestamp = VeDirectMppt.getLastUpdate();
}
}
void WebApiWsVedirectLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len)

View File

@ -8,7 +8,7 @@
#include "InverterSettings.h"
#include "Led_Single.h"
#include "MessageOutput.h"
#include "VeDirectMpptController.h"
#include "VictronMppt.h"
#include "Battery.h"
#include "Huawei_can.h"
#include "MqttHandleDtu.h"
@ -161,16 +161,8 @@ void setup()
Datastore.init();
// Initialize ve.direct communication
MessageOutput.println(F("Initialize ve.direct interface... "));
if (PinMapping.isValidVictronConfig()) {
MessageOutput.printf("ve.direct rx = %d, tx = %d\r\n", pin.victron_rx, pin.victron_tx);
VeDirectMppt.init(pin.victron_rx, pin.victron_tx,
&MessageOutput, config.Vedirect_VerboseLogging);
MessageOutput.println(F("done"));
} else {
MessageOutput.println(F("Invalid pin config"));
}
VictronMppt.init();
// Power meter
PowerMeter.init();
@ -202,11 +194,8 @@ void loop()
yield();
Datastore.loop();
yield();
// Vedirect_Enabled is unknown to lib. Therefor check has to be done here
if (Configuration.get().Vedirect_Enabled) {
VeDirectMppt.loop();
VictronMppt.loop();
yield();
}
MqttSettings.loop();
yield();
MqttHandleDtu.loop();