diff --git a/include/BatteryStats.h b/include/BatteryStats.h index 14672aea..bef2e7d6 100644 --- a/include/BatteryStats.h +++ b/include/BatteryStats.h @@ -111,6 +111,7 @@ class VictronSmartShuntStats : public BatteryStats { float _voltage; float _current; float _temperature; + bool _tempPresent; uint8_t _chargeCycles; uint32_t _timeToGo; float _chargedEnergy; diff --git a/include/Configuration.h b/include/Configuration.h index 8cb44caf..55d31acd 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -177,6 +177,7 @@ struct CONFIG_T { uint8_t Battery_JkBmsPollingInterval; bool Huawei_Enabled; + uint32_t Huawei_CAN_Controller_Frequency; bool Huawei_Auto_Power_Enabled; float Huawei_Auto_Power_Voltage_Limit; float Huawei_Auto_Power_Enable_Voltage_Limit; diff --git a/include/Huawei_can.h b/include/Huawei_can.h index 3b063411..eb69c6c5 100644 --- a/include/Huawei_can.h +++ b/include/Huawei_can.h @@ -90,7 +90,8 @@ typedef struct RectifierParameters { class HuaweiCanCommClass { public: - bool init(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, uint8_t huawei_irq, uint8_t huawei_cs); + bool init(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, + uint8_t huawei_irq, uint8_t huawei_cs, uint32_t frequency); void loop(); bool gotNewRxDataFrame(bool clear); uint8_t getErrorCode(bool clear); 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..d64fbb4c --- /dev/null +++ b/include/VictronMppt.h @@ -0,0 +1,48 @@ +// 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; + + // total panel input power of all MPPT charge controllers in Watts + int32_t getPanelPowerWatts() const; + + // sum of total yield of all MPPT charge controllers in kWh + double getYieldTotal() const; + + // sum of today's yield of all MPPT charge controllers in kWh + double getYieldDay() 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/include/defaults.h b/include/defaults.h index c0206741..47bea025 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -137,6 +137,7 @@ #define BATTERY_JKBMS_POLLING_INTERVAL 5 #define HUAWEI_ENABLED false +#define HUAWEI_CAN_CONTROLLER_FREQUENCY 8000000UL #define HUAWEI_AUTO_POWER_VOLTAGE_LIMIT 42.0 #define HUAWEI_AUTO_POWER_ENABLE_VOLTAGE_LIMIT 42.0 #define HUAWEI_AUTO_POWER_LOWER_POWER_LIMIT 150 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..27357a1d 100644 --- a/lib/VeDirectFrameHandler/VeDirectShuntController.cpp +++ b/lib/VeDirectFrameHandler/VeDirectShuntController.cpp @@ -17,12 +17,13 @@ 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); + _tmpFrame.tempPresent = true; } else if (strcmp(name, "P") == 0) { _tmpFrame.P = atoi(value); @@ -96,18 +97,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..9e1a5f13 100644 --- a/lib/VeDirectFrameHandler/VeDirectShuntController.h +++ b/lib/VeDirectFrameHandler/VeDirectShuntController.h @@ -11,6 +11,7 @@ public: struct veShuntStruct : veStruct { int32_t T; // Battery temperature + bool tempPresent = false; // Battery temperature sensor is attached to the shunt int32_t P; // Instantaneous power int32_t CE; // Consumed Amp Hours int32_t SOC; // State-of-charge @@ -41,7 +42,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..912deeab 100644 --- a/src/BatteryStats.cpp +++ b/src/BatteryStats.cpp @@ -208,12 +208,14 @@ 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; _dischargedEnergy = shuntData.H17 / 100; _manufacturer = "Victron " + _modelName; + _temperature = shuntData.T; + _tempPresent = shuntData.tempPresent; // shuntData.AR is a bitfield, so we need to check each bit individually _alarmLowVoltage = shuntData.AR & 1; @@ -235,6 +237,9 @@ void VictronSmartShuntStats::getLiveViewData(JsonVariant& root) const { addLiveViewValue(root, "chargeCycles", _chargeCycles, "", 0); addLiveViewValue(root, "chargedEnergy", _chargedEnergy, "KWh", 1); addLiveViewValue(root, "dischargedEnergy", _dischargedEnergy, "KWh", 1); + if (_tempPresent) { + addLiveViewValue(root, "temperature", _temperature, "°C", 0); + } addLiveViewAlarm(root, "lowVoltage", _alarmLowVoltage); addLiveViewAlarm(root, "highVoltage", _alarmHighVoltage); diff --git a/src/Configuration.cpp b/src/Configuration.cpp index 74a12615..b223738c 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -190,6 +190,7 @@ bool ConfigurationClass::write() JsonObject huawei = doc.createNestedObject("huawei"); huawei["enabled"] = config.Huawei_Enabled; + huawei["can_controller_frequency"] = config.Huawei_CAN_Controller_Frequency; huawei["auto_power_enabled"] = config.Huawei_Auto_Power_Enabled; huawei["voltage_limit"] = config.Huawei_Auto_Power_Voltage_Limit; huawei["enable_voltage_limit"] = config.Huawei_Auto_Power_Enable_Voltage_Limit; @@ -413,6 +414,7 @@ bool ConfigurationClass::read() JsonObject huawei = doc["huawei"]; config.Huawei_Enabled = huawei["enabled"] | HUAWEI_ENABLED; + config.Huawei_CAN_Controller_Frequency = huawei["can_controller_frequency"] | HUAWEI_CAN_CONTROLLER_FREQUENCY; config.Huawei_Auto_Power_Enabled = huawei["auto_power_enabled"] | false; config.Huawei_Auto_Power_Voltage_Limit = huawei["voltage_limit"] | HUAWEI_AUTO_POWER_VOLTAGE_LIMIT; config.Huawei_Auto_Power_Enable_Voltage_Limit = huawei["enable_voltage_limit"] | HUAWEI_AUTO_POWER_ENABLE_VOLTAGE_LIMIT; diff --git a/src/Huawei_can.cpp b/src/Huawei_can.cpp index 5a6341d1..ffc8929e 100644 --- a/src/Huawei_can.cpp +++ b/src/Huawei_can.cpp @@ -30,7 +30,8 @@ void HuaweiCanCommunicationTask(void* parameter) { } } -bool HuaweiCanCommClass::init(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, uint8_t huawei_irq, uint8_t huawei_cs) { +bool HuaweiCanCommClass::init(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, + uint8_t huawei_irq, uint8_t huawei_cs, uint32_t frequency) { SPI = new SPIClass(HSPI); SPI->begin(huawei_clk, huawei_miso, huawei_mosi, huawei_cs); pinMode(huawei_cs, OUTPUT); @@ -39,8 +40,14 @@ bool HuaweiCanCommClass::init(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t pinMode(huawei_irq, INPUT_PULLUP); _huaweiIrq = huawei_irq; + auto mcp_frequency = MCP_8MHZ; + if (16000000UL == frequency) { mcp_frequency = MCP_16MHZ; } + else if (8000000UL != frequency) { + MessageOutput.printf("Huawei CAN: unknown frequency %d Hz, using 8 MHz\r\n", mcp_frequency); + } + _CAN = new MCP_CAN(SPI, huawei_cs); - if (!_CAN->begin(MCP_STDEXT, CAN_125KBPS, MCP_8MHZ) == CAN_OK) { + if (!_CAN->begin(MCP_STDEXT, CAN_125KBPS, mcp_frequency) == CAN_OK) { return false; } @@ -198,7 +205,7 @@ void HuaweiCanClass::init(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huaw return; } - if (!HuaweiCanComm.init(huawei_miso, huawei_mosi, huawei_clk, huawei_irq, huawei_cs)) { + if (!HuaweiCanComm.init(huawei_miso, huawei_mosi, huawei_clk, huawei_irq, huawei_cs, config.Huawei_CAN_Controller_Frequency)) { MessageOutput.println("[HuaweiCanClass::init] Error Initializing Huawei CAN communication..."); return; }; diff --git a/src/MqttHandlVedirectHass.cpp b/src/MqttHandlVedirectHass.cpp index 85e392b8..8db755d2 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; } @@ -67,9 +68,12 @@ void MqttHandleVedirectHassClass::publishConfig() // battery info publishSensor("Battery voltage", NULL, "V", "voltage", "measurement", "V"); publishSensor("Battery current", NULL, "I", "current", "measurement", "A"); - + publishSensor("Battery power (calculated)", NULL, "P", "power", "measurement", "W"); + publishSensor("Battery efficiency (calculated)", NULL, "E", "efficiency", "measurement", "%"); + // panel info publishSensor("Panel voltage", NULL, "VPV", "voltage", "measurement", "V"); + publishSensor("Panel current (calculated)", NULL, "IPV", "current", "measurement", "A"); publishSensor("Panel power", NULL, "PPV", "power", "measurement", "W"); publishSensor("Panel yield total", NULL, "H19", "energy", "total_increasing", "kWh"); publishSensor("Panel yield today", NULL, "H20", "energy", "total", "kWh"); @@ -82,7 +86,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 +98,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 +137,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 +151,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 +176,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; } @@ -186,4 +191,4 @@ void MqttHandleVedirectHassClass::publish(const String& subtopic, const String& String topic = Configuration.get().Mqtt_Hass_Topic; topic += subtopic; MqttSettings.publishGeneric(topic.c_str(), payload.c_str(), Configuration.get().Mqtt_Hass_Retain); -} \ No newline at end of file +} 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/PowerMeter.cpp b/src/PowerMeter.cpp index 4598c85a..a7b1fee8 100644 --- a/src/PowerMeter.cpp +++ b/src/PowerMeter.cpp @@ -134,14 +134,17 @@ void PowerMeterClass::loop() CONFIG_T const& config = Configuration.get(); _verboseLogging = config.PowerMeter_VerboseLogging; - if (config.PowerMeter_Enabled && config.PowerMeter_Source == SOURCE_SML) { + if (!config.PowerMeter_Enabled) { return; } + + if (config.PowerMeter_Source == SOURCE_SML) { if (!smlReadLoop()) { return; + } else { + _lastPowerMeterUpdate = millis(); } } - if (!config.PowerMeter_Enabled - || (millis() - _lastPowerMeterCheck) < (config.PowerMeter_Interval * 1000)) { + if ((millis() - _lastPowerMeterCheck) < (config.PowerMeter_Interval * 1000)) { return; } diff --git a/src/VictronMppt.cpp b/src/VictronMppt.cpp new file mode 100644 index 00000000..609f8bf0 --- /dev/null +++ b/src/VictronMppt.cpp @@ -0,0 +1,129 @@ +// 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 std::make_shared(); + } + + return _controllers[idx]->getData(); +} + +int32_t VictronMpptClass::getPowerOutputWatts() const +{ + int32_t sum = 0; + + for (const auto& upController : _controllers) { + sum += upController->getData()->P; + } + + return sum; +} + +int32_t VictronMpptClass::getPanelPowerWatts() const +{ + int32_t sum = 0; + + for (const auto& upController : _controllers) { + sum += upController->getData()->PPV; + } + + return sum; +} + +double VictronMpptClass::getYieldTotal() const +{ + double sum = 0; + + for (const auto& upController : _controllers) { + sum += upController->getData()->H19; + } + + return sum; +} + +double VictronMpptClass::getYieldDay() const +{ + double sum = 0; + + for (const auto& upController : _controllers) { + sum += upController->getData()->H20; + } + + return sum; +} diff --git a/src/WebApi_Huawei.cpp b/src/WebApi_Huawei.cpp index f8f75e23..2a7689ae 100644 --- a/src/WebApi_Huawei.cpp +++ b/src/WebApi_Huawei.cpp @@ -190,6 +190,7 @@ void WebApiHuaweiClass::onAdminGet(AsyncWebServerRequest* request) const CONFIG_T& config = Configuration.get(); root[F("enabled")] = config.Huawei_Enabled; + root[F("can_controller_frequency")] = config.Huawei_CAN_Controller_Frequency; root[F("auto_power_enabled")] = config.Huawei_Auto_Power_Enabled; root[F("voltage_limit")] = static_cast(config.Huawei_Auto_Power_Voltage_Limit * 100) / 100.0; root[F("enable_voltage_limit")] = static_cast(config.Huawei_Auto_Power_Enable_Voltage_Limit * 100) / 100.0; @@ -240,6 +241,7 @@ void WebApiHuaweiClass::onAdminPost(AsyncWebServerRequest* request) } if (!(root.containsKey("enabled")) || + !(root.containsKey("can_controller_frequency")) || !(root.containsKey("auto_power_enabled")) || !(root.containsKey("voltage_limit")) || !(root.containsKey("lower_power_limit")) || @@ -253,6 +255,7 @@ void WebApiHuaweiClass::onAdminPost(AsyncWebServerRequest* request) CONFIG_T& config = Configuration.get(); config.Huawei_Enabled = root[F("enabled")].as(); + config.Huawei_CAN_Controller_Frequency = root[F("can_controller_frequency")].as(); config.Huawei_Auto_Power_Enabled = root[F("auto_power_enabled")].as(); config.Huawei_Auto_Power_Voltage_Limit = root[F("voltage_limit")].as(); config.Huawei_Auto_Power_Enable_Voltage_Limit = root[F("enable_voltage_limit")].as(); @@ -267,6 +270,14 @@ void WebApiHuaweiClass::onAdminPost(AsyncWebServerRequest* request) response->setLength(); request->send(response); + // TODO(schlimmchen): HuaweiCan has no real concept of the fact that the + // config might change. at least not regarding CAN parameters. until that + // changes, the ESP must restart for configuration changes to take effect. + yield(); + delay(1000); + yield(); + ESP.restart(); + const PinMapping_t& pin = PinMapping.get(); // Properly turn this on if (config.Huawei_Enabled) { 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..ffbf6b20 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 @@ -67,20 +67,14 @@ void WebApiWsLiveClass::loop() try { std::lock_guard lock(_mutex); - DynamicJsonDocument root(4096 * INV_MAX_COUNT); + DynamicJsonDocument root(4200 * INV_MAX_COUNT); // TODO(helge) check if this calculation is correct JsonVariant var = root; generateJsonResponse(var); - + String buffer; - // free JsonDocument as soon as possible - { - DynamicJsonDocument root(4096 * INV_MAX_COUNT); // TODO(helge) check if this calculation is correct - JsonVariant var = root; - generateJsonResponse(var); - serializeJson(root, buffer); - } - if (buffer) { + serializeJson(root, buffer); + if (Configuration.get().Security_AllowReadonly) { _ws.setAuthentication("", ""); } else { @@ -191,10 +185,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); - + + addTotalField(totalVeObj, "Power", VictronMppt.getPanelPowerWatts(), "W", 1); + addTotalField(totalVeObj, "YieldDay", VictronMppt.getYieldDay() * 1000, "Wh", 0); + addTotalField(totalVeObj, "YieldTotal", VictronMppt.getYieldTotal(), "kWh", 2); + JsonObject huaweiObj = root.createNestedObject("huawei"); huaweiObj[F("enabled")] = Configuration.get().Huawei_Enabled; const RectifierParameters_t * rp = HuaweiCan.get(); @@ -251,7 +246,7 @@ void WebApiWsLiveClass::onLivedataStatus(AsyncWebServerRequest* request) try { std::lock_guard lock(_mutex); - AsyncJsonResponse* response = new AsyncJsonResponse(false, 4096 * INV_MAX_COUNT); + AsyncJsonResponse* response = new AsyncJsonResponse(false, 4200 * INV_MAX_COUNT); JsonVariant root = response->getRoot(); generateJsonResponse(root); 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(); diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 3ee57952..5c6a2fb6 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -388,7 +388,7 @@ "Cancel": "Abbrechen", "RebootOpenDTU": "OpenDTU neustarten", "RebootQuestion": "Möchten Sie das Gerät wirklich neu starten?", - "RebootHint": "Hinweis: Ein manueller Neustart muss normalerweise nicht durchgeführt werden. OpenDTU führt jeden erforderlichen Neustart (z. B. nach einem Firmware-Update) automatisch durch. Einstellungen werden auch ohne Neustart übernommen. Wenn Sie aufgrund eines Fehlers einen Neustart durchführen müssen, denken Sie bitte daran, diesen unter https://github.com/tbnobody/OpenDTU/issues zu melden." + "RebootHint": "Hinweis: Ein manueller Neustart muss normalerweise nicht durchgeführt werden. OpenDTU führt jeden erforderlichen Neustart (z. B. nach einem Firmware-Update) automatisch durch. Einstellungen werden auch ohne Neustart übernommen. Wenn Sie aufgrund eines Fehlers einen Neustart durchführen müssen, denken Sie bitte daran, diesen unter Github zu melden." }, "dtuadmin": { "DtuSettings": "DTU-Einstellungen", @@ -701,11 +701,11 @@ "ProjectOriginBody3": "Die Software wurde nach bestem Wissen und Gewissen entwickelt. Dennoch kann keine Haftung für eine Fehlfunktion oder einen Garantieverlust des Wechselrichters übernommen werden.", "ProjectOriginBody4": "OpenDTU ist frei verfügbar. Wenn Sie Geld für die Software bezahlt haben, wurden Sie wahrscheinlich abgezockt.", "NewsUpdates": "Neuigkeiten und Updates", - "NewsUpdatesBody": "Neue Updates sind auf Github zu finden: https://github.com/tbnobody/OpenDTU", + "NewsUpdatesBody": "Neue Updates sind auf Github zu finden.", "ErrorReporting": "Fehlerberichte", - "ErrorReportingBody": "Bitte melden Sie Probleme über die Ticketverwaltung von Github.", + "ErrorReportingBody": "Bitte melden Sie Probleme über die Ticketverwaltung von Github.", "Discussion": "Diskussion", - "DiscussionBody": "Diskutieren Sie mit uns auf Discord oder Github" + "DiscussionBody": "Diskutieren Sie mit uns auf Discord oder Github." }, "hints": { "RadioProblem": "Es konnte keine Verbindung zu einem der konfigurierten Funkmodule hergestellt werden. Bitte überprüfen Sie die Verdrahtung.", @@ -777,6 +777,7 @@ "ChargerSettings": "AC Ladegerät Einstellungen", "Configuration": "AC Ladegerät Konfiguration", "EnableHuawei": "Huawei R4850G2 an CAN Bus Interface aktiv", + "CanControllerFrequency": "Frequenz des Quarzes am CAN Controller", "EnableAutoPower": "Automatische Leistungssteuerung", "Limits": "Limits", "VoltageLimit": "Ladespannungslimit", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 71a7d63b..0d35def5 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -390,7 +390,7 @@ "Cancel": "Cancel", "RebootOpenDTU": "Reboot OpenDTU", "RebootQuestion": "Do you really want to reboot the device?", - "RebootHint": "Note: A manual reboot does not normally have to be performed. OpenDTU performs any required reboot (e.g. after a firmware update) automatically. Settings are also adopted without rebooting. If you need to reboot due to an error, please consider reporting it at https://github.com/tbnobody/OpenDTU/issues." + "RebootHint": "Note: A manual reboot does not normally have to be performed. OpenDTU performs any required reboot (e.g. after a firmware update) automatically. Settings are also adopted without rebooting. If you need to reboot due to an error, please consider reporting it at Github." }, "dtuadmin": { "DtuSettings": "DTU Settings", @@ -710,11 +710,11 @@ "ProjectOriginBody3": "The software was developed to the best of our knowledge and belief. Nevertheless, no liability can be accepted for a malfunction or guarantee loss of the inverter.", "ProjectOriginBody4": "OpenDTU is freely available. If you paid money for the software, you probably got ripped off.", "NewsUpdates": "News & Updates", - "NewsUpdatesBody": "New updates can be found on Github: https://github.com/tbnobody/OpenDTU", + "NewsUpdatesBody": "New updates can be found on Github.", "ErrorReporting": "Error Reporting", - "ErrorReportingBody": "Please report issues using the feature provided by Github", + "ErrorReportingBody": "Please report issues using the feature provided by Github.", "Discussion": "Discussion", - "DiscussionBody": "Discuss with us on Discord or Github" + "DiscussionBody": "Discuss with us on Discord or Github." }, "hints": { "RadioProblem": "Could not connect to a configured radio module. Please check the wiring.", @@ -787,6 +787,7 @@ "ChargerSettings": "AC Charger Settings", "Configuration": "AC Charger Configuration", "EnableHuawei": "Enable Huawei R4850G2 on CAN Bus Interface", + "CanControllerFrequency": "CAN controller quarz frequency", "EnableAutoPower": "Automatic power control", "Limits": "Limits", "VoltageLimit": "Charge Voltage limit", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index 49602252..6eff2da7 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -388,7 +388,7 @@ "Cancel": "Annuler", "RebootOpenDTU": "Redémarrer OpenDTU", "RebootQuestion": "Voulez-vous vraiment redémarrer l'appareil ?", - "RebootHint": "Astuce : Normalement, il n'est pas nécessaire de procéder à un redémarrage manuel. OpenDTU effectue automatiquement tout redémarrage nécessaire (par exemple, après une mise à jour du firmware). Les paramètres sont également adoptés sans redémarrage. Si vous devez redémarrer en raison d'une erreur, veuillez envisager de la signaler à l'adresse suivante https://github.com/tbnobody/OpenDTU/issues." + "RebootHint": "Astuce : Normalement, il n'est pas nécessaire de procéder à un redémarrage manuel. OpenDTU effectue automatiquement tout redémarrage nécessaire (par exemple, après une mise à jour du firmware). Les paramètres sont également adoptés sans redémarrage. Si vous devez redémarrer en raison d'une erreur, veuillez envisager de la signaler à l'adresse suivante Github." }, "dtuadmin": { "DtuSettings": "Paramètres du DTU", @@ -667,11 +667,11 @@ "ProjectOriginBody3": "Le logiciel a été développé au mieux de nos connaissances et de nos convictions. Néanmoins, aucune responsabilité ne peut être acceptée en cas de dysfonctionnement ou de perte de garantie de l'onduleur.", "ProjectOriginBody4": "OpenDTU est disponible gratuitement. Si vous avez payé pour le logiciel, vous avez probablement été arnaqué.", "NewsUpdates": "Actualités et mises à jour", - "NewsUpdatesBody": "Les nouvelles mises à jour peuvent être trouvées sur https://github.com/tbnobody/OpenDTU", + "NewsUpdatesBody": "Les nouvelles mises à jour peuvent être trouvées sur Github.", "ErrorReporting": "Rapport d'erreurs", - "ErrorReportingBody": "Veuillez signaler les problèmes en utilisant la fonction fournie par Github.", + "ErrorReportingBody": "Veuillez signaler les problèmes en utilisant la fonction fournie par Github.", "Discussion": "Discussion", - "DiscussionBody": "Discutez avec nous sur Discord ou sur Github" + "DiscussionBody": "Discutez avec nous sur Discord ou sur Github." }, "hints": { "RadioProblem": "Impossible de se connecter à un module radio configuré.. Veuillez vérifier le câblage.", @@ -743,6 +743,7 @@ "ChargerSettings": "AC Charger Settings", "Configuration": "AC Charger Configuration", "EnableHuawei": "Enable Huawei R4850G2 on CAN Bus Interface", + "CanControllerFrequency": "CAN controller quarz frequency", "EnableAutoPower": "Automatic power control", "Limits": "Limits", "VoltageLimit": "Charge Voltage limit", diff --git a/webapp/src/types/AcChargerConfig.ts b/webapp/src/types/AcChargerConfig.ts index e50dc7aa..2e1b9ca8 100644 --- a/webapp/src/types/AcChargerConfig.ts +++ b/webapp/src/types/AcChargerConfig.ts @@ -1,5 +1,6 @@ export interface AcChargerConfig { enabled: boolean; + can_controller_frequency: number; auto_power_enabled: boolean; voltage_limit: number; enable_voltage_limit: number; diff --git a/webapp/src/views/AcChargerAdminView.vue b/webapp/src/views/AcChargerAdminView.vue index d6473769..09527d58 100644 --- a/webapp/src/views/AcChargerAdminView.vue +++ b/webapp/src/views/AcChargerAdminView.vue @@ -9,6 +9,20 @@ + +
+ +
+ +
+
+