diff --git a/include/PinMapping.h b/include/PinMapping.h index 5e4c7fa0..c9541c4b 100644 --- a/include/PinMapping.h +++ b/include/PinMapping.h @@ -62,7 +62,6 @@ public: bool isValidNrf24Config(); bool isValidCmt2300Config(); bool isValidEthConfig(); - bool isValidVictronConfig(); bool isValidHuaweiConfig(); private: diff --git a/include/VictronMppt.h b/include/VictronMppt.h new file mode 100644 index 00000000..6718ef33 --- /dev/null +++ b/include/VictronMppt.h @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include + +#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; + std::vector _controllers; +}; + +extern VictronMpptClass VictronMppt; diff --git a/include/WebApi_ws_vedirect_live.h b/include/WebApi_ws_vedirect_live.h index 13b27d9f..d084e74e 100644 --- a/include/WebApi_ws_vedirect_live.h +++ b/include/WebApi_ws_vedirect_live.h @@ -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; }; \ No newline at end of file diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp index 39ce4fab..0c0c544e 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp +++ b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp @@ -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(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 +String const& VeDirectFrameHandler::getAsString(std::map 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 const& values, uint8_t val); +template String const& VeDirectFrameHandler::getAsString(std::map const& values, uint16_t val); +template String const& VeDirectFrameHandler::getAsString(std::map 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 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); } diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHandler.h b/lib/VeDirectFrameHandler/VeDirectFrameHandler.h index 2cb55487..bc6678b5 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHandler.h +++ b/lib/VeDirectFrameHandler/VeDirectFrameHandler.h @@ -13,45 +13,47 @@ #include #include +#include #include #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 + static String const& getAsString(std::map 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 _vedirectSerial; diff --git a/lib/VeDirectFrameHandler/VeDirectMpptController.cpp b/lib/VeDirectFrameHandler/VeDirectMpptController.cpp index 0f8246d7..5635cd45 100644 --- a/lib/VeDirectFrameHandler/VeDirectMpptController.cpp +++ b/lib/VeDirectFrameHandler/VeDirectMpptController.cpp @@ -1,25 +1,24 @@ #include +#include #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(); 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,139 +64,114 @@ 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) { - _tmpFrame.P = _tmpFrame.V * _tmpFrame.I; +void VeDirectMpptController::frameValidEvent() { + _tmpFrame.P = _tmpFrame.V * _tmpFrame.I; - _tmpFrame.IPV = 0; - if (_tmpFrame.VPV > 0) { - _tmpFrame.IPV = _tmpFrame.PPV / _tmpFrame.VPV; - } - - _tmpFrame.E = 0; - if ( _tmpFrame.PPV > 0) { - _efficiency.addNumber(static_cast(_tmpFrame.P * 100) / _tmpFrame.PPV); - _tmpFrame.E = _efficiency.getAverage(); - } - - veFrame = _tmpFrame; - _tmpFrame = {}; - _lastUpdate = millis(); + _tmpFrame.IPV = 0; + if (_tmpFrame.VPV > 0) { + _tmpFrame.IPV = _tmpFrame.PPV / _tmpFrame.VPV; } + + _tmpFrame.E = 0; + if ( _tmpFrame.PPV > 0) { + _efficiency.addNumber(static_cast(_tmpFrame.P * 100) / _tmpFrame.PPV); + _tmpFrame.E = _efficiency.getAverage(); + } + + _spData = std::make_shared(_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 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 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 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 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); } diff --git a/lib/VeDirectFrameHandler/VeDirectMpptController.h b/lib/VeDirectFrameHandler/VeDirectMpptController.h index 78945429..04e0d8ca 100644 --- a/lib/VeDirectFrameHandler/VeDirectMpptController.h +++ b/lib/VeDirectFrameHandler/VeDirectMpptController.h @@ -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; + 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 _efficiency; }; - -extern VeDirectMpptController VeDirectMppt; \ No newline at end of file diff --git a/lib/VeDirectFrameHandler/VeDirectShuntController.cpp b/lib/VeDirectFrameHandler/VeDirectShuntController.cpp index 249472c5..7a1fa59a 100644 --- a/lib/VeDirectFrameHandler/VeDirectShuntController.cpp +++ b/lib/VeDirectFrameHandler/VeDirectShuntController.cpp @@ -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) { - veFrame = _tmpFrame; - _tmpFrame = {}; - _lastUpdate = millis(); - } + if (_tmpFrame.PID == 0) { return; } + + veFrame = _tmpFrame; + _tmpFrame = {}; + _lastUpdate = millis(); } diff --git a/lib/VeDirectFrameHandler/VeDirectShuntController.h b/lib/VeDirectFrameHandler/VeDirectShuntController.h index 28ffd071..115af35b 100644 --- a/lib/VeDirectFrameHandler/VeDirectShuntController.h +++ b/lib/VeDirectFrameHandler/VeDirectShuntController.h @@ -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 }; diff --git a/src/BatteryStats.cpp b/src/BatteryStats.cpp index 67b77ba9..b1f616b1 100644 --- a/src/BatteryStats.cpp +++ b/src/BatteryStats.cpp @@ -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; diff --git a/src/MqttHandlVedirectHass.cpp b/src/MqttHandlVedirectHass.cpp index 85e392b8..e5a9be5c 100644 --- a/src/MqttHandlVedirectHass.cpp +++ b/src/MqttHandlVedirectHass.cpp @@ -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(" ", "_"); @@ -94,9 +95,9 @@ void MqttHandleVedirectHassClass::publishSensor(const char* caption, const char* String configTopic = "sensor/dtu_victron_" + serial + "/" + sensorId + "/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; } diff --git a/src/MqttHandleVedirect.cpp b/src/MqttHandleVedirect.cpp index 6013fb24..7466fc1a 100644 --- a/src/MqttHandleVedirect.cpp +++ b/src/MqttHandleVedirect.cpp @@ -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 diff --git a/src/PinMapping.cpp b/src/PinMapping.cpp index b680edb3..2cc48906 100644 --- a/src/PinMapping.cpp +++ b/src/PinMapping.cpp @@ -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 diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index c5c3dc86..feb41dd8 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -10,7 +10,7 @@ #include "MqttSettings.h" #include "NetworkSettings.h" #include "Huawei_can.h" -#include +#include #include "MessageOutput.h" #include #include @@ -364,14 +364,12 @@ int32_t PowerLimiterClass::inverterPowerDcToAc(std::shared_ptr */ void PowerLimiterClass::unconditionalSolarPassthrough(std::shared_ptr 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() diff --git a/src/VictronMppt.cpp b/src/VictronMppt.cpp new file mode 100644 index 00000000..b857e0ec --- /dev/null +++ b/src/VictronMppt.cpp @@ -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 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(); + upController->init(rx, tx, &MessageOutput, config.Vedirect_VerboseLogging); + _controllers.push_back(std::move(upController)); +} + +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(); +} + +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; +} + +VeDirectMpptController::spData_t 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 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; +} diff --git a/src/WebApi_vedirect.cpp b/src/WebApi_vedirect.cpp index d92fda0a..1b884265 100644 --- a/src/WebApi_vedirect.cpp +++ b/src/WebApi_vedirect.cpp @@ -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(); Configuration.write(); - VeDirectMppt.setVerboseLogging(config.Vedirect_VerboseLogging); + VictronMppt.init(); retMsg[F("type")] = F("success"); retMsg[F("message")] = F("Settings saved!"); diff --git a/src/WebApi_ws_live.cpp b/src/WebApi_ws_live.cpp index 463f629c..fdca8baa 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -10,7 +10,7 @@ #include "Battery.h" #include "Huawei_can.h" #include "PowerMeter.h" -#include "VeDirectMpptController.h" +#include "VictronMppt.h" #include "defaults.h" #include @@ -191,10 +191,12 @@ 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; const RectifierParameters_t * rp = HuaweiCan.get(); diff --git a/src/WebApi_ws_vedirect_live.cpp b/src/WebApi_ws_vedirect_live.cpp index 5f26cf34..53c81d12 100644 --- a/src/WebApi_ws_vedirect_live.cpp +++ b/src/WebApi_ws_vedirect_live.cpp @@ -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) diff --git a/src/main.cpp b/src/main.cpp index 3851b127..b39845ca 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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(); - yield(); - } + VictronMppt.loop(); + yield(); MqttSettings.loop(); yield(); MqttHandleDtu.loop();