Integration of Victron SmartShunt via VE.Direct (#452)

* Move Mppt logic to subclass

* Added Definitions for Shunts and restructering

* First integration of SmartShunt data into Web Interface

* Code cleanup

* VE.Direct: whitespace cleanup

* VE.Direct: manage HardwareSerial in unique_ptr

* VE.Direct: _efficiency is only needed by MPPT

* VE.Direct: keep as many members private as possible

* VE.Direct: use int8_t for pins (as before)

* VictronSmartShunt: _verboseLogging is not used

* VE.Direct: OR (off reason) is MPPT specific

it also applies to Phoenix inverters and Smart BuckBoost, but since
there is no support for those, the code is moved to the MPPT controller.

* Added Shunt alarms to liveview
Changed from double to int for several readings

* Update build.yml to allow manual builds

---------

Co-authored-by: Philipp Sandhaus <philipp.sandhaus@cewe.de>
Co-authored-by: Bernhard Kirchen <schlimmchen@posteo.net>
This commit is contained in:
Philipp Sandhaus 2023-09-22 17:24:57 +02:00 committed by GitHub
parent 160d3f23bd
commit 7142921021
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 749 additions and 361 deletions

View File

@ -14,6 +14,7 @@ on:
paths-ignore: paths-ignore:
- docs/** - docs/**
- '**/*.md' - '**/*.md'
workflow_dispatch:
jobs: jobs:
get_default_envs: get_default_envs:

View File

@ -6,6 +6,7 @@
#include "AsyncJson.h" #include "AsyncJson.h"
#include "Arduino.h" #include "Arduino.h"
#include "JkBmsDataPoints.h" #include "JkBmsDataPoints.h"
#include "VeDirectShuntController.h"
// mandatory interface for all kinds of batteries // mandatory interface for all kinds of batteries
class BatteryStats { class BatteryStats {
@ -98,3 +99,27 @@ class JkBmsBatteryStats : public BatteryStats {
mutable uint32_t _lastMqttPublish = 0; mutable uint32_t _lastMqttPublish = 0;
mutable uint32_t _lastFullMqttPublish = 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;
};

View File

@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#pragma once #pragma once
#include "VeDirectFrameHandler.h" #include "VeDirectMpptController.h"
#include "Configuration.h" #include "Configuration.h"
#include <Arduino.h> #include <Arduino.h>
@ -18,7 +18,8 @@ public:
void init(); void init();
void loop(); void loop();
private: private:
veStruct _kvFrame{};
VeDirectMpptController::veMpptStruct _kvFrame{};
// point of time in millis() when updated values will be published // point of time in millis() when updated values will be published
uint32_t _nextPublishUpdatesOnly = 0; uint32_t _nextPublishUpdatesOnly = 0;

View File

@ -2,7 +2,7 @@
#pragma once #pragma once
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include "VeDirectFrameHandler.h" #include "VeDirectMpptController.h"
class MqttHandleVedirectHassClass { class MqttHandleVedirectHassClass {
public: public:

View File

@ -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<BatteryStats> getStats() const final { return _stats; }
private:
std::shared_ptr<VictronSmartShuntStats> _stats =
std::make_shared<VictronSmartShuntStats>();
};

View File

@ -3,7 +3,7 @@
#include "ArduinoJson.h" #include "ArduinoJson.h"
#include <ESPAsyncWebServer.h> #include <ESPAsyncWebServer.h>
#include <VeDirectFrameHandler.h> #include <VeDirectMpptController.h>
class WebApiWsVedirectLiveClass { class WebApiWsVedirectLiveClass {
public: public:

View File

@ -49,9 +49,7 @@ enum States {
RECORD_HEX = 6 RECORD_HEX = 6
}; };
HardwareSerial VedirectSerial(1);
VeDirectFrameHandler VeDirect;
class Silent : public Print { class Silent : public Print {
public: public:
@ -62,16 +60,15 @@ static Silent MessageOutputDummy;
VeDirectFrameHandler::VeDirectFrameHandler() : VeDirectFrameHandler::VeDirectFrameHandler() :
_msgOut(&MessageOutputDummy), _msgOut(&MessageOutputDummy),
_lastUpdate(0),
_state(IDLE), _state(IDLE),
_checksum(0), _checksum(0),
_textPointer(0), _textPointer(0),
_hexSize(0), _hexSize(0),
_name(""), _name(""),
_value(""), _value(""),
_tmpFrame(),
_debugIn(0), _debugIn(0),
_lastByteMillis(0), _lastByteMillis(0)
_lastUpdate(0)
{ {
} }
@ -81,10 +78,11 @@ void VeDirectFrameHandler::setVerboseLogging(bool verboseLogging)
if (!_verboseLogging) { _debugIn = 0; } 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 = std::make_unique<HardwareSerial>(hwSerialPort);
VedirectSerial.flush(); _vedirectSerial->begin(19200, SERIAL_8N1, rx, tx);
_vedirectSerial->flush();
_msgOut = msgOut; _msgOut = msgOut;
setVerboseLogging(verboseLogging); setVerboseLogging(verboseLogging);
} }
@ -103,8 +101,8 @@ void VeDirectFrameHandler::dumpDebugBuffer() {
void VeDirectFrameHandler::loop() void VeDirectFrameHandler::loop()
{ {
while ( VedirectSerial.available()) { while ( _vedirectSerial->available()) {
rxData(VedirectSerial.read()); rxData(_vedirectSerial->read());
_lastByteMillis = millis(); _lastByteMillis = millis();
} }
@ -116,7 +114,6 @@ void VeDirectFrameHandler::loop()
if (_verboseLogging) { dumpDebugBuffer(); } if (_verboseLogging) { dumpDebugBuffer(); }
_checksum = 0; _checksum = 0;
_state = IDLE; _state = IDLE;
_tmpFrame = { };
} }
} }
@ -227,93 +224,25 @@ void VeDirectFrameHandler::rxData(uint8_t inbyte)
* textRxEvent * textRxEvent
* This function is called every time a new name/value is successfully parsed. It writes the values to the temporary buffer. * 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) { if (strcmp(name, "PID") == 0) {
_tmpFrame.PID = strtol(value, nullptr, 0); frame.PID = strtol(value, nullptr, 0);
} }
else if (strcmp(name, "SER") == 0) { else if (strcmp(name, "SER") == 0) {
strcpy(_tmpFrame.SER, value); strcpy(frame.SER, value);
} }
else if (strcmp(name, "FW") == 0) { else if (strcmp(name, "FW") == 0) {
strcpy(_tmpFrame.FW, value); strcpy(frame.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);
} }
else if (strcmp(name, "V") == 0) { 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) { else if (strcmp(name, "I") == 0) {
_tmpFrame.I = round(atof(value) / 10.0) / 100.0; frame.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);
} }
} }
/*
* 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<double>(_tmpFrame.P * 100) / _tmpFrame.PPV);
_tmpFrame.E = _efficiency.getAverage();
}
veFrame = _tmpFrame;
_lastUpdate = millis();
}
_tmpFrame = {};
}
/* /*
* hexRxEvent * hexRxEvent
@ -340,11 +269,11 @@ int VeDirectFrameHandler::hexRxEvent(uint8_t inbyte) {
return ret; return ret;
} }
bool VeDirectFrameHandler::isDataValid() { bool VeDirectFrameHandler::isDataValid(veStruct frame) {
if (_lastUpdate == 0) { if (_lastUpdate == 0) {
return false; return false;
} }
if (strlen(veFrame.SER) == 0) { if (strlen(frame.SER) == 0) {
return false; return false;
} }
return true; return true;
@ -574,53 +503,34 @@ String VeDirectFrameHandler::getPidAsString(uint16_t pid)
case 0XA116: case 0XA116:
strPID = "SmartSolar MPPT VE.Can 250|85 rev2"; strPID = "SmartSolar MPPT VE.Can 250|85 rev2";
break; 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: default:
strPID = pid; strPID = pid;
} }
return strPID; 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";
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;
}
/* /*
* getErrAsString * getErrAsString
@ -696,72 +606,3 @@ String VeDirectFrameHandler::getErrAsString(uint8_t err)
} }
return strERR; 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;
}

View File

@ -13,94 +13,48 @@
#include <Arduino.h> #include <Arduino.h>
#include <array> #include <array>
#include <memory>
#define VE_MAX_VALUE_LEN 33 // VE.Direct Protocol: max value size is 33 including /0 #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 #define VE_MAX_HEX_LEN 100 // Maximum size of hex frame - max payload 34 byte (=68 char) + safe buffer
typedef struct { typedef struct {
uint16_t PID; // product id uint16_t PID = 0; // product id
char SER[VE_MAX_VALUE_LEN]; // serial number char SER[VE_MAX_VALUE_LEN]; // serial number
char FW[VE_MAX_VALUE_LEN]; // firmware release 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) int32_t P = 0; // battery output power in W (calculated)
uint8_t CS; // current state of operation e. g. OFF or Bulk double V = 0; // battery voltage in V
uint8_t ERR; // error code double I = 0; // battery current in A
uint32_t OR; // off reason double E = 0; // efficiency in percent (calculated, moving average)
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
} veStruct; } veStruct;
template<typename T, size_t WINDOW_SIZE>
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<double>(_sum) / _count;
}
private:
std::array<T, WINDOW_SIZE> _window;
T _sum;
size_t _index;
size_t _count;
};
class VeDirectFrameHandler { class VeDirectFrameHandler {
public: public:
VeDirectFrameHandler(); VeDirectFrameHandler();
void setVerboseLogging(bool verboseLogging); 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 void loop(); // main loop to read ve.direct data
unsigned long getLastUpdate(); // timestamp of last successful frame read 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 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 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: private:
void setLastUpdate(); // set timestampt after successful frame read void setLastUpdate(); // set timestampt after successful frame read
void dumpDebugBuffer(); void dumpDebugBuffer();
void rxData(uint8_t inbyte); // byte of serial data void rxData(uint8_t inbyte); // byte of serial data
void textRxEvent(char *, char *); virtual void textRxEvent(char *, char *) = 0;
void frameEndEvent(bool); // copy temp struct to public struct virtual void frameEndEvent(bool) = 0; // copy temp struct to public struct
int hexRxEvent(uint8_t); int hexRxEvent(uint8_t);
Print* _msgOut; std::unique_ptr<HardwareSerial> _vedirectSerial;
bool _verboseLogging;
int _state; // current state int _state; // current state
int _prevState; // previous state int _prevState; // previous state
uint8_t _checksum; // checksum value uint8_t _checksum; // checksum value
@ -108,13 +62,7 @@ private:
int _hexSize; // length of hex buffer int _hexSize; // length of hex buffer
char _name[VE_MAX_VALUE_LEN]; // buffer for the field name char _name[VE_MAX_VALUE_LEN]; // buffer for the field name
char _value[VE_MAX_VALUE_LEN]; // buffer for the field value char _value[VE_MAX_VALUE_LEN]; // buffer for the field value
veStruct _tmpFrame{}; // private struct for received name and value pairs
MovingAverage<double, 5> _efficiency;
std::array<uint8_t, 512> _debugBuffer; std::array<uint8_t, 512> _debugBuffer;
unsigned _debugIn; unsigned _debugIn;
uint32_t _lastByteMillis; uint32_t _lastByteMillis;
uint32_t _lastUpdate;
}; };
extern VeDirectFrameHandler VeDirect;

View File

@ -0,0 +1,203 @@
#include <Arduino.h>
#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<double>(_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;
}

View File

@ -0,0 +1,74 @@
#pragma once
#include <Arduino.h>
#include "VeDirectFrameHandler.h"
template<typename T, size_t WINDOW_SIZE>
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<double>(_sum) / _count;
}
private:
std::array<T, WINDOW_SIZE> _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<double, 5> _efficiency;
};
extern VeDirectMpptController VeDirectMppt;

View File

@ -0,0 +1,113 @@
#include <Arduino.h>
#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();
}
}

View File

@ -0,0 +1,48 @@
#pragma once
#include <Arduino.h>
#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;

View File

@ -4,6 +4,7 @@
#include "MqttSettings.h" #include "MqttSettings.h"
#include "PylontechCanReceiver.h" #include "PylontechCanReceiver.h"
#include "JkBmsController.h" #include "JkBmsController.h"
#include "VictronSmartShunt.h"
BatteryClass Battery; BatteryClass Battery;
@ -42,6 +43,10 @@ void BatteryClass::init()
_upProvider = std::make_unique<JkBms::Controller>(); _upProvider = std::make_unique<JkBms::Controller>();
if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; } if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; }
break; break;
case 3:
_upProvider = std::make_unique<VictronSmartShunt>();
if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; }
break;
default: default:
MessageOutput.printf("Unknown battery provider: %d\r\n", config.Battery_Provider); MessageOutput.printf("Unknown battery provider: %d\r\n", config.Battery_Provider);
break; break;

View File

@ -203,3 +203,52 @@ void JkBmsBatteryStats::updateFrom(JkBms::DataPointContainer const& dp)
_lastUpdate = millis(); _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));
}

View File

@ -50,7 +50,7 @@ void MqttHandleVedirectHassClass::publishConfig()
return; return;
} }
// ensure data is revieved from victron // ensure data is revieved from victron
if (!VeDirect.isDataValid()) { if (!VeDirectMppt.isDataValid()) {
return; 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 ) 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; String sensorId = caption;
sensorId.replace(" ", "_"); sensorId.replace(" ", "_");
@ -96,7 +96,7 @@ void MqttHandleVedirectHassClass::publishSensor(const char* caption, const char*
+ "/config"; + "/config";
String statTopic = MqttSettings.getPrefix() + "victron/"; String statTopic = MqttSettings.getPrefix() + "victron/";
statTopic.concat(VeDirect.veFrame.SER); statTopic.concat(VeDirectMppt.veFrame.SER);
statTopic.concat("/"); statTopic.concat("/");
statTopic.concat(subTopic); 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) 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; String sensorId = caption;
sensorId.replace(" ", "_"); sensorId.replace(" ", "_");
@ -147,7 +147,7 @@ void MqttHandleVedirectHassClass::publishBinarySensor(const char* caption, const
+ "/config"; + "/config";
String statTopic = MqttSettings.getPrefix() + "victron/"; String statTopic = MqttSettings.getPrefix() + "victron/";
statTopic.concat(VeDirect.veFrame.SER); statTopic.concat(VeDirectMppt.veFrame.SER);
statTopic.concat("/"); statTopic.concat("/");
statTopic.concat(subTopic); statTopic.concat(subTopic);
@ -172,12 +172,12 @@ void MqttHandleVedirectHassClass::publishBinarySensor(const char* caption, const
void MqttHandleVedirectHassClass::createDeviceInfo(JsonObject& object) void MqttHandleVedirectHassClass::createDeviceInfo(JsonObject& object)
{ {
String serial = VeDirect.veFrame.SER; String serial = VeDirectMppt.veFrame.SER;
object[F("name")] = "Victron(" + serial + ")"; object[F("name")] = "Victron(" + serial + ")";
object[F("ids")] = serial; object[F("ids")] = serial;
object[F("cu")] = String(F("http://")) + NetworkSettings.localIP().toString(); object[F("cu")] = String(F("http://")) + NetworkSettings.localIP().toString();
object[F("mf")] = F("OpenDTU"); 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; object[F("sw")] = AUTO_GIT_HASH;
} }

View File

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

View File

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

32
src/VictronSmartShunt.cpp Normal file
View File

@ -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 || pin.battery_tx < 0) {
MessageOutput.println(F("[VictronSmartShunt] Invalid pin config"));
return false;
}
auto tx = static_cast<gpio_num_t>(pin.battery_tx);
auto rx = static_cast<gpio_num_t>(pin.battery_rx);
VeDirectShunt.init(rx, tx, &MessageOutput, verboseLogging);
return true;
}
void VictronSmartShunt::loop()
{
VeDirectShunt.loop();
_stats->updateFrom(VeDirectShunt.veFrame);
}

View File

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

View File

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

View File

@ -50,8 +50,8 @@ void WebApiWsVedirectLiveClass::loop()
_lastVedirectUpdateCheck = millis(); _lastVedirectUpdateCheck = millis();
uint32_t maxTimeStamp = 0; uint32_t maxTimeStamp = 0;
if (VeDirect.getLastUpdate() > maxTimeStamp) { if (VeDirectMppt.getLastUpdate() > maxTimeStamp) {
maxTimeStamp = VeDirect.getLastUpdate(); maxTimeStamp = VeDirectMppt.getLastUpdate();
} }
// Update on ve.direct change or at least after 10 seconds // Update on ve.direct change or at least after 10 seconds
@ -88,56 +88,56 @@ void WebApiWsVedirectLiveClass::loop()
void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root) void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root)
{ {
// device info // device info
root["device"]["data_age"] = (millis() - VeDirect.getLastUpdate() ) / 1000; root["device"]["data_age"] = (millis() - VeDirectMppt.getLastUpdate() ) / 1000;
root["device"]["age_critical"] = !VeDirect.isDataValid(); root["device"]["age_critical"] = !VeDirectMppt.isDataValid();
root["device"]["PID"] = VeDirect.getPidAsString(VeDirect.veFrame.PID); root["device"]["PID"] = VeDirectMppt.getPidAsString(VeDirectMppt.veFrame.PID);
root["device"]["SER"] = VeDirect.veFrame.SER; root["device"]["SER"] = VeDirectMppt.veFrame.SER;
root["device"]["FW"] = VeDirect.veFrame.FW; root["device"]["FW"] = VeDirectMppt.veFrame.FW;
root["device"]["LOAD"] = VeDirect.veFrame.LOAD == true ? "ON" : "OFF"; root["device"]["LOAD"] = VeDirectMppt.veFrame.LOAD == true ? "ON" : "OFF";
root["device"]["CS"] = VeDirect.getCsAsString(VeDirect.veFrame.CS); root["device"]["CS"] = VeDirectMppt.getCsAsString(VeDirectMppt.veFrame.CS);
root["device"]["ERR"] = VeDirect.getErrAsString(VeDirect.veFrame.ERR); root["device"]["ERR"] = VeDirectMppt.getErrAsString(VeDirectMppt.veFrame.ERR);
root["device"]["OR"] = VeDirect.getOrAsString(VeDirect.veFrame.OR); root["device"]["OR"] = VeDirectMppt.getOrAsString(VeDirectMppt.veFrame.OR);
root["device"]["MPPT"] = VeDirect.getMpptAsString(VeDirect.veFrame.MPPT); root["device"]["MPPT"] = VeDirectMppt.getMpptAsString(VeDirectMppt.veFrame.MPPT);
root["device"]["HSDS"]["v"] = VeDirect.veFrame.HSDS; root["device"]["HSDS"]["v"] = VeDirectMppt.veFrame.HSDS;
root["device"]["HSDS"]["u"] = "d"; root["device"]["HSDS"]["u"] = "d";
// battery info // battery info
root["output"]["P"]["v"] = VeDirect.veFrame.P; root["output"]["P"]["v"] = VeDirectMppt.veFrame.P;
root["output"]["P"]["u"] = "W"; root["output"]["P"]["u"] = "W";
root["output"]["P"]["d"] = 0; 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"]["u"] = "V";
root["output"]["V"]["d"] = 2; 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"]["u"] = "A";
root["output"]["I"]["d"] = 2; 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"]["u"] = "%";
root["output"]["E"]["d"] = 1; root["output"]["E"]["d"] = 1;
// panel info // panel info
root["input"]["PPV"]["v"] = VeDirect.veFrame.PPV; root["input"]["PPV"]["v"] = VeDirectMppt.veFrame.PPV;
root["input"]["PPV"]["u"] = "W"; root["input"]["PPV"]["u"] = "W";
root["input"]["PPV"]["d"] = 0; 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"]["u"] = "V";
root["input"]["VPV"]["d"] = 2; 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"]["u"] = "A";
root["input"]["IPV"]["d"] = 2; 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"]["u"] = "kWh";
root["input"]["YieldToday"]["d"] = 3; 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"]["u"] = "kWh";
root["input"]["YieldYesterday"]["d"] = 3; 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"]["u"] = "kWh";
root["input"]["YieldTotal"]["d"] = 3; 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"]["u"] = "W";
root["input"]["MaximumPowerToday"]["d"] = 0; 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"]["u"] = "W";
root["input"]["MaximumPowerYesterday"]["d"] = 0; root["input"]["MaximumPowerYesterday"]["d"] = 0;
@ -147,8 +147,8 @@ void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root)
root["dpl"]["PLSTATE"] = PowerLimiter.getPowerLimiterState(); root["dpl"]["PLSTATE"] = PowerLimiter.getPowerLimiterState();
root["dpl"]["PLLIMIT"] = PowerLimiter.getLastRequestedPowerLimit(); root["dpl"]["PLLIMIT"] = PowerLimiter.getLastRequestedPowerLimit();
if (VeDirect.getLastUpdate() > _newestVedirectTimestamp) { if (VeDirectMppt.getLastUpdate() > _newestVedirectTimestamp) {
_newestVedirectTimestamp = VeDirect.getLastUpdate(); _newestVedirectTimestamp = VeDirectMppt.getLastUpdate();
} }
} }

View File

@ -8,7 +8,7 @@
#include "InverterSettings.h" #include "InverterSettings.h"
#include "Led_Single.h" #include "Led_Single.h"
#include "MessageOutput.h" #include "MessageOutput.h"
#include "VeDirectFrameHandler.h" #include "VeDirectMpptController.h"
#include "Battery.h" #include "Battery.h"
#include "Huawei_can.h" #include "Huawei_can.h"
#include "MqttHandleDtu.h" #include "MqttHandleDtu.h"
@ -165,7 +165,7 @@ void setup()
MessageOutput.println(F("Initialize ve.direct interface... ")); MessageOutput.println(F("Initialize ve.direct interface... "));
if (PinMapping.isValidVictronConfig()) { if (PinMapping.isValidVictronConfig()) {
MessageOutput.printf("ve.direct rx = %d, tx = %d\r\n", pin.victron_rx, pin.victron_tx); 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, config.Vedirect_VerboseLogging);
MessageOutput.println(F("done")); MessageOutput.println(F("done"));
} else { } else {
@ -204,7 +204,7 @@ void loop()
yield(); yield();
// Vedirect_Enabled is unknown to lib. Therefor check has to be done here // Vedirect_Enabled is unknown to lib. Therefor check has to be done here
if (Configuration.get().Vedirect_Enabled) { if (Configuration.get().Vedirect_Enabled) {
VeDirect.loop(); VeDirectMppt.loop();
yield(); yield();
} }
MqttSettings.loop(); MqttSettings.loop();

View File

@ -599,6 +599,7 @@
"Provider": "Datenanbieter", "Provider": "Datenanbieter",
"ProviderPylontechCan": "Pylontech per CAN-Bus", "ProviderPylontechCan": "Pylontech per CAN-Bus",
"ProviderJkBmsSerial": "Jikong (JK) BMS per serieller Verbindung", "ProviderJkBmsSerial": "Jikong (JK) BMS per serieller Verbindung",
"ProviderVictron": "Victron SmartShunt per VE.Direct Schnittstelle",
"JkBmsConfiguration": "JK BMS Einstellungen", "JkBmsConfiguration": "JK BMS Einstellungen",
"JkBmsInterface": "Schnittstellentyp", "JkBmsInterface": "Schnittstellentyp",
"JkBmsInterfaceUart": "TTL-UART an der MCU", "JkBmsInterfaceUart": "TTL-UART an der MCU",
@ -821,10 +822,14 @@
"underTemperature": "Untertemperatur", "underTemperature": "Untertemperatur",
"highTemperature": "Hohe Temperatur", "highTemperature": "Hohe Temperatur",
"overTemperature": "Übertemperatur", "overTemperature": "Übertemperatur",
"lowSOC": "Geringer Ladezustand",
"lowVoltage": "Niedrige Spannung", "lowVoltage": "Niedrige Spannung",
"underVoltage": "Unterspannung", "underVoltage": "Unterspannung",
"highVoltage": "Hohe Spannung", "highVoltage": "Hohe Spannung",
"overVoltage": "Überspannung", "overVoltage": "Überspannung",
"bmsInternal": "BMS intern" "bmsInternal": "BMS intern",
"chargeCycles": "Ladezyklen",
"chargedEnergy": "Geladene Energie",
"dischargedEnergy": "Entladene Energie"
} }
} }

View File

@ -608,6 +608,7 @@
"Provider": "Data Provider", "Provider": "Data Provider",
"ProviderPylontechCan": "Pylontech using CAN bus", "ProviderPylontechCan": "Pylontech using CAN bus",
"ProviderJkBmsSerial": "Jikong (JK) BMS using serial connection", "ProviderJkBmsSerial": "Jikong (JK) BMS using serial connection",
"ProviderVictron": "Victron SmartShunt using VE.Direct interface",
"JkBmsConfiguration": "JK BMS Settings", "JkBmsConfiguration": "JK BMS Settings",
"JkBmsInterface": "Interface Type", "JkBmsInterface": "Interface Type",
"JkBmsInterfaceUart": "TTL-UART on MCU", "JkBmsInterfaceUart": "TTL-UART on MCU",
@ -832,9 +833,13 @@
"highTemperature": "High temperature", "highTemperature": "High temperature",
"overTemperature": "Overtemperature", "overTemperature": "Overtemperature",
"lowVoltage": "Low voltage", "lowVoltage": "Low voltage",
"lowSOC": "Low state of charge",
"underVoltage": "Undervoltage", "underVoltage": "Undervoltage",
"highVoltage": "High voltage", "highVoltage": "High voltage",
"overVoltage": "Overvoltage", "overVoltage": "Overvoltage",
"bmsInternal": "BMS internal" "bmsInternal": "BMS internal",
"chargeCycles": "Charge cycles",
"chargedEnergy": "Charged energy",
"dischargedEnergy": "Discharged energy"
} }
} }

View File

@ -514,6 +514,23 @@
"UpdatesOnly": "Publish values to MQTT only when they change", "UpdatesOnly": "Publish values to MQTT only when they change",
"Save": "@:dtuadmin.Save" "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": { "inverteradmin": {
"InverterSettings": "Paramètres des onduleurs", "InverterSettings": "Paramètres des onduleurs",
"AddInverter": "Ajouter un nouvel onduleur", "AddInverter": "Ajouter un nouvel onduleur",
@ -769,9 +786,13 @@
"highTemperature": "High temperature", "highTemperature": "High temperature",
"overTemperature": "Overtemperature", "overTemperature": "Overtemperature",
"lowVoltage": "Low voltage", "lowVoltage": "Low voltage",
"lowSOC": "Low state of charge",
"underVoltage": "Undervoltage", "underVoltage": "Undervoltage",
"highVoltage": "High voltage", "highVoltage": "High voltage",
"overVoltage": "Overvoltage", "overVoltage": "Overvoltage",
"bmsInternal": "BMS internal" "bmsInternal": "BMS internal",
"chargeCycles": "Charge cycles",
"chargedEnergy": "Charged energy",
"dischargedEnergy": "Discharged energy"
} }
} }

View File

@ -80,6 +80,7 @@ export default defineComponent({
providerTypeList: [ providerTypeList: [
{ key: 0, value: 'PylontechCan' }, { key: 0, value: 'PylontechCan' },
{ key: 1, value: 'JkBmsSerial' }, { key: 1, value: 'JkBmsSerial' },
{ key: 3, value: 'Victron' },
], ],
jkBmsInterfaceTypeList: [ jkBmsInterfaceTypeList: [
{ key: 0, value: 'Uart' }, { key: 0, value: 'Uart' },

Binary file not shown.