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:
- docs/**
- '**/*.md'
workflow_dispatch:
jobs:
get_default_envs:

View File

@ -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;
};

View File

@ -1,7 +1,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "VeDirectFrameHandler.h"
#include "VeDirectMpptController.h"
#include "Configuration.h"
#include <Arduino.h>
@ -18,7 +18,8 @@ public:
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;

View File

@ -2,7 +2,7 @@
#pragma once
#include <ArduinoJson.h>
#include "VeDirectFrameHandler.h"
#include "VeDirectMpptController.h"
class MqttHandleVedirectHassClass {
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 <ESPAsyncWebServer.h>
#include <VeDirectFrameHandler.h>
#include <VeDirectMpptController.h>
class WebApiWsVedirectLiveClass {
public:

View File

@ -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<HardwareSerial>(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<double>(_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,53 +503,34 @@ String VeDirectFrameHandler::getPidAsString(uint16_t pid)
case 0XA116:
strPID = "SmartSolar MPPT VE.Can 250|85 rev2";
break;
case 0xA381:
strPID = "BMV-712 Smart";
break;
case 0xA382:
strPID = "BMV-710H Smart";
break;
case 0xA383:
strPID = "BMV-712 Smart Rev2";
break;
case 0xA389:
strPID = "SmartShunt 500A/50mV";
break;
case 0xA38A:
strPID = "SmartShunt 1000A/50mV";
break;
case 0xA38B:
strPID = "SmartShunt 2000A/50mV";
break;
case 0xA3F0:
strPID = "SmartShunt 2000A/50mV" ;
break;
default:
strPID = pid;
}
return strPID;
}
/*
* 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
@ -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;
}

View File

@ -13,94 +13,48 @@
#include <Arduino.h>
#include <array>
#include <memory>
#define VE_MAX_VALUE_LEN 33 // VE.Direct Protocol: max value size is 33 including /0
#define VE_MAX_HEX_LEN 100 // Maximum size of hex frame - max payload 34 byte (=68 char) + safe buffer
typedef struct {
uint16_t PID; // 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<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 {
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<HardwareSerial> _vedirectSerial;
int _state; // current state
int _prevState; // previous state
uint8_t _checksum; // checksum value
@ -108,13 +62,7 @@ private:
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<double, 5> _efficiency;
std::array<uint8_t, 512> _debugBuffer;
unsigned _debugIn;
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 "PylontechCanReceiver.h"
#include "JkBmsController.h"
#include "VictronSmartShunt.h"
BatteryClass Battery;
@ -42,6 +43,10 @@ void BatteryClass::init()
_upProvider = std::make_unique<JkBms::Controller>();
if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; }
break;
case 3:
_upProvider = std::make_unique<VictronSmartShunt>();
if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; }
break;
default:
MessageOutput.printf("Unknown battery provider: %d\r\n", config.Battery_Provider);
break;

View File

@ -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));
}

View File

@ -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;
}

View File

@ -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

View File

@ -10,7 +10,7 @@
#include "MqttSettings.h"
#include "NetworkSettings.h"
#include "Huawei_can.h"
#include <VeDirectFrameHandler.h>
#include <VeDirectMpptController.h>
#include "MessageOutput.h"
#include <ctime>
#include <cmath>
@ -366,12 +366,12 @@ void PowerLimiterClass::unconditionalSolarPassthrough(std::shared_ptr<InverterAb
{
CONFIG_T& config = Configuration.get();
if (!config.Vedirect_Enabled || !VeDirect.isDataValid()) {
if (!config.Vedirect_Enabled || !VeDirectMppt.isDataValid()) {
shutdown(Status::NoVeDirect);
return;
}
int32_t solarPower = VeDirect.veFrame.V * VeDirect.veFrame.I;
int32_t solarPower = VeDirectMppt.veFrame.V * VeDirectMppt.veFrame.I;
setNewPowerLimit(inverter, inverterPowerDcToAc(inverter, solarPower));
announceStatus(Status::UnconditionalSolarPassthrough);
}
@ -407,11 +407,11 @@ bool PowerLimiterClass::canUseDirectSolarPower()
if (!config.PowerLimiter_SolarPassThroughEnabled
|| isBelowStopThreshold()
|| !config.Vedirect_Enabled
|| !VeDirect.isDataValid()) {
|| !VeDirectMppt.isDataValid()) {
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 VeDirect.veFrame.V * VeDirect.veFrame.I;
return VeDirectMppt.veFrame.V * VeDirectMppt.veFrame.I;
}
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
*/
#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<bool>();
Configuration.write();
VeDirect.setVerboseLogging(config.Vedirect_VerboseLogging);
VeDirectMppt.setVerboseLogging(config.Vedirect_VerboseLogging);
retMsg[F("type")] = F("success");
retMsg[F("message")] = F("Settings saved!");

View File

@ -10,7 +10,7 @@
#include "Battery.h"
#include "Huawei_can.h"
#include "PowerMeter.h"
#include "VeDirectFrameHandler.h"
#include "VeDirectMpptController.h"
#include "defaults.h"
#include <AsyncJson.h>
@ -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;

View File

@ -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();
}
}

View File

@ -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();

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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' },

Binary file not shown.