diff --git a/include/BatteryStats.h b/include/BatteryStats.h index 6dc816a8c..14672aeaa 100644 --- a/include/BatteryStats.h +++ b/include/BatteryStats.h @@ -6,6 +6,7 @@ #include "AsyncJson.h" #include "Arduino.h" #include "JkBmsDataPoints.h" +#include "VeDirectShuntController.h" // mandatory interface for all kinds of batteries class BatteryStats { @@ -98,3 +99,27 @@ class JkBmsBatteryStats : public BatteryStats { mutable uint32_t _lastMqttPublish = 0; mutable uint32_t _lastFullMqttPublish = 0; }; + +class VictronSmartShuntStats : public BatteryStats { + public: + void getLiveViewData(JsonVariant& root) const final; + void mqttPublish() const final; + + void updateFrom(VeDirectShuntController::veShuntStruct const& shuntData); + + private: + float _voltage; + float _current; + float _temperature; + uint8_t _chargeCycles; + uint32_t _timeToGo; + float _chargedEnergy; + float _dischargedEnergy; + String _modelName; + + bool _alarmLowVoltage; + bool _alarmHighVoltage; + bool _alarmLowSOC; + bool _alarmLowTemperature; + bool _alarmHighTemperature; +}; diff --git a/include/MqttHandleVedirect.h b/include/MqttHandleVedirect.h index e1abc90b0..79c04fb28 100644 --- a/include/MqttHandleVedirect.h +++ b/include/MqttHandleVedirect.h @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once -#include "VeDirectFrameHandler.h" +#include "VeDirectMpptController.h" #include "Configuration.h" #include @@ -18,7 +18,8 @@ class MqttHandleVedirectClass { void init(); void loop(); private: - veStruct _kvFrame{}; + + VeDirectMpptController::veMpptStruct _kvFrame{}; // point of time in millis() when updated values will be published uint32_t _nextPublishUpdatesOnly = 0; diff --git a/include/MqttHandleVedirectHass.h b/include/MqttHandleVedirectHass.h index 3a7209912..ca22a73f9 100644 --- a/include/MqttHandleVedirectHass.h +++ b/include/MqttHandleVedirectHass.h @@ -2,7 +2,7 @@ #pragma once #include -#include "VeDirectFrameHandler.h" +#include "VeDirectMpptController.h" class MqttHandleVedirectHassClass { public: diff --git a/include/VictronSmartShunt.h b/include/VictronSmartShunt.h new file mode 100644 index 000000000..c532db6c2 --- /dev/null +++ b/include/VictronSmartShunt.h @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "Battery.h" + +class VictronSmartShunt : public BatteryProvider { +public: + bool init(bool verboseLogging) final; + void deinit() final { } + void loop() final; + std::shared_ptr getStats() const final { return _stats; } + +private: + std::shared_ptr _stats = + std::make_shared(); +}; diff --git a/include/WebApi_ws_vedirect_live.h b/include/WebApi_ws_vedirect_live.h index d17f51c05..13b27d9fa 100644 --- a/include/WebApi_ws_vedirect_live.h +++ b/include/WebApi_ws_vedirect_live.h @@ -3,7 +3,7 @@ #include "ArduinoJson.h" #include -#include +#include class WebApiWsVedirectLiveClass { public: diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp index 52b40e2cb..39ce4fabf 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp +++ b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp @@ -49,9 +49,7 @@ enum States { RECORD_HEX = 6 }; -HardwareSerial VedirectSerial(1); -VeDirectFrameHandler VeDirect; class Silent : public Print { public: @@ -62,16 +60,15 @@ static Silent MessageOutputDummy; VeDirectFrameHandler::VeDirectFrameHandler() : _msgOut(&MessageOutputDummy), + _lastUpdate(0), _state(IDLE), _checksum(0), _textPointer(0), _hexSize(0), _name(""), _value(""), - _tmpFrame(), _debugIn(0), - _lastByteMillis(0), - _lastUpdate(0) + _lastByteMillis(0) { } @@ -81,10 +78,11 @@ void VeDirectFrameHandler::setVerboseLogging(bool verboseLogging) if (!_verboseLogging) { _debugIn = 0; } } -void VeDirectFrameHandler::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging) +void VeDirectFrameHandler::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort) { - VedirectSerial.begin(19200, SERIAL_8N1, rx, tx); - VedirectSerial.flush(); + _vedirectSerial = std::make_unique(hwSerialPort); + _vedirectSerial->begin(19200, SERIAL_8N1, rx, tx); + _vedirectSerial->flush(); _msgOut = msgOut; setVerboseLogging(verboseLogging); } @@ -103,8 +101,8 @@ void VeDirectFrameHandler::dumpDebugBuffer() { void VeDirectFrameHandler::loop() { - while ( VedirectSerial.available()) { - rxData(VedirectSerial.read()); + while ( _vedirectSerial->available()) { + rxData(_vedirectSerial->read()); _lastByteMillis = millis(); } @@ -116,7 +114,6 @@ void VeDirectFrameHandler::loop() if (_verboseLogging) { dumpDebugBuffer(); } _checksum = 0; _state = IDLE; - _tmpFrame = { }; } } @@ -227,93 +224,25 @@ 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) { +void VeDirectFrameHandler::textRxEvent(char * name, char * value, veStruct& frame) { if (strcmp(name, "PID") == 0) { - _tmpFrame.PID = strtol(value, nullptr, 0); + frame.PID = strtol(value, nullptr, 0); } else if (strcmp(name, "SER") == 0) { - strcpy(_tmpFrame.SER, value); + strcpy(frame.SER, value); } else if (strcmp(name, "FW") == 0) { - strcpy(_tmpFrame.FW, value); - } - else if (strcmp(name, "LOAD") == 0) { - if (strcmp(value, "ON") == 0) - _tmpFrame.LOAD = true; - else - _tmpFrame.LOAD = false; - } - else if (strcmp(name, "CS") == 0) { - _tmpFrame.CS = atoi(value); - } - else if (strcmp(name, "ERR") == 0) { - _tmpFrame.ERR = atoi(value); - } - else if (strcmp(name, "OR") == 0) { - _tmpFrame.OR = strtol(value, nullptr, 0); - } - else if (strcmp(name, "MPPT") == 0) { - _tmpFrame.MPPT = atoi(value); - } - else if (strcmp(name, "HSDS") == 0) { - _tmpFrame.HSDS = atoi(value); + strcpy(frame.FW, value); } else if (strcmp(name, "V") == 0) { - _tmpFrame.V = round(atof(value) / 10.0) / 100.0; + frame.V = round(atof(value) / 10.0) / 100.0; } else if (strcmp(name, "I") == 0) { - _tmpFrame.I = round(atof(value) / 10.0) / 100.0; - } - else if (strcmp(name, "VPV") == 0) { - _tmpFrame.VPV = round(atof(value) / 10.0) / 100.0; - } - else if (strcmp(name, "PPV") == 0) { - _tmpFrame.PPV = atoi(value); - } - else if (strcmp(name, "H19") == 0) { - _tmpFrame.H19 = atof(value) / 100.0; - } - else if (strcmp(name, "H20") == 0) { - _tmpFrame.H20 = atof(value) / 100.0; - } - else if (strcmp(name, "H21") == 0) { - _tmpFrame.H21 = atoi(value); - } - else if (strcmp(name, "H22") == 0) { - _tmpFrame.H22 = atof(value) / 100.0; - } - else if (strcmp(name, "H23") == 0) { - _tmpFrame.H23 = atoi(value); + frame.I = round(atof(value) / 10.0) / 100.0; } } -/* - * 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. - */ -void VeDirectFrameHandler::frameEndEvent(bool valid) { - if ( valid ) { - _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; - _lastUpdate = millis(); - } - _tmpFrame = {}; -} /* * hexRxEvent @@ -340,11 +269,11 @@ int VeDirectFrameHandler::hexRxEvent(uint8_t inbyte) { return ret; } -bool VeDirectFrameHandler::isDataValid() { +bool VeDirectFrameHandler::isDataValid(veStruct frame) { if (_lastUpdate == 0) { return false; } - if (strlen(veFrame.SER) == 0) { + if (strlen(frame.SER) == 0) { return false; } return true; @@ -574,54 +503,35 @@ String VeDirectFrameHandler::getPidAsString(uint16_t pid) case 0XA116: strPID = "SmartSolar MPPT VE.Can 250|85 rev2"; break; - default: - strPID = pid; - } - return strPID; -} - -/* - * getCsAsString - * This function returns the state of operations (CS) as readable text. - */ -String VeDirectFrameHandler::getCsAsString(uint8_t cs) -{ - String strCS =""; - - switch(cs) { - case 0: - strCS = "OFF"; - break; - case 2: - strCS = "Fault"; - break; - case 3: - strCS = "Bulk"; + case 0xA381: + strPID = "BMV-712 Smart"; break; - case 4: - strCS = "Absorbtion"; + case 0xA382: + strPID = "BMV-710H Smart"; break; - case 5: - strCS = "Float"; + case 0xA383: + strPID = "BMV-712 Smart Rev2"; break; - case 7: - strCS = "Equalize (manual)"; + case 0xA389: + strPID = "SmartShunt 500A/50mV"; break; - case 245: - strCS = "Starting-up"; + case 0xA38A: + strPID = "SmartShunt 1000A/50mV"; break; - case 247: - strCS = "Auto equalize / Recondition"; + case 0xA38B: + strPID = "SmartShunt 2000A/50mV"; break; - case 252: - strCS = "External Control"; + case 0xA3F0: + strPID = "SmartShunt 2000A/50mV" ; break; default: - strCS = cs; + strPID = pid; } - return strCS; + return strPID; } + + /* * getErrAsString * This function returns error state (ERR) as readable text. @@ -696,72 +606,3 @@ String VeDirectFrameHandler::getErrAsString(uint8_t err) } return strERR; } - -/* - * getOrAsString - * This function returns the off reason (OR) as readable text. - */ -String VeDirectFrameHandler::getOrAsString(uint32_t offReason) -{ - String strOR =""; - - 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; -} - -/* - * getMpptAsString - * This function returns the state of MPPT (MPPT) as readable text. - */ -String VeDirectFrameHandler::getMpptAsString(uint8_t mppt) -{ - String strMPPT =""; - - 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; -} diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHandler.h b/lib/VeDirectFrameHandler/VeDirectFrameHandler.h index 6a43bb64b..2cb554875 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHandler.h +++ b/lib/VeDirectFrameHandler/VeDirectFrameHandler.h @@ -13,94 +13,48 @@ #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; // product id + uint16_t PID = 0; // product id char SER[VE_MAX_VALUE_LEN]; // serial number char FW[VE_MAX_VALUE_LEN]; // firmware release number - bool LOAD; // virtual load output state (on if battery voltage reaches upper limit, off if battery reaches lower limit) - uint8_t CS; // current state of operation e. g. OFF or Bulk - uint8_t ERR; // error code - uint32_t OR; // off reason - uint8_t MPPT; // state of MPP tracker - uint32_t HSDS; // day sequence number 1...365 - int32_t P; // battery output power in W (calculated) - double V; // battery voltage in V - double I; // battery current in A - double E; // efficiency in percent (calculated, moving average) - int32_t PPV; // panel power in W - double VPV; // panel voltage in V - double IPV; // panel current in A (calculated) - double H19; // yield total kWh - double H20; // yield today kWh - int32_t H21; // maximum power today W - double H22; // yield yesterday kWh - int32_t H23; // maximum power yesterday W + 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; -template -class MovingAverage { -public: - MovingAverage() - : _sum(0) - , _index(0) - , _count(0) { } - - void addNumber(T num) { - if (_count < WINDOW_SIZE) { - _count++; - } else { - _sum -= _window[_index]; - } - - _window[_index] = num; - _sum += num; - _index = (_index + 1) % WINDOW_SIZE; - } - - double getAverage() const { - if (_count == 0) { return 0.0; } - return static_cast(_sum) / _count; - } - -private: - std::array _window; - T _sum; - size_t _index; - size_t _count; -}; - class VeDirectFrameHandler { - public: - VeDirectFrameHandler(); void setVerboseLogging(bool verboseLogging); - void init(int8_t rx, int8_t tx, Print* msgOut, 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(); // return true if data valid and not outdated + bool isDataValid(veStruct frame); // return true if data valid and not outdated String getPidAsString(uint16_t pid); // product id as string - String getCsAsString(uint8_t cs); // current state as string String getErrAsString(uint8_t err); // errer state as string - String getOrAsString(uint32_t offReason); // off reason as string - String getMpptAsString(uint8_t mppt); // state of mppt as string - veStruct veFrame{}; // public struct for received name and value pairs +protected: + void textRxEvent(char *, char *, veStruct& ); + + bool _verboseLogging; + Print* _msgOut; + uint32_t _lastUpdate; private: void setLastUpdate(); // set timestampt after successful frame read void dumpDebugBuffer(); void rxData(uint8_t inbyte); // byte of serial data - void textRxEvent(char *, char *); - void frameEndEvent(bool); // copy temp struct to public struct + virtual void textRxEvent(char *, char *) = 0; + virtual void frameEndEvent(bool) = 0; // copy temp struct to public struct int hexRxEvent(uint8_t); - Print* _msgOut; - bool _verboseLogging; + std::unique_ptr _vedirectSerial; int _state; // current state int _prevState; // previous state uint8_t _checksum; // checksum value @@ -108,13 +62,7 @@ class VeDirectFrameHandler { int _hexSize; // length of hex buffer char _name[VE_MAX_VALUE_LEN]; // buffer for the field name char _value[VE_MAX_VALUE_LEN]; // buffer for the field value - veStruct _tmpFrame{}; // private struct for received name and value pairs - MovingAverage _efficiency; std::array _debugBuffer; unsigned _debugIn; uint32_t _lastByteMillis; - uint32_t _lastUpdate; }; - -extern VeDirectFrameHandler VeDirect; - diff --git a/lib/VeDirectFrameHandler/VeDirectMpptController.cpp b/lib/VeDirectFrameHandler/VeDirectMpptController.cpp new file mode 100644 index 000000000..0f8246d7e --- /dev/null +++ b/lib/VeDirectFrameHandler/VeDirectMpptController.cpp @@ -0,0 +1,203 @@ +#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); + if (_verboseLogging) { _msgOut->println("Finished init MPPTController"); } +} + +bool VeDirectMpptController::isDataValid() { + return VeDirectFrameHandler::isDataValid(veFrame); +} + +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); + if (strcmp(name, "LOAD") == 0) { + if (strcmp(value, "ON") == 0) + _tmpFrame.LOAD = true; + else + _tmpFrame.LOAD = false; + } + else if (strcmp(name, "CS") == 0) { + _tmpFrame.CS = atoi(value); + } + else if (strcmp(name, "ERR") == 0) { + _tmpFrame.ERR = atoi(value); + } + else if (strcmp(name, "OR") == 0) { + _tmpFrame.OR = strtol(value, nullptr, 0); + } + else if (strcmp(name, "MPPT") == 0) { + _tmpFrame.MPPT = atoi(value); + } + else if (strcmp(name, "HSDS") == 0) { + _tmpFrame.HSDS = atoi(value); + } + else if (strcmp(name, "VPV") == 0) { + _tmpFrame.VPV = round(atof(value) / 10.0) / 100.0; + } + else if (strcmp(name, "PPV") == 0) { + _tmpFrame.PPV = atoi(value); + } + else if (strcmp(name, "H19") == 0) { + _tmpFrame.H19 = atof(value) / 100.0; + } + else if (strcmp(name, "H20") == 0) { + _tmpFrame.H20 = atof(value) / 100.0; + } + else if (strcmp(name, "H21") == 0) { + _tmpFrame.H21 = atoi(value); + } + else if (strcmp(name, "H22") == 0) { + _tmpFrame.H22 = atof(value) / 100.0; + } + else if (strcmp(name, "H23") == 0) { + _tmpFrame.H23 = atoi(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. + */ +void VeDirectMpptController::frameEndEvent(bool valid) { + if (valid) { + _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(); + } +} + +/* + * getCsAsString + * This function returns the state of operations (CS) as readable text. + */ +String VeDirectMpptController::getCsAsString(uint8_t cs) +{ + String strCS =""; + + 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; +} + +/* + * getMpptAsString + * This function returns the state of MPPT (MPPT) as readable text. + */ +String VeDirectMpptController::getMpptAsString(uint8_t mppt) +{ + String strMPPT =""; + + 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; +} + +/* + * getOrAsString + * This function returns the off reason (OR) as readable text. + */ +String VeDirectMpptController::getOrAsString(uint32_t offReason) +{ + String strOR =""; + + 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; +} diff --git a/lib/VeDirectFrameHandler/VeDirectMpptController.h b/lib/VeDirectFrameHandler/VeDirectMpptController.h new file mode 100644 index 000000000..789454298 --- /dev/null +++ b/lib/VeDirectFrameHandler/VeDirectMpptController.h @@ -0,0 +1,74 @@ +#pragma once + +#include +#include "VeDirectFrameHandler.h" + +template +class MovingAverage { +public: + MovingAverage() + : _sum(0) + , _index(0) + , _count(0) { } + + void addNumber(T num) { + if (_count < WINDOW_SIZE) { + _count++; + } else { + _sum -= _window[_index]; + } + + _window[_index] = num; + _sum += num; + _index = (_index + 1) % WINDOW_SIZE; + } + + double getAverage() const { + if (_count == 0) { return 0.0; } + return static_cast(_sum) / _count; + } + +private: + std::array _window; + T _sum; + size_t _index; + size_t _count; +}; + +class VeDirectMpptController : public VeDirectFrameHandler { +public: + VeDirectMpptController(); + + 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 + + struct veMpptStruct : veStruct { + uint8_t MPPT; // state of MPP tracker + int32_t PPV; // panel power in W + 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) + uint8_t CS; // current state of operation e. g. OFF or Bulk + uint8_t ERR; // error code + uint32_t OR; // off reason + uint32_t HSDS; // day sequence number 1...365 + double H19; // yield total kWh + double H20; // yield today kWh + int32_t H21; // maximum power today W + double H22; // yield yesterday kWh + int32_t H23; // maximum power yesterday W + }; + + veMpptStruct veFrame{}; + +private: + void textRxEvent(char * name, char * value) final; + void frameEndEvent(bool) final; // copy temp struct to public struct + 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 new file mode 100644 index 000000000..249472c54 --- /dev/null +++ b/lib/VeDirectFrameHandler/VeDirectShuntController.cpp @@ -0,0 +1,113 @@ +#include +#include "VeDirectShuntController.h" + +VeDirectShuntController VeDirectShunt; + +VeDirectShuntController::VeDirectShuntController() +{ +} + +void VeDirectShuntController::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging) +{ + VeDirectFrameHandler::init(rx, tx, msgOut, verboseLogging, 2); + if (_verboseLogging) { + _msgOut->println("Finished init ShuntController"); + } +} + +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 (strcmp(name, "T") == 0) { + _tmpFrame.T = atoi(value); + } + else if (strcmp(name, "P") == 0) { + _tmpFrame.P = atoi(value); + } + else if (strcmp(name, "CE") == 0) { + _tmpFrame.CE = atoi(value); + } + else if (strcmp(name, "SOC") == 0) { + _tmpFrame.SOC = atoi(value); + } + else if (strcmp(name, "TTG") == 0) { + _tmpFrame.TTG = atoi(value); + } + else if (strcmp(name, "ALARM") == 0) { + _tmpFrame.ALARM = (strcmp(value, "ON") == 0); + } + else if (strcmp(name, "H1") == 0) { + _tmpFrame.H1 = atoi(value); + } + else if (strcmp(name, "H2") == 0) { + _tmpFrame.H2 = atoi(value); + } + else if (strcmp(name, "H3") == 0) { + _tmpFrame.H3 = atoi(value); + } + else if (strcmp(name, "H4") == 0) { + _tmpFrame.H4 = atoi(value); + } + else if (strcmp(name, "H5") == 0) { + _tmpFrame.H5 = atoi(value); + } + else if (strcmp(name, "H6") == 0) { + _tmpFrame.H6 = atoi(value); + } + else if (strcmp(name, "H7") == 0) { + _tmpFrame.H7 = atoi(value); + } + else if (strcmp(name, "H8") == 0) { + _tmpFrame.H8 = atoi(value); + } + else if (strcmp(name, "H9") == 0) { + _tmpFrame.H9 = atoi(value); + } + else if (strcmp(name, "H10") == 0) { + _tmpFrame.H10 = atoi(value); + } + else if (strcmp(name, "H11") == 0) { + _tmpFrame.H11 = atoi(value); + } + else if (strcmp(name, "H12") == 0) { + _tmpFrame.H12 = atoi(value); + } + else if (strcmp(name, "H13") == 0) { + _tmpFrame.H13 = atoi(value); + } + else if (strcmp(name, "H14") == 0) { + _tmpFrame.H14 = atoi(value); + } + else if (strcmp(name, "H15") == 0) { + _tmpFrame.H15 = atoi(value); + } + else if (strcmp(name, "H16") == 0) { + _tmpFrame.H16 = atoi(value); + } + else if (strcmp(name, "H17") == 0) { + _tmpFrame.H17 = atoi(value); + } + else if (strcmp(name, "H18") == 0) { + _tmpFrame.H18 = atoi(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. + */ +void VeDirectShuntController::frameEndEvent(bool valid) { + // 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(); + } +} diff --git a/lib/VeDirectFrameHandler/VeDirectShuntController.h b/lib/VeDirectFrameHandler/VeDirectShuntController.h new file mode 100644 index 000000000..28ffd0718 --- /dev/null +++ b/lib/VeDirectFrameHandler/VeDirectShuntController.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include "VeDirectFrameHandler.h" + +class VeDirectShuntController : public VeDirectFrameHandler { +public: + VeDirectShuntController(); + + void init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging); + + struct veShuntStruct : veStruct { + int32_t T; // Battery temperature + int32_t P; // Instantaneous power + int32_t CE; // Consumed Amp Hours + int32_t SOC; // State-of-charge + uint32_t TTG; // Time-to-go + bool ALARM; // Alarm condition active + uint32_t AR; // Alarm Reason + int32_t H1; // Depth of the deepest discharge + int32_t H2; // Depth of the last discharge + int32_t H3; // Depth of the average discharge + int32_t H4; // Number of charge cycles + int32_t H5; // Number of full discharges + int32_t H6; // Cumulative Amp Hours drawn + int32_t H7; // Minimum main (battery) voltage + int32_t H8; // Maximum main (battery) voltage + int32_t H9; // Number of seconds since last full charge + int32_t H10; // Number of automatic synchronizations + int32_t H11; // Number of low main voltage alarms + int32_t H12; // Number of high main voltage alarms + int32_t H13; // Number of low auxiliary voltage alarms + int32_t H14; // Number of high auxiliary voltage alarms + int32_t H15; // Minimum auxiliary (battery) voltage + int32_t H16; // Maximum auxiliary (battery) voltage + int32_t H17; // Amount of discharged energy + int32_t H18; // Amount of charged energy + }; + + veShuntStruct veFrame{}; + +private: + void textRxEvent(char * name, char * value) final; + void frameEndEvent(bool) final; // copy temp struct to public struct + veShuntStruct _tmpFrame{}; // private struct for received name and value pairs +}; + +extern VeDirectShuntController VeDirectShunt; diff --git a/src/Battery.cpp b/src/Battery.cpp index 805a5779b..e3af69c56 100644 --- a/src/Battery.cpp +++ b/src/Battery.cpp @@ -4,6 +4,7 @@ #include "MqttSettings.h" #include "PylontechCanReceiver.h" #include "JkBmsController.h" +#include "VictronSmartShunt.h" BatteryClass Battery; @@ -42,6 +43,10 @@ void BatteryClass::init() _upProvider = std::make_unique(); if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; } break; + case 3: + _upProvider = std::make_unique(); + if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; } + break; default: MessageOutput.printf("Unknown battery provider: %d\r\n", config.Battery_Provider); break; diff --git a/src/BatteryStats.cpp b/src/BatteryStats.cpp index 81939e7f7..67b77ba99 100644 --- a/src/BatteryStats.cpp +++ b/src/BatteryStats.cpp @@ -203,3 +203,52 @@ void JkBmsBatteryStats::updateFrom(JkBms::DataPointContainer const& dp) _lastUpdate = millis(); } + +void VictronSmartShuntStats::updateFrom(VeDirectShuntController::veShuntStruct const& shuntData) { + _SoC = shuntData.SOC / 10; + _voltage = shuntData.V; + _current = shuntData.I; + _modelName = VeDirectShunt.getPidAsString(shuntData.PID); + _chargeCycles = shuntData.H4; + _timeToGo = shuntData.TTG / 60; + _chargedEnergy = shuntData.H18 / 100; + _dischargedEnergy = shuntData.H17 / 100; + _manufacturer = "Victron " + _modelName; + + // shuntData.AR is a bitfield, so we need to check each bit individually + _alarmLowVoltage = shuntData.AR & 1; + _alarmHighVoltage = shuntData.AR & 2; + _alarmLowSOC = shuntData.AR & 4; + _alarmLowTemperature = shuntData.AR & 32; + _alarmHighTemperature = shuntData.AR & 64; + + _lastUpdate = VeDirectShunt.getLastUpdate(); + _lastUpdateSoC = VeDirectShunt.getLastUpdate(); +} + +void VictronSmartShuntStats::getLiveViewData(JsonVariant& root) const { + BatteryStats::getLiveViewData(root); + + // values go into the "Status" card of the web application + addLiveViewValue(root, "voltage", _voltage, "V", 2); + addLiveViewValue(root, "current", _current, "A", 1); + addLiveViewValue(root, "chargeCycles", _chargeCycles, "", 0); + addLiveViewValue(root, "chargedEnergy", _chargedEnergy, "KWh", 1); + addLiveViewValue(root, "dischargedEnergy", _dischargedEnergy, "KWh", 1); + + addLiveViewAlarm(root, "lowVoltage", _alarmLowVoltage); + addLiveViewAlarm(root, "highVoltage", _alarmHighVoltage); + addLiveViewAlarm(root, "lowSOC", _alarmLowSOC); + addLiveViewAlarm(root, "lowTemperature", _alarmLowTemperature); + addLiveViewAlarm(root, "highTemperature", _alarmHighTemperature); +} + +void VictronSmartShuntStats::mqttPublish() const { + BatteryStats::mqttPublish(); + + MqttSettings.publish(F("battery/voltage"), String(_voltage)); + MqttSettings.publish(F("battery/current"), String(_current)); + MqttSettings.publish(F("battery/chargeCycles"), String(_chargeCycles)); + MqttSettings.publish(F("battery/chargedEnergy"), String(_chargedEnergy)); + MqttSettings.publish(F("battery/dischargedEnergy"), String(_dischargedEnergy)); +} diff --git a/src/HttpPowerMeter.cpp b/src/HttpPowerMeter.cpp index 6f441705c..3c0e358d9 100644 --- a/src/HttpPowerMeter.cpp +++ b/src/HttpPowerMeter.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include void HttpPowerMeterClass::init() @@ -53,8 +54,6 @@ bool HttpPowerMeterClass::updateValues() bool HttpPowerMeterClass::httpRequest(const char* url, Auth authType, const char* username, const char* password, const char* httpHeader, const char* httpValue, uint32_t timeout, char* response, size_t responseSize, char* error, size_t errorSize) { - - String newUrl = url; String urlProtocol; String urlHostname; String urlUri; @@ -63,17 +62,6 @@ bool HttpPowerMeterClass::httpRequest(const char* url, Auth authType, const char response[0] = '\0'; error[0] = '\0'; - if (authType == Auth::basic) { - newUrl = urlProtocol; - newUrl += "://"; - newUrl += username; - newUrl += ":"; - newUrl += password; - newUrl += "@"; - newUrl += urlHostname; - newUrl += urlUri; - } - // secureWifiClient MUST be created before HTTPClient // see discussion: https://github.com/helgeerbe/OpenDTU-OnBattery/issues/381 std::unique_ptr wifiClient; @@ -87,8 +75,8 @@ bool HttpPowerMeterClass::httpRequest(const char* url, Auth authType, const char } - if (!httpClient.begin(*wifiClient, newUrl)) { - snprintf_P(error, errorSize, "httpClient.begin(%s) failed", newUrl.c_str()); + if (!httpClient.begin(*wifiClient, url)) { + snprintf_P(error, errorSize, "httpClient.begin(%s) failed", url); return false; } prepareRequest(timeout, httpHeader, httpValue); @@ -96,6 +84,13 @@ bool HttpPowerMeterClass::httpRequest(const char* url, Auth authType, const char if (authType == Auth::digest) { const char *headers[1] = {"WWW-Authenticate"}; httpClient.collectHeaders(headers, 1); + } else if (authType == Auth::basic) { + String authString = username; + authString += ":"; + authString += password; + String auth = "Basic "; + auth.concat(base64::encode(authString)); + httpClient.addHeader("Authorization", auth); } int httpCode = httpClient.GET(); @@ -149,8 +144,8 @@ bool HttpPowerMeterClass::httpRequest(const char* url, Auth authType, const char authorization += response; authorization += "\", algorithm=SHA-256"; httpClient.end(); - if (!httpClient.begin(*wifiClient, newUrl)) { - snprintf_P(error, errorSize, "httpClient.begin(%s) for digest auth failed", newUrl.c_str()); + if (!httpClient.begin(*wifiClient, url)) { + snprintf_P(error, errorSize, "httpClient.begin(%s) for digest auth failed", url); return false; } prepareRequest(timeout, httpHeader, httpValue); @@ -170,7 +165,7 @@ bool HttpPowerMeterClass::httpRequest(const char* url, Auth authType, const char snprintf(response, responseSize, responseBody.c_str()); } } else if (httpCode <= 0) { - snprintf_P(error, errorSize, "Error(%s): %s", newUrl.c_str(), httpClient.errorToString(httpCode).c_str()); + snprintf_P(error, errorSize, "Error(%s): %s", url, httpClient.errorToString(httpCode).c_str()); } else if (httpCode != HTTP_CODE_OK) { snprintf_P(error, errorSize, "Bad HTTP code: %d", httpCode); } diff --git a/src/MqttHandlVedirectHass.cpp b/src/MqttHandlVedirectHass.cpp index 687e9ac3b..85e392b86 100644 --- a/src/MqttHandlVedirectHass.cpp +++ b/src/MqttHandlVedirectHass.cpp @@ -50,7 +50,7 @@ void MqttHandleVedirectHassClass::publishConfig() return; } // ensure data is revieved from victron - if (!VeDirect.isDataValid()) { + if (!VeDirectMppt.isDataValid()) { return; } @@ -82,7 +82,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 = VeDirect.veFrame.SER; + String serial = VeDirectMppt.veFrame.SER; String sensorId = caption; sensorId.replace(" ", "_"); @@ -96,7 +96,7 @@ void MqttHandleVedirectHassClass::publishSensor(const char* caption, const char* + "/config"; String statTopic = MqttSettings.getPrefix() + "victron/"; - statTopic.concat(VeDirect.veFrame.SER); + statTopic.concat(VeDirectMppt.veFrame.SER); statTopic.concat("/"); statTopic.concat(subTopic); @@ -133,7 +133,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 = VeDirect.veFrame.SER; + String serial = VeDirectMppt.veFrame.SER; String sensorId = caption; sensorId.replace(" ", "_"); @@ -147,7 +147,7 @@ void MqttHandleVedirectHassClass::publishBinarySensor(const char* caption, const + "/config"; String statTopic = MqttSettings.getPrefix() + "victron/"; - statTopic.concat(VeDirect.veFrame.SER); + statTopic.concat(VeDirectMppt.veFrame.SER); statTopic.concat("/"); statTopic.concat(subTopic); @@ -172,12 +172,12 @@ void MqttHandleVedirectHassClass::publishBinarySensor(const char* caption, const void MqttHandleVedirectHassClass::createDeviceInfo(JsonObject& object) { - String serial = VeDirect.veFrame.SER; + String serial = VeDirectMppt.veFrame.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")] = VeDirect.getPidAsString(VeDirect.veFrame.PID); + object[F("mdl")] = VeDirectMppt.getPidAsString(VeDirectMppt.veFrame.PID); object[F("sw")] = AUTO_GIT_HASH; } diff --git a/src/MqttHandleVedirect.cpp b/src/MqttHandleVedirect.cpp index d1f13259b..4e244ffa9 100644 --- a/src/MqttHandleVedirect.cpp +++ b/src/MqttHandleVedirect.cpp @@ -2,7 +2,7 @@ /* * Copyright (C) 2022 Helge Erbe and others */ -#include "VeDirectFrameHandler.h" +#include "VeDirectMpptController.h" #include "MqttHandleVedirect.h" #include "MqttSettings.h" #include "MessageOutput.h" @@ -29,7 +29,7 @@ void MqttHandleVedirectClass::loop() return; } - if (!VeDirect.isDataValid()) { + if (!VeDirectMppt.isDataValid()) { return; } @@ -52,67 +52,67 @@ void MqttHandleVedirectClass::loop() String value; String topic = "victron/"; - topic.concat(VeDirect.veFrame.SER); + topic.concat(VeDirectMppt.veFrame.SER); topic.concat("/"); - if (_PublishFull || VeDirect.veFrame.PID != _kvFrame.PID) - MqttSettings.publish(topic + "PID", VeDirect.getPidAsString(VeDirect.veFrame.PID)); - if (_PublishFull || strcmp(VeDirect.veFrame.SER, _kvFrame.SER) != 0) - MqttSettings.publish(topic + "SER", VeDirect.veFrame.SER ); - if (_PublishFull || strcmp(VeDirect.veFrame.FW, _kvFrame.FW) != 0) - MqttSettings.publish(topic + "FW", VeDirect.veFrame.FW); - if (_PublishFull || VeDirect.veFrame.LOAD != _kvFrame.LOAD) - MqttSettings.publish(topic + "LOAD", VeDirect.veFrame.LOAD == true ? "ON": "OFF"); - if (_PublishFull || VeDirect.veFrame.CS != _kvFrame.CS) - MqttSettings.publish(topic + "CS", VeDirect.getCsAsString(VeDirect.veFrame.CS)); - if (_PublishFull || VeDirect.veFrame.ERR != _kvFrame.ERR) - MqttSettings.publish(topic + "ERR", VeDirect.getErrAsString(VeDirect.veFrame.ERR)); - if (_PublishFull || VeDirect.veFrame.OR != _kvFrame.OR) - MqttSettings.publish(topic + "OR", VeDirect.getOrAsString(VeDirect.veFrame.OR)); - if (_PublishFull || VeDirect.veFrame.MPPT != _kvFrame.MPPT) - MqttSettings.publish(topic + "MPPT", VeDirect.getMpptAsString(VeDirect.veFrame.MPPT)); - if (_PublishFull || VeDirect.veFrame.HSDS != _kvFrame.HSDS) { - value = VeDirect.veFrame.HSDS; + 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; MqttSettings.publish(topic + "HSDS", value); } - if (_PublishFull || VeDirect.veFrame.V != _kvFrame.V) { - value = VeDirect.veFrame.V; + if (_PublishFull || VeDirectMppt.veFrame.V != _kvFrame.V) { + value = VeDirectMppt.veFrame.V; MqttSettings.publish(topic + "V", value); } - if (_PublishFull || VeDirect.veFrame.I != _kvFrame.I) { - value = VeDirect.veFrame.I; + if (_PublishFull || VeDirectMppt.veFrame.I != _kvFrame.I) { + value = VeDirectMppt.veFrame.I; MqttSettings.publish(topic + "I", value); } - if (_PublishFull || VeDirect.veFrame.VPV != _kvFrame.VPV) { - value = VeDirect.veFrame.VPV; + if (_PublishFull || VeDirectMppt.veFrame.VPV != _kvFrame.VPV) { + value = VeDirectMppt.veFrame.VPV; MqttSettings.publish(topic + "VPV", value); } - if (_PublishFull || VeDirect.veFrame.PPV != _kvFrame.PPV) { - value = VeDirect.veFrame.PPV; + if (_PublishFull || VeDirectMppt.veFrame.PPV != _kvFrame.PPV) { + value = VeDirectMppt.veFrame.PPV; MqttSettings.publish(topic + "PPV", value); } - if (_PublishFull || VeDirect.veFrame.H19 != _kvFrame.H19) { - value = VeDirect.veFrame.H19; + if (_PublishFull || VeDirectMppt.veFrame.H19 != _kvFrame.H19) { + value = VeDirectMppt.veFrame.H19; MqttSettings.publish(topic + "H19", value); } - if (_PublishFull || VeDirect.veFrame.H20 != _kvFrame.H20) { - value = VeDirect.veFrame.H20; + if (_PublishFull || VeDirectMppt.veFrame.H20 != _kvFrame.H20) { + value = VeDirectMppt.veFrame.H20; MqttSettings.publish(topic + "H20", value); } - if (_PublishFull || VeDirect.veFrame.H21 != _kvFrame.H21) { - value = VeDirect.veFrame.H21; + if (_PublishFull || VeDirectMppt.veFrame.H21 != _kvFrame.H21) { + value = VeDirectMppt.veFrame.H21; MqttSettings.publish(topic + "H21", value); } - if (_PublishFull || VeDirect.veFrame.H22 != _kvFrame.H22) { - value = VeDirect.veFrame.H22; + if (_PublishFull || VeDirectMppt.veFrame.H22 != _kvFrame.H22) { + value = VeDirectMppt.veFrame.H22; MqttSettings.publish(topic + "H22", value); } - if (_PublishFull || VeDirect.veFrame.H23 != _kvFrame.H23) { - value = VeDirect.veFrame.H23; + if (_PublishFull || VeDirectMppt.veFrame.H23 != _kvFrame.H23) { + value = VeDirectMppt.veFrame.H23; MqttSettings.publish(topic + "H23", value); } if (!_PublishFull) { - _kvFrame= VeDirect.veFrame; + _kvFrame= VeDirectMppt.veFrame; } // now calculate next points of time to publish diff --git a/src/PinMapping.cpp b/src/PinMapping.cpp index 31cb0f58a..b680edb3e 100644 --- a/src/PinMapping.cpp +++ b/src/PinMapping.cpp @@ -307,8 +307,7 @@ bool PinMappingClass::isValidEthConfig() bool PinMappingClass::isValidVictronConfig() { - return _pinMapping.victron_rx >= 0 - && _pinMapping.victron_tx >= 0; + return _pinMapping.victron_rx >= 0; } bool PinMappingClass::isValidHuaweiConfig() diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index 41d16db9e..25add8d66 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 @@ -366,12 +366,12 @@ void PowerLimiterClass::unconditionalSolarPassthrough(std::shared_ptr= 20; // enough power? + return VeDirectMppt.veFrame.PPV >= 20; // enough power? } @@ -569,7 +569,7 @@ int32_t PowerLimiterClass::getSolarChargePower() return 0; } - return VeDirect.veFrame.V * VeDirect.veFrame.I; + return VeDirectMppt.veFrame.V * VeDirectMppt.veFrame.I; } float PowerLimiterClass::getLoadCorrectedVoltage() diff --git a/src/VictronSmartShunt.cpp b/src/VictronSmartShunt.cpp new file mode 100644 index 000000000..30e75545d --- /dev/null +++ b/src/VictronSmartShunt.cpp @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "VictronSmartShunt.h" +#include "Configuration.h" +#include "PinMapping.h" +#include "MessageOutput.h" + + +bool VictronSmartShunt::init(bool verboseLogging) +{ + MessageOutput.println(F("[VictronSmartShunt] Initialize interface...")); + + const PinMapping_t& pin = PinMapping.get(); + MessageOutput.printf("[VictronSmartShunt] Interface rx = %d, tx = %d\r\n", + pin.battery_rx, pin.battery_tx); + + if (pin.battery_rx < 0) { + MessageOutput.println(F("[VictronSmartShunt] Invalid pin config")); + return false; + } + + auto tx = static_cast(pin.battery_tx); + auto rx = static_cast(pin.battery_rx); + + VeDirectShunt.init(rx, tx, &MessageOutput, verboseLogging); + return true; +} + +void VictronSmartShunt::loop() +{ + VeDirectShunt.loop(); + _stats->updateFrom(VeDirectShunt.veFrame); +} diff --git a/src/WebApi_vedirect.cpp b/src/WebApi_vedirect.cpp index 651e9e406..d92fda0a0 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 "VeDirectFrameHandler.h" +#include "VeDirectMpptController.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(); - VeDirect.setVerboseLogging(config.Vedirect_VerboseLogging); + VeDirectMppt.setVerboseLogging(config.Vedirect_VerboseLogging); 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 42bd41d67..463f629c6 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 "VeDirectFrameHandler.h" +#include "VeDirectMpptController.h" #include "defaults.h" #include @@ -191,9 +191,9 @@ 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", VeDirect.veFrame.PPV, "W", 1); - addTotalField(totalVeObj, "YieldDay", VeDirect.veFrame.H20 * 1000, "Wh", 0); - addTotalField(totalVeObj, "YieldTotal", VeDirect.veFrame.H19, "kWh", 2); + 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); JsonObject huaweiObj = root.createNestedObject("huawei"); huaweiObj[F("enabled")] = Configuration.get().Huawei_Enabled; diff --git a/src/WebApi_ws_vedirect_live.cpp b/src/WebApi_ws_vedirect_live.cpp index f4835e4cb..5f26cf348 100644 --- a/src/WebApi_ws_vedirect_live.cpp +++ b/src/WebApi_ws_vedirect_live.cpp @@ -50,8 +50,8 @@ void WebApiWsVedirectLiveClass::loop() _lastVedirectUpdateCheck = millis(); uint32_t maxTimeStamp = 0; - if (VeDirect.getLastUpdate() > maxTimeStamp) { - maxTimeStamp = VeDirect.getLastUpdate(); + if (VeDirectMppt.getLastUpdate() > maxTimeStamp) { + maxTimeStamp = VeDirectMppt.getLastUpdate(); } // Update on ve.direct change or at least after 10 seconds @@ -88,56 +88,56 @@ void WebApiWsVedirectLiveClass::loop() void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root) { // device info - root["device"]["data_age"] = (millis() - VeDirect.getLastUpdate() ) / 1000; - root["device"]["age_critical"] = !VeDirect.isDataValid(); - root["device"]["PID"] = VeDirect.getPidAsString(VeDirect.veFrame.PID); - root["device"]["SER"] = VeDirect.veFrame.SER; - root["device"]["FW"] = VeDirect.veFrame.FW; - root["device"]["LOAD"] = VeDirect.veFrame.LOAD == true ? "ON" : "OFF"; - root["device"]["CS"] = VeDirect.getCsAsString(VeDirect.veFrame.CS); - root["device"]["ERR"] = VeDirect.getErrAsString(VeDirect.veFrame.ERR); - root["device"]["OR"] = VeDirect.getOrAsString(VeDirect.veFrame.OR); - root["device"]["MPPT"] = VeDirect.getMpptAsString(VeDirect.veFrame.MPPT); - root["device"]["HSDS"]["v"] = VeDirect.veFrame.HSDS; + 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"]["HSDS"]["u"] = "d"; // battery info - root["output"]["P"]["v"] = VeDirect.veFrame.P; + root["output"]["P"]["v"] = VeDirectMppt.veFrame.P; root["output"]["P"]["u"] = "W"; root["output"]["P"]["d"] = 0; - root["output"]["V"]["v"] = VeDirect.veFrame.V; + root["output"]["V"]["v"] = VeDirectMppt.veFrame.V; root["output"]["V"]["u"] = "V"; root["output"]["V"]["d"] = 2; - root["output"]["I"]["v"] = VeDirect.veFrame.I; + root["output"]["I"]["v"] = VeDirectMppt.veFrame.I; root["output"]["I"]["u"] = "A"; root["output"]["I"]["d"] = 2; - root["output"]["E"]["v"] = VeDirect.veFrame.E; + root["output"]["E"]["v"] = VeDirectMppt.veFrame.E; root["output"]["E"]["u"] = "%"; root["output"]["E"]["d"] = 1; // panel info - root["input"]["PPV"]["v"] = VeDirect.veFrame.PPV; + root["input"]["PPV"]["v"] = VeDirectMppt.veFrame.PPV; root["input"]["PPV"]["u"] = "W"; root["input"]["PPV"]["d"] = 0; - root["input"]["VPV"]["v"] = VeDirect.veFrame.VPV; + root["input"]["VPV"]["v"] = VeDirectMppt.veFrame.VPV; root["input"]["VPV"]["u"] = "V"; root["input"]["VPV"]["d"] = 2; - root["input"]["IPV"]["v"] = VeDirect.veFrame.IPV; + root["input"]["IPV"]["v"] = VeDirectMppt.veFrame.IPV; root["input"]["IPV"]["u"] = "A"; root["input"]["IPV"]["d"] = 2; - root["input"]["YieldToday"]["v"] = VeDirect.veFrame.H20; + root["input"]["YieldToday"]["v"] = VeDirectMppt.veFrame.H20; root["input"]["YieldToday"]["u"] = "kWh"; root["input"]["YieldToday"]["d"] = 3; - root["input"]["YieldYesterday"]["v"] = VeDirect.veFrame.H22; + root["input"]["YieldYesterday"]["v"] = VeDirectMppt.veFrame.H22; root["input"]["YieldYesterday"]["u"] = "kWh"; root["input"]["YieldYesterday"]["d"] = 3; - root["input"]["YieldTotal"]["v"] = VeDirect.veFrame.H19; + root["input"]["YieldTotal"]["v"] = VeDirectMppt.veFrame.H19; root["input"]["YieldTotal"]["u"] = "kWh"; root["input"]["YieldTotal"]["d"] = 3; - root["input"]["MaximumPowerToday"]["v"] = VeDirect.veFrame.H21; + root["input"]["MaximumPowerToday"]["v"] = VeDirectMppt.veFrame.H21; root["input"]["MaximumPowerToday"]["u"] = "W"; root["input"]["MaximumPowerToday"]["d"] = 0; - root["input"]["MaximumPowerYesterday"]["v"] = VeDirect.veFrame.H23; + root["input"]["MaximumPowerYesterday"]["v"] = VeDirectMppt.veFrame.H23; root["input"]["MaximumPowerYesterday"]["u"] = "W"; root["input"]["MaximumPowerYesterday"]["d"] = 0; @@ -147,8 +147,8 @@ void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root) root["dpl"]["PLSTATE"] = PowerLimiter.getPowerLimiterState(); root["dpl"]["PLLIMIT"] = PowerLimiter.getLastRequestedPowerLimit(); - if (VeDirect.getLastUpdate() > _newestVedirectTimestamp) { - _newestVedirectTimestamp = VeDirect.getLastUpdate(); + if (VeDirectMppt.getLastUpdate() > _newestVedirectTimestamp) { + _newestVedirectTimestamp = VeDirectMppt.getLastUpdate(); } } diff --git a/src/main.cpp b/src/main.cpp index 50e1437f3..3851b1274 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -8,7 +8,7 @@ #include "InverterSettings.h" #include "Led_Single.h" #include "MessageOutput.h" -#include "VeDirectFrameHandler.h" +#include "VeDirectMpptController.h" #include "Battery.h" #include "Huawei_can.h" #include "MqttHandleDtu.h" @@ -165,7 +165,7 @@ void setup() 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); - VeDirect.init(pin.victron_rx, pin.victron_tx, + VeDirectMppt.init(pin.victron_rx, pin.victron_tx, &MessageOutput, config.Vedirect_VerboseLogging); MessageOutput.println(F("done")); } else { @@ -204,7 +204,7 @@ void loop() yield(); // Vedirect_Enabled is unknown to lib. Therefor check has to be done here if (Configuration.get().Vedirect_Enabled) { - VeDirect.loop(); + VeDirectMppt.loop(); yield(); } MqttSettings.loop(); diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 22d85d10d..3ee579527 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -599,6 +599,7 @@ "Provider": "Datenanbieter", "ProviderPylontechCan": "Pylontech per CAN-Bus", "ProviderJkBmsSerial": "Jikong (JK) BMS per serieller Verbindung", + "ProviderVictron": "Victron SmartShunt per VE.Direct Schnittstelle", "JkBmsConfiguration": "JK BMS Einstellungen", "JkBmsInterface": "Schnittstellentyp", "JkBmsInterfaceUart": "TTL-UART an der MCU", @@ -821,10 +822,14 @@ "underTemperature": "Untertemperatur", "highTemperature": "Hohe Temperatur", "overTemperature": "Übertemperatur", + "lowSOC": "Geringer Ladezustand", "lowVoltage": "Niedrige Spannung", "underVoltage": "Unterspannung", "highVoltage": "Hohe Spannung", "overVoltage": "Überspannung", - "bmsInternal": "BMS intern" + "bmsInternal": "BMS intern", + "chargeCycles": "Ladezyklen", + "chargedEnergy": "Geladene Energie", + "dischargedEnergy": "Entladene Energie" } } diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 3b9d8ded0..71a7d63b5 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -608,6 +608,7 @@ "Provider": "Data Provider", "ProviderPylontechCan": "Pylontech using CAN bus", "ProviderJkBmsSerial": "Jikong (JK) BMS using serial connection", + "ProviderVictron": "Victron SmartShunt using VE.Direct interface", "JkBmsConfiguration": "JK BMS Settings", "JkBmsInterface": "Interface Type", "JkBmsInterfaceUart": "TTL-UART on MCU", @@ -832,9 +833,13 @@ "highTemperature": "High temperature", "overTemperature": "Overtemperature", "lowVoltage": "Low voltage", + "lowSOC": "Low state of charge", "underVoltage": "Undervoltage", "highVoltage": "High voltage", "overVoltage": "Overvoltage", - "bmsInternal": "BMS internal" + "bmsInternal": "BMS internal", + "chargeCycles": "Charge cycles", + "chargedEnergy": "Charged energy", + "dischargedEnergy": "Discharged energy" } } diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index fd4b069ae..496022529 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -514,6 +514,23 @@ "UpdatesOnly": "Publish values to MQTT only when they change", "Save": "@:dtuadmin.Save" }, + "batteryadmin": { + "BatterySettings": "Battery Settings", + "BatteryConfiguration": "General Interface Settings", + "EnableBattery": "Enable Interface", + "VerboseLogging": "@:base.VerboseLogging", + "Provider": "Data Provider", + "ProviderPylontechCan": "Pylontech using CAN bus", + "ProviderJkBmsSerial": "Jikong (JK) BMS using serial connection", + "ProviderVictron": "Victron SmartShunt using VE.Direct interface", + "JkBmsConfiguration": "JK BMS Settings", + "JkBmsInterface": "Interface Type", + "JkBmsInterfaceUart": "TTL-UART on MCU", + "JkBmsInterfaceTransceiver": "RS-485 Transceiver on MCU", + "PollingInterval": "Polling Interval", + "Seconds": "@:dtuadmin.Seconds", + "Save": "@:dtuadmin.Save" + }, "inverteradmin": { "InverterSettings": "Paramètres des onduleurs", "AddInverter": "Ajouter un nouvel onduleur", @@ -769,9 +786,13 @@ "highTemperature": "High temperature", "overTemperature": "Overtemperature", "lowVoltage": "Low voltage", + "lowSOC": "Low state of charge", "underVoltage": "Undervoltage", "highVoltage": "High voltage", "overVoltage": "Overvoltage", - "bmsInternal": "BMS internal" + "bmsInternal": "BMS internal", + "chargeCycles": "Charge cycles", + "chargedEnergy": "Charged energy", + "dischargedEnergy": "Discharged energy" } } diff --git a/webapp/src/views/BatteryAdminView.vue b/webapp/src/views/BatteryAdminView.vue index b1475c164..4cf1e6735 100644 --- a/webapp/src/views/BatteryAdminView.vue +++ b/webapp/src/views/BatteryAdminView.vue @@ -80,6 +80,7 @@ export default defineComponent({ providerTypeList: [ { key: 0, value: 'PylontechCan' }, { key: 1, value: 'JkBmsSerial' }, + { key: 3, value: 'Victron' }, ], jkBmsInterfaceTypeList: [ { key: 0, value: 'Uart' }, diff --git a/webapp_dist/js/app.js.gz b/webapp_dist/js/app.js.gz index f22526180..343f7cae8 100644 Binary files a/webapp_dist/js/app.js.gz and b/webapp_dist/js/app.js.gz differ