polish VE.Direct HEX support
* show charge controller temperature in live view * send hex requests right after decoding a frame. this seems to have the best chance of getting an answer to all requests. * deem 0xFFFFFFFF value of network total DC power as invalid indicator. neither network state, nor network info, nor network mode seem to indicate that the charge controller is part of a VE.Smart network. for that reason, we revert to always querying the network total DC power value, but testing it for max(uin32_t) value, which seems to indicate that the charge controller is not part of a VE.Smart network. * improve (verbose) logging, e.g., use _logId, and print names of response codes and known registers, always print error messages, add additional tests to prevent overly verbose messages. * move hex protocol definitions to VeDirectData.h header and use enum classes * define register addresses in enum class * move values retrieved through hex protocol into main MPPT data struct * do not send HEX requests if the serial interface cannot send data * detect whether smart battery sense temperature is available * web app: make all VE.Direct sub-cards iterable. this makes addind more values much simpler and saves a bunch of code in the web app. * make VeDirectFrameHandler state a type-safe enum class * unindent MPPT controller loop() * whitespace cleanup
This commit is contained in:
parent
aadd7303ac
commit
6b8c93d2e6
@ -222,3 +222,39 @@ frozen::string const& veMpptStruct::getOrAsString() const
|
||||
|
||||
return getAsString(values, OR);
|
||||
}
|
||||
|
||||
frozen::string const& VeDirectHexData::getResponseAsString() const
|
||||
{
|
||||
using Response = VeDirectHexResponse;
|
||||
static constexpr frozen::map<Response, frozen::string, 7> values = {
|
||||
{ Response::DONE, "Done" },
|
||||
{ Response::UNKNOWN, "Unknown" },
|
||||
{ Response::ERROR, "Error" },
|
||||
{ Response::PING, "Ping" },
|
||||
{ Response::GET, "Get" },
|
||||
{ Response::SET, "Set" },
|
||||
{ Response::ASYNC, "Async" }
|
||||
};
|
||||
|
||||
return getAsString(values, rsp);
|
||||
}
|
||||
|
||||
frozen::string const& VeDirectHexData::getRegisterAsString() const
|
||||
{
|
||||
using Register = VeDirectHexRegister;
|
||||
static constexpr frozen::map<Register, frozen::string, 11> values = {
|
||||
{ Register::DeviceMode, "Device Mode" },
|
||||
{ Register::DeviceState, "Device State" },
|
||||
{ Register::RemoteControlUsed, "Remote Control Used" },
|
||||
{ Register::PanelVoltage, "Panel Voltage" },
|
||||
{ Register::ChargerVoltage, "Charger Voltage" },
|
||||
{ Register::NetworkTotalDcInputPower, "Network Total DC Input Power" },
|
||||
{ Register::ChargeControllerTemperature, "Charger Controller Temperature" },
|
||||
{ Register::SmartBatterySenseTemperature, "Smart Battery Sense Temperature" },
|
||||
{ Register::NetworkInfo, "Network Info" },
|
||||
{ Register::NetworkMode, "Network Mode" },
|
||||
{ Register::NetworkStatus, "Network Status" }
|
||||
};
|
||||
|
||||
return getAsString(values, addr);
|
||||
}
|
||||
|
||||
@ -34,6 +34,17 @@ struct veMpptStruct : veStruct {
|
||||
float H22; // yield yesterday kWh
|
||||
int32_t H23; // maximum power yesterday W
|
||||
|
||||
// these are values communicated through the HEX protocol. the pair's first
|
||||
// value is the timestamp the respective info was last received. if it is
|
||||
// zero, the value is deemed invalid. the timestamp is reset if no current
|
||||
// value could be retrieved.
|
||||
std::pair<uint32_t, int32_t> MpptTemperatureMilliCelsius;
|
||||
std::pair<uint32_t, int32_t> SmartBatterySenseTemperatureMilliCelsius;
|
||||
std::pair<uint32_t, uint32_t> NetworkTotalDcInputPowerMilliWatts;
|
||||
std::pair<uint32_t, uint8_t> NetworkInfo;
|
||||
std::pair<uint32_t, uint8_t> NetworkMode;
|
||||
std::pair<uint32_t, uint8_t> NetworkStatus;
|
||||
|
||||
frozen::string const& getMpptAsString() const; // state of mppt as string
|
||||
frozen::string const& getCsAsString() const; // current state as string
|
||||
frozen::string const& getErrAsString() const; // error state as string
|
||||
@ -68,3 +79,57 @@ struct veShuntStruct : veStruct {
|
||||
int32_t H17; // Amount of discharged energy
|
||||
int32_t H18; // Amount of charged energy
|
||||
};
|
||||
|
||||
enum class VeDirectHexCommand : uint8_t {
|
||||
ENTER_BOOT = 0x0,
|
||||
PING = 0x1,
|
||||
RSV1 = 0x2,
|
||||
APP_VERSION = 0x3,
|
||||
PRODUCT_ID = 0x4,
|
||||
RSV2 = 0x5,
|
||||
RESTART = 0x6,
|
||||
GET = 0x7,
|
||||
SET = 0x8,
|
||||
RSV3 = 0x9,
|
||||
ASYNC = 0xA,
|
||||
RSV4 = 0xB,
|
||||
RSV5 = 0xC,
|
||||
RSV6 = 0xD,
|
||||
RSV7 = 0xE,
|
||||
RSV8 = 0xF
|
||||
};
|
||||
|
||||
enum class VeDirectHexResponse : uint8_t {
|
||||
DONE = 0x1,
|
||||
UNKNOWN = 0x3,
|
||||
ERROR = 0x4,
|
||||
PING = 0x5,
|
||||
GET = 0x7,
|
||||
SET = 0x8,
|
||||
ASYNC = 0xA
|
||||
};
|
||||
|
||||
enum class VeDirectHexRegister : uint16_t {
|
||||
DeviceMode = 0x0200,
|
||||
DeviceState = 0x0201,
|
||||
RemoteControlUsed = 0x0202,
|
||||
PanelVoltage = 0xEDBB,
|
||||
ChargerVoltage = 0xEDD5,
|
||||
NetworkTotalDcInputPower = 0x2027,
|
||||
ChargeControllerTemperature = 0xEDDB,
|
||||
SmartBatterySenseTemperature = 0xEDEC,
|
||||
NetworkInfo = 0x200D,
|
||||
NetworkMode = 0x200E,
|
||||
NetworkStatus = 0x200F
|
||||
};
|
||||
|
||||
struct VeDirectHexData {
|
||||
VeDirectHexResponse rsp; // hex response code
|
||||
VeDirectHexRegister addr; // register address
|
||||
uint8_t flags; // flags
|
||||
uint32_t value; // integer value of register
|
||||
char text[VE_MAX_HEX_LEN]; // text/string response
|
||||
|
||||
frozen::string const& getResponseAsString() const;
|
||||
frozen::string const& getRegisterAsString() const;
|
||||
};
|
||||
|
||||
@ -39,18 +39,6 @@
|
||||
// The name of the record that contains the checksum.
|
||||
static constexpr char checksumTagName[] = "CHECKSUM";
|
||||
|
||||
// state machine
|
||||
enum States {
|
||||
IDLE = 1,
|
||||
RECORD_BEGIN = 2,
|
||||
RECORD_NAME = 3,
|
||||
RECORD_VALUE = 4,
|
||||
CHECKSUM = 5,
|
||||
RECORD_HEX = 6
|
||||
};
|
||||
|
||||
|
||||
|
||||
class Silent : public Print {
|
||||
public:
|
||||
size_t write(uint8_t c) final { return 0; }
|
||||
@ -62,7 +50,7 @@ template<typename T>
|
||||
VeDirectFrameHandler<T>::VeDirectFrameHandler() :
|
||||
_msgOut(&MessageOutputDummy),
|
||||
_lastUpdate(0),
|
||||
_state(IDLE),
|
||||
_state(State::IDLE),
|
||||
_checksum(0),
|
||||
_textPointer(0),
|
||||
_hexSize(0),
|
||||
@ -79,6 +67,7 @@ void VeDirectFrameHandler<T>::init(char const* who, int8_t rx, int8_t tx, Print*
|
||||
_vedirectSerial = std::make_unique<HardwareSerial>(hwSerialPort);
|
||||
_vedirectSerial->begin(19200, SERIAL_8N1, rx, tx);
|
||||
_vedirectSerial->flush();
|
||||
_canSend = (tx != -1);
|
||||
_msgOut = msgOut;
|
||||
_verboseLogging = verboseLogging;
|
||||
_debugIn = 0;
|
||||
@ -103,7 +92,7 @@ template<typename T>
|
||||
void VeDirectFrameHandler<T>::reset()
|
||||
{
|
||||
_checksum = 0;
|
||||
_state = IDLE;
|
||||
_state = State::IDLE;
|
||||
_textData.clear();
|
||||
}
|
||||
|
||||
@ -118,8 +107,9 @@ void VeDirectFrameHandler<T>::loop()
|
||||
// there will never be a large gap between two bytes of the same frame.
|
||||
// if such a large gap is observed, reset the state machine so it tries
|
||||
// to decode a new frame once more data arrives.
|
||||
if (IDLE != _state && (millis() - _lastByteMillis) > 500) {
|
||||
_msgOut->printf("%s Resetting state machine (was %d) after timeout\r\n", _logId, _state);
|
||||
if (State::IDLE != _state && (millis() - _lastByteMillis) > 500) {
|
||||
_msgOut->printf("%s Resetting state machine (was %d) after timeout\r\n",
|
||||
_logId, static_cast<unsigned>(_state));
|
||||
if (_verboseLogging) { dumpDebugBuffer(); }
|
||||
reset();
|
||||
}
|
||||
@ -141,34 +131,34 @@ void VeDirectFrameHandler<T>::rxData(uint8_t inbyte)
|
||||
}
|
||||
}
|
||||
|
||||
if ( (inbyte == ':') && (_state != CHECKSUM) ) {
|
||||
if ( (inbyte == ':') && (_state != State::CHECKSUM) ) {
|
||||
_prevState = _state; //hex frame can interrupt TEXT
|
||||
_state = RECORD_HEX;
|
||||
_state = State::RECORD_HEX;
|
||||
_hexSize = 0;
|
||||
}
|
||||
if (_state != RECORD_HEX) {
|
||||
if (_state != State::RECORD_HEX) {
|
||||
_checksum += inbyte;
|
||||
}
|
||||
inbyte = toupper(inbyte);
|
||||
|
||||
switch(_state) {
|
||||
case IDLE:
|
||||
case State::IDLE:
|
||||
/* wait for \n of the start of an record */
|
||||
switch(inbyte) {
|
||||
case '\n':
|
||||
_state = RECORD_BEGIN;
|
||||
_state = State::RECORD_BEGIN;
|
||||
break;
|
||||
case '\r': /* Skip */
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case RECORD_BEGIN:
|
||||
case State::RECORD_BEGIN:
|
||||
_textPointer = _name;
|
||||
*_textPointer++ = inbyte;
|
||||
_state = RECORD_NAME;
|
||||
_state = State::RECORD_NAME;
|
||||
break;
|
||||
case RECORD_NAME:
|
||||
case State::RECORD_NAME:
|
||||
// The record name is being received, terminated by a \t
|
||||
switch(inbyte) {
|
||||
case '\t':
|
||||
@ -176,12 +166,12 @@ void VeDirectFrameHandler<T>::rxData(uint8_t inbyte)
|
||||
if ( _textPointer < (_name + sizeof(_name)) ) {
|
||||
*_textPointer = 0; /* Zero terminate */
|
||||
if (strcmp(_name, checksumTagName) == 0) {
|
||||
_state = CHECKSUM;
|
||||
_state = State::CHECKSUM;
|
||||
break;
|
||||
}
|
||||
}
|
||||
_textPointer = _value; /* Reset value pointer */
|
||||
_state = RECORD_VALUE;
|
||||
_state = State::RECORD_VALUE;
|
||||
break;
|
||||
case '#': /* Ignore # from serial number*/
|
||||
break;
|
||||
@ -192,7 +182,7 @@ void VeDirectFrameHandler<T>::rxData(uint8_t inbyte)
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case RECORD_VALUE:
|
||||
case State::RECORD_VALUE:
|
||||
// The record value is being received. The \r indicates a new record.
|
||||
switch(inbyte) {
|
||||
case '\n':
|
||||
@ -200,7 +190,7 @@ void VeDirectFrameHandler<T>::rxData(uint8_t inbyte)
|
||||
*_textPointer = 0; // make zero ended
|
||||
_textData.push_back({_name, _value});
|
||||
}
|
||||
_state = RECORD_BEGIN;
|
||||
_state = State::RECORD_BEGIN;
|
||||
break;
|
||||
case '\r': /* Skip */
|
||||
break;
|
||||
@ -211,7 +201,7 @@ void VeDirectFrameHandler<T>::rxData(uint8_t inbyte)
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case CHECKSUM:
|
||||
case State::CHECKSUM:
|
||||
{
|
||||
if (_verboseLogging) { dumpDebugBuffer(); }
|
||||
if (_checksum == 0) {
|
||||
@ -227,7 +217,7 @@ void VeDirectFrameHandler<T>::rxData(uint8_t inbyte)
|
||||
reset();
|
||||
break;
|
||||
}
|
||||
case RECORD_HEX:
|
||||
case State::RECORD_HEX:
|
||||
_state = hexRxEvent(inbyte);
|
||||
break;
|
||||
}
|
||||
@ -279,17 +269,23 @@ void VeDirectFrameHandler<T>::processTextData(std::string const& name, std::stri
|
||||
* This function records hex answers or async messages
|
||||
*/
|
||||
template<typename T>
|
||||
int VeDirectFrameHandler<T>::hexRxEvent(uint8_t inbyte)
|
||||
typename VeDirectFrameHandler<T>::State VeDirectFrameHandler<T>::hexRxEvent(uint8_t inbyte)
|
||||
{
|
||||
int ret=RECORD_HEX; // default - continue recording until end of frame
|
||||
State ret = State::RECORD_HEX; // default - continue recording until end of frame
|
||||
|
||||
switch (inbyte) {
|
||||
case '\n':
|
||||
// now we can analyse the hex message
|
||||
_hexBuffer[_hexSize] = '\0';
|
||||
VeDirectHexData data;
|
||||
if (disassembleHexData(data))
|
||||
hexDataHandler(data);
|
||||
if (disassembleHexData(data) && !hexDataHandler(data) && _verboseLogging) {
|
||||
_msgOut->printf("%s Unhandled Hex %s Response, addr: 0x%04X (%s), "
|
||||
"value: 0x%08X, flags: 0x%02X\r\n", _logId,
|
||||
data.getResponseAsString().data(),
|
||||
static_cast<unsigned>(data.addr),
|
||||
data.getRegisterAsString().data(),
|
||||
data.value, data.flags);
|
||||
}
|
||||
|
||||
// restore previous state
|
||||
ret=_prevState;
|
||||
@ -301,7 +297,7 @@ int VeDirectFrameHandler<T>::hexRxEvent(uint8_t inbyte)
|
||||
if (_hexSize>=VE_MAX_HEX_LEN) { // oops -buffer overflow - something went wrong, we abort
|
||||
_msgOut->printf("%s hexRx buffer overflow - aborting read\r\n", _logId);
|
||||
_hexSize=0;
|
||||
ret=IDLE;
|
||||
ret = State::IDLE;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -19,39 +19,6 @@
|
||||
#include <deque>
|
||||
#include "VeDirectData.h"
|
||||
|
||||
// hex send commands
|
||||
enum VeDirectHexCommand {
|
||||
ENTER_BOOT = 0x00,
|
||||
PING = 0x01,
|
||||
APP_VERSION = 0x02,
|
||||
PRODUCT_ID = 0x04,
|
||||
RESTART = 0x06,
|
||||
GET = 0x07,
|
||||
SET = 0x08,
|
||||
ASYNC = 0x0A,
|
||||
UNKNOWN = 0x0F
|
||||
};
|
||||
|
||||
// hex receive responses
|
||||
enum VeDirectHexResponse {
|
||||
R_DONE = 0x01,
|
||||
R_UNKNOWN = 0x03,
|
||||
R_ERROR = 0x04,
|
||||
R_PING = 0x05,
|
||||
R_GET = 0x07,
|
||||
R_SET = 0x08,
|
||||
R_ASYNC = 0x0A,
|
||||
};
|
||||
|
||||
// hex response data, contains the disassembeled hex message, forwarded to virtual funktion hexDataHandler()
|
||||
struct VeDirectHexData {
|
||||
VeDirectHexResponse rsp; // hex response type
|
||||
uint16_t id; // register address
|
||||
uint8_t flag; // flag
|
||||
uint32_t value; // value from register
|
||||
char text[VE_MAX_HEX_LEN]; // text/string response
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
class VeDirectFrameHandler {
|
||||
public:
|
||||
@ -59,12 +26,12 @@ public:
|
||||
uint32_t getLastUpdate() const; // timestamp of last successful frame read
|
||||
bool isDataValid() const; // return true if data valid and not outdated
|
||||
T const& getData() const { return _tmpFrame; }
|
||||
bool sendHexCommand(VeDirectHexCommand cmd, uint16_t id = 0, uint32_t value = 0, uint8_t valunibble = 0); // send hex commands via ve.direct
|
||||
bool sendHexCommand(VeDirectHexCommand cmd, VeDirectHexRegister addr, uint32_t value = 0, uint8_t valsize = 0);
|
||||
|
||||
protected:
|
||||
VeDirectFrameHandler();
|
||||
void init(char const* who, int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort);
|
||||
virtual void hexDataHandler(VeDirectHexData const &data) { } // handles the disassembeled hex response
|
||||
virtual bool hexDataHandler(VeDirectHexData const &data) { return false; } // handles the disassembeled hex response
|
||||
|
||||
bool _verboseLogging;
|
||||
Print* _msgOut;
|
||||
@ -72,6 +39,9 @@ protected:
|
||||
|
||||
T _tmpFrame;
|
||||
|
||||
bool _canSend;
|
||||
char _logId[32];
|
||||
|
||||
private:
|
||||
void reset();
|
||||
void dumpDebugBuffer();
|
||||
@ -79,22 +49,32 @@ private:
|
||||
void processTextData(std::string const& name, std::string const& value);
|
||||
virtual bool processTextDataDerived(std::string const& name, std::string const& value) = 0;
|
||||
virtual void frameValidEvent() { }
|
||||
int hexRxEvent(uint8_t);
|
||||
bool disassembleHexData(VeDirectHexData &data); //return true if disassembling was possible
|
||||
|
||||
std::unique_ptr<HardwareSerial> _vedirectSerial;
|
||||
int _state; // current state
|
||||
int _prevState; // previous state
|
||||
|
||||
enum class State {
|
||||
IDLE = 1,
|
||||
RECORD_BEGIN = 2,
|
||||
RECORD_NAME = 3,
|
||||
RECORD_VALUE = 4,
|
||||
CHECKSUM = 5,
|
||||
RECORD_HEX = 6
|
||||
};
|
||||
State _state;
|
||||
State _prevState;
|
||||
|
||||
State hexRxEvent(uint8_t inbyte);
|
||||
|
||||
uint8_t _checksum; // checksum value
|
||||
char * _textPointer; // pointer to the private buffer we're writing to, name or value
|
||||
int _hexSize; // length of hex buffer
|
||||
char _hexBuffer[VE_MAX_HEX_LEN] = { }; // buffer for received hex frames
|
||||
int _hexSize; // length of hex buffer
|
||||
char _hexBuffer[VE_MAX_HEX_LEN]; // buffer for received hex frames
|
||||
char _name[VE_MAX_VALUE_LEN]; // buffer for the field name
|
||||
char _value[VE_MAX_VALUE_LEN]; // buffer for the field value
|
||||
std::array<uint8_t, 512> _debugBuffer;
|
||||
unsigned _debugIn;
|
||||
uint32_t _lastByteMillis;
|
||||
char _logId[32];
|
||||
|
||||
/**
|
||||
* not every frame contains every value the device is communicating, i.e.,
|
||||
|
||||
@ -6,22 +6,17 @@ HexHandler.cpp
|
||||
*
|
||||
* How to use:
|
||||
* 1. Use sendHexCommand() to send hex messages. Use the Victron documentation to find the parameter.
|
||||
* 2. The from class "VeDirectFrameHandler" derived class X must overwrite the function
|
||||
* 2. The from class "VeDirectFrameHandler" derived class X must overwrite the function
|
||||
* void VeDirectFrameHandler::hexDataHandler(VeDirectHexData const &data)
|
||||
* to handle the received hex messages. All hex messages will be forwarted to function hexDataHandler()
|
||||
* 3. Analyse the content of data (struct VeDirectHexData) to check if a message fits.
|
||||
* 3. Analyse the content of data (struct VeDirectHexData) to check if a message fits.
|
||||
*
|
||||
* 2024.03.08 - 0.4 - adds the ability to send hex commands and to parse hex messages
|
||||
* 2024.03.08 - 0.4 - adds the ability to send hex commands and to parse hex messages
|
||||
*
|
||||
*/
|
||||
#include <Arduino.h>
|
||||
#include "VeDirectFrameHandler.h"
|
||||
|
||||
|
||||
// support for debugging, 0=without extended logging, 1=with extended logging
|
||||
constexpr int MODUL_DEBUG = 0;
|
||||
|
||||
|
||||
/*
|
||||
* calcHexFrameCheckSum()
|
||||
* help function to calculate the hex checksum
|
||||
@ -30,7 +25,7 @@ constexpr int MODUL_DEBUG = 0;
|
||||
#define hex2byte(b) (ascii2hex(*(b)))*16+((ascii2hex(*(b+1))))
|
||||
static uint8_t calcHexFrameCheckSum(const char* buffer, int size) {
|
||||
uint8_t checksum=0x55-ascii2hex(buffer[1]);
|
||||
for (int i=2; i<size; i+=2)
|
||||
for (int i=2; i<size; i+=2)
|
||||
checksum -= hex2byte(buffer+i);
|
||||
return (checksum);
|
||||
}
|
||||
@ -46,7 +41,7 @@ static uint32_t AsciiHexLE2Int(const char *ascii, const uint8_t anz) {
|
||||
char help[9] = {};
|
||||
|
||||
// sort from little endian format to normal format
|
||||
switch (anz) {
|
||||
switch (anz) {
|
||||
case 1:
|
||||
help[0] = ascii[0];
|
||||
break;
|
||||
@ -54,19 +49,19 @@ static uint32_t AsciiHexLE2Int(const char *ascii, const uint8_t anz) {
|
||||
case 4:
|
||||
case 8:
|
||||
for (uint8_t i = 0; i < anz; i += 2) {
|
||||
help[i] = ascii[anz-i-2];
|
||||
help[i] = ascii[anz-i-2];
|
||||
help[i+1] = ascii[anz-i-1];
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return (static_cast<uint32_t>(strtoul(help, nullptr, 16)));
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* disassembleHexData()
|
||||
* analysis the hex message and extract: response, id, flag and value/text
|
||||
* analysis the hex message and extract: response, address, flags and value/text
|
||||
* buffer: pointer to message (ascii hex little endian format)
|
||||
* data: disassembeled message
|
||||
* return: true = successful disassembeld, false = hex sum fault or message
|
||||
@ -76,29 +71,30 @@ template<typename T>
|
||||
bool VeDirectFrameHandler<T>::disassembleHexData(VeDirectHexData &data) {
|
||||
bool state = false;
|
||||
char * buffer = _hexBuffer;
|
||||
auto len = strlen(buffer);
|
||||
auto len = strlen(buffer);
|
||||
|
||||
// reset hex data first
|
||||
data = {};
|
||||
data = {};
|
||||
|
||||
if ((len > 3) && (calcHexFrameCheckSum(buffer, len) == 0x00)) {
|
||||
data.rsp = static_cast<VeDirectHexResponse>(AsciiHexLE2Int(buffer+1, 1));
|
||||
|
||||
using Response = VeDirectHexResponse;
|
||||
switch (data.rsp) {
|
||||
case R_DONE:
|
||||
case R_ERROR:
|
||||
case R_PING:
|
||||
case R_UNKNOWN:
|
||||
case Response::DONE:
|
||||
case Response::ERROR:
|
||||
case Response::PING:
|
||||
case Response::UNKNOWN:
|
||||
strncpy(data.text, buffer+2, len-4);
|
||||
state = true;
|
||||
break;
|
||||
case R_GET:
|
||||
case R_SET:
|
||||
case R_ASYNC:
|
||||
data.id = AsciiHexLE2Int(buffer+2, 4);
|
||||
case Response::GET:
|
||||
case Response::SET:
|
||||
case Response::ASYNC:
|
||||
data.addr = static_cast<VeDirectHexRegister>(AsciiHexLE2Int(buffer+2, 4));
|
||||
|
||||
// future option: to analyse the flag here?
|
||||
data.flag = AsciiHexLE2Int(buffer+6, 2);
|
||||
// future option: to analyse the flags here?
|
||||
data.flags = AsciiHexLE2Int(buffer+6, 2);
|
||||
|
||||
if (len == 12) { // 8bit value
|
||||
data.value = AsciiHexLE2Int(buffer+8, 2);
|
||||
@ -120,13 +116,8 @@ bool VeDirectFrameHandler<T>::disassembleHexData(VeDirectHexData &data) {
|
||||
}
|
||||
}
|
||||
|
||||
if constexpr(MODUL_DEBUG == 1) {
|
||||
_msgOut->printf("[VE.Direct] debug: disassembleHexData(), rsp: %i, id: 0x%04X, value: 0x%X, Flag: 0x%02X\r\n",
|
||||
data.rsp, data.id, data.value, data.flag);
|
||||
}
|
||||
|
||||
if (_verboseLogging && !state)
|
||||
_msgOut->printf("[VE.Direct] failed to disassemble the hex message: %s\r\n", buffer);
|
||||
if (!state)
|
||||
_msgOut->printf("%s failed to disassemble the hex message: %s\r\n", _logId, buffer);
|
||||
|
||||
return (state);
|
||||
}
|
||||
@ -156,7 +147,7 @@ static String Int2HexLEString(uint32_t value, uint8_t anz) {
|
||||
default:
|
||||
;
|
||||
}
|
||||
return String(help);
|
||||
return String(help);
|
||||
}
|
||||
|
||||
|
||||
@ -164,40 +155,41 @@ static String Int2HexLEString(uint32_t value, uint8_t anz) {
|
||||
* sendHexCommand()
|
||||
* send the hex commend after assembling the command string
|
||||
* cmd: command
|
||||
* id: id/register, default 0
|
||||
* value: value to write into a id/register, default 0
|
||||
* valsize: size of the value/id, 8, 16 or 32 bit, default 0
|
||||
* addr: register address, default 0
|
||||
* value: value to write into a register, default 0
|
||||
* valsize: size of the value, 8, 16 or 32 bit, default 0
|
||||
* return: true = message assembeld and send, false = it was not possible to put the message together
|
||||
* SAMPLE: ping command: sendHexCommand(PING),
|
||||
* SAMPLE: ping command: sendHexCommand(PING),
|
||||
* read total DC input power sendHexCommand(GET, 0xEDEC)
|
||||
* set Charge current limit 10A sendHexCommand(SET, 0x2015, 64, 16)
|
||||
*
|
||||
*
|
||||
* WARNING: some values are stored in non-volatile memory. Continuous writing, for example from a control loop, will
|
||||
* lead to early failure.
|
||||
* lead to early failure.
|
||||
* On MPPT for example 0xEDE0 - 0xEDFF. Check the Vivtron doc "BlueSolar-HEX-protocol.pdf"
|
||||
*/
|
||||
template<typename T>
|
||||
bool VeDirectFrameHandler<T>::sendHexCommand(VeDirectHexCommand cmd, uint16_t id, uint32_t value, uint8_t valsize) {
|
||||
bool VeDirectFrameHandler<T>::sendHexCommand(VeDirectHexCommand cmd, VeDirectHexRegister addr, uint32_t value, uint8_t valsize) {
|
||||
bool ret = false;
|
||||
uint8_t flag = 0x00; // always 0x00
|
||||
uint8_t flags = 0x00; // always 0x00
|
||||
|
||||
String txData = ":" + Int2HexLEString(cmd, 1); // add the command nibble
|
||||
String txData = ":" + Int2HexLEString(static_cast<uint32_t>(cmd), 1); // add the command nibble
|
||||
|
||||
using Command = VeDirectHexCommand;
|
||||
switch (cmd) {
|
||||
case PING:
|
||||
case APP_VERSION:
|
||||
case PRODUCT_ID:
|
||||
ret = true;
|
||||
break;
|
||||
case GET:
|
||||
case ASYNC:
|
||||
txData += Int2HexLEString(id, 4); // add the id/register (4 nibble)
|
||||
txData += Int2HexLEString(flag, 2); // add the flag (2 nibble)
|
||||
case Command::PING:
|
||||
case Command::APP_VERSION:
|
||||
case Command::PRODUCT_ID:
|
||||
ret = true;
|
||||
break;
|
||||
case SET:
|
||||
txData += Int2HexLEString(id, 4); // add the id/register (4 nibble)
|
||||
txData += Int2HexLEString(flag, 2); // add the flag (2 nibble)
|
||||
case Command::GET:
|
||||
case Command::ASYNC:
|
||||
txData += Int2HexLEString(static_cast<uint16_t>(addr), 4);
|
||||
txData += Int2HexLEString(flags, 2); // add the flags (2 nibble)
|
||||
ret = true;
|
||||
break;
|
||||
case Command::SET:
|
||||
txData += Int2HexLEString(static_cast<uint16_t>(addr), 4);
|
||||
txData += Int2HexLEString(flags, 2); // add the flags (2 nibble)
|
||||
if ((valsize == 8) || (valsize == 16) || (valsize == 32)) {
|
||||
txData += Int2HexLEString(value, valsize/4); // add value (2-8 nibble)
|
||||
ret = true;
|
||||
@ -205,7 +197,7 @@ bool VeDirectFrameHandler<T>::sendHexCommand(VeDirectHexCommand cmd, uint16_t id
|
||||
break;
|
||||
default:
|
||||
ret = false;
|
||||
break;
|
||||
break;
|
||||
}
|
||||
|
||||
if (ret) {
|
||||
@ -213,15 +205,16 @@ bool VeDirectFrameHandler<T>::sendHexCommand(VeDirectHexCommand cmd, uint16_t id
|
||||
txData += Int2HexLEString(calcHexFrameCheckSum(txData.c_str(), txData.length()), 2);
|
||||
String send = txData + "\n"; // hex command end byte
|
||||
_vedirectSerial->write(send.c_str(), send.length());
|
||||
|
||||
if constexpr(MODUL_DEBUG == 1) {
|
||||
auto blen = _vedirectSerial->availableForWrite();
|
||||
_msgOut->printf("[VE.Direct] debug: sendHexCommand(): %s, Free FIFO-Buffer: %u\r\n", txData.c_str(), blen);
|
||||
}
|
||||
}
|
||||
|
||||
if (_verboseLogging && !ret)
|
||||
_msgOut->println("[VE.Direct] send hex command fault:" + txData);
|
||||
if (_verboseLogging) {
|
||||
auto blen = _vedirectSerial->availableForWrite();
|
||||
_msgOut->printf("%s Sending Hex Command: %s, Free FIFO-Buffer: %u\r\n",
|
||||
_logId, txData.c_str(), blen);
|
||||
}
|
||||
}
|
||||
|
||||
if (!ret)
|
||||
_msgOut->printf("%s send hex command fault: %s\r\n", _logId, txData.c_str());
|
||||
|
||||
return (ret);
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
/* VeDirectMpptController.cpp
|
||||
*
|
||||
*
|
||||
*
|
||||
* 2020.08.20 - 0.0 - ???
|
||||
* 2024.03.18 - 0.1 - add of: - temperature from "Smart Battery Sense" connected over VE.Smart network
|
||||
* - temperature from internal MPPT sensor
|
||||
@ -10,10 +10,7 @@
|
||||
#include <Arduino.h>
|
||||
#include "VeDirectMpptController.h"
|
||||
|
||||
|
||||
// support for debugging, 0=without extended logging, 1=with extended logging
|
||||
constexpr int MODUL_DEBUG = 0;
|
||||
|
||||
//#define PROCESS_NETWORK_STATE
|
||||
|
||||
void VeDirectMpptController::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort)
|
||||
{
|
||||
@ -93,32 +90,50 @@ void VeDirectMpptController::frameValidEvent() {
|
||||
_efficiency.addNumber(static_cast<float>(_tmpFrame.P * 100) / _tmpFrame.PPV);
|
||||
_tmpFrame.E = _efficiency.getAverage();
|
||||
}
|
||||
}
|
||||
|
||||
if (!_canSend) { return; }
|
||||
|
||||
/*
|
||||
// loop()
|
||||
// send hex commands to MPPT every 5 seconds
|
||||
*/
|
||||
void VeDirectMpptController::loop()
|
||||
{
|
||||
VeDirectFrameHandler::loop();
|
||||
|
||||
// Copy from the "VE.Direct Protocol" documentation
|
||||
// For firmware version v1.52 and below, when no VE.Direct queries are sent to the device, the
|
||||
// charger periodically sends human readable (TEXT) data to the serial port. For firmware
|
||||
// versions v1.53 and above, the charger always periodically sends TEXT data to the serial port.
|
||||
// --> We just use hex commandes for firmware >= 1.53 to keep text messages alive
|
||||
if (atoi(_tmpFrame.FW) >= 153 ) {
|
||||
if ((millis() - _lastPingTime) > 5000) {
|
||||
if (atoi(_tmpFrame.FW) < 153) { return; }
|
||||
|
||||
sendHexCommand(GET, 0x2027); // MPPT total DC input power
|
||||
sendHexCommand(GET, 0xEDDB); // MPPT internal temperature
|
||||
sendHexCommand(GET, 0xEDEC); // "Smart Battery Sense" temperature
|
||||
sendHexCommand(GET, 0x200F); // Network info
|
||||
_lastPingTime = millis();
|
||||
using Command = VeDirectHexCommand;
|
||||
using Register = VeDirectHexRegister;
|
||||
|
||||
sendHexCommand(Command::GET, Register::ChargeControllerTemperature);
|
||||
sendHexCommand(Command::GET, Register::SmartBatterySenseTemperature);
|
||||
sendHexCommand(Command::GET, Register::NetworkTotalDcInputPower);
|
||||
|
||||
#ifdef PROCESS_NETWORK_STATE
|
||||
sendHexCommand(Command::GET, Register::NetworkInfo);
|
||||
sendHexCommand(Command::GET, Register::NetworkMode);
|
||||
sendHexCommand(Command::GET, Register::NetworkStatus);
|
||||
#endif // PROCESS_NETWORK_STATE
|
||||
}
|
||||
|
||||
|
||||
void VeDirectMpptController::loop()
|
||||
{
|
||||
VeDirectFrameHandler::loop();
|
||||
|
||||
auto resetTimestamp = [this](auto& pair) {
|
||||
if (pair.first > 0 && (millis() - pair.first) > (10 * 1000)) {
|
||||
pair.first = 0;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
resetTimestamp(_tmpFrame.MpptTemperatureMilliCelsius);
|
||||
resetTimestamp(_tmpFrame.SmartBatterySenseTemperatureMilliCelsius);
|
||||
resetTimestamp(_tmpFrame.NetworkTotalDcInputPowerMilliWatts);
|
||||
|
||||
#ifdef PROCESS_NETWORK_STATE
|
||||
resetTimestamp(_tmpFrame.NetworkInfo);
|
||||
resetTimestamp(_tmpFrame.NetworkMode);
|
||||
resetTimestamp(_tmpFrame.NetworkStatus);
|
||||
#endif // PROCESS_NETWORK_STATE
|
||||
}
|
||||
|
||||
|
||||
@ -127,62 +142,104 @@ void VeDirectMpptController::loop()
|
||||
* analyse the content of VE.Direct hex messages
|
||||
* Handels the received hex data from the MPPT
|
||||
*/
|
||||
void VeDirectMpptController::hexDataHandler(VeDirectHexData const &data) {
|
||||
bool state = false;
|
||||
bool VeDirectMpptController::hexDataHandler(VeDirectHexData const &data) {
|
||||
if (data.rsp != VeDirectHexResponse::GET &&
|
||||
data.rsp != VeDirectHexResponse::ASYNC) { return false; }
|
||||
|
||||
switch (data.rsp) {
|
||||
case R_GET:
|
||||
case R_ASYNC:
|
||||
auto regLog = static_cast<uint16_t>(data.addr);
|
||||
|
||||
// check if MPPT internal temperature is available
|
||||
if(data.id == 0xEDDB) {
|
||||
_ExData.T = static_cast<int32_t>(data.value) * 10; // conversion from unit [0.01°C] to unit [m°C]
|
||||
_ExData.Tts = millis();
|
||||
state = true;
|
||||
|
||||
if constexpr(MODUL_DEBUG == 1)
|
||||
_msgOut->printf("[VE.Direct] debug: hexDataHandler(), MTTP Temperature: %.2f°C\r\n", _ExData.T/1000.0);
|
||||
}
|
||||
switch (data.addr) {
|
||||
case VeDirectHexRegister::ChargeControllerTemperature:
|
||||
_tmpFrame.MpptTemperatureMilliCelsius =
|
||||
{ millis(), static_cast<int32_t>(data.value) * 10 };
|
||||
|
||||
// check if temperature from "Smart Battery Sense" is available
|
||||
if(data.id == 0xEDEC) {
|
||||
_ExData.TSBS = static_cast<int32_t>(data.value) * 10 - 272150; // conversion from unit [0.01K] to unit [m°C]
|
||||
_ExData.TSBSts = millis();
|
||||
state = true;
|
||||
|
||||
if constexpr(MODUL_DEBUG == 1)
|
||||
_msgOut->printf("[VE.Direct] debug: hexDataHandler(), Battery Temperature: %.2f°C\r\n", _ExData.TSBS/1000.0);
|
||||
}
|
||||
if (_verboseLogging) {
|
||||
_msgOut->printf("%s Hex Data: MPPT Temperature (0x%04X): %.2f°C\r\n",
|
||||
_logId, regLog,
|
||||
_tmpFrame.MpptTemperatureMilliCelsius.second / 1000.0);
|
||||
}
|
||||
return true;
|
||||
break;
|
||||
|
||||
// check if "Total DC power" is available
|
||||
if(data.id == 0x2027) {
|
||||
_ExData.TDCP = data.value * 10; // conversion from unit [0.01W] to unit [mW]
|
||||
_ExData.TDCPts = millis();
|
||||
state = true;
|
||||
case VeDirectHexRegister::SmartBatterySenseTemperature:
|
||||
if (data.value == 0xFFFF) {
|
||||
if (_verboseLogging) {
|
||||
_msgOut->printf("%s Hex Data: Smart Battery Sense Temperature is not available\r\n", _logId);
|
||||
}
|
||||
return true; // we know what to do with it, and we decided to ignore the value
|
||||
}
|
||||
|
||||
if constexpr(MODUL_DEBUG == 1)
|
||||
_msgOut->printf("[VE.Direct] debug: hexDataHandler(), Total Power: %.2fW\r\n", _ExData.TDCP/1000.0);
|
||||
}
|
||||
_tmpFrame.SmartBatterySenseTemperatureMilliCelsius =
|
||||
{ millis(), static_cast<int32_t>(data.value) * 10 - 272150 };
|
||||
|
||||
// check if connected MPPT is charge instance master
|
||||
// Hint: not used right now but maybe necessary for future extensions
|
||||
if(data.id == 0x200F) {
|
||||
_veMaster = ((data.value & 0x0F) == 0x02) ? true : false;
|
||||
state = true;
|
||||
if (_verboseLogging) {
|
||||
_msgOut->printf("%s Hex Data: Smart Battery Sense Temperature (0x%04X): %.2f°C\r\n",
|
||||
_logId, regLog,
|
||||
_tmpFrame.SmartBatterySenseTemperatureMilliCelsius.second / 1000.0);
|
||||
}
|
||||
return true;
|
||||
break;
|
||||
|
||||
if constexpr(MODUL_DEBUG == 1)
|
||||
_msgOut->printf("[VE.Direct] debug: hexDataHandler(), Networkmode: 0x%X\r\n", data.value);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
case VeDirectHexRegister::NetworkTotalDcInputPower:
|
||||
if (data.value == 0xFFFFFFFF) {
|
||||
if (_verboseLogging) {
|
||||
_msgOut->printf("%s Hex Data: Network total DC power value "
|
||||
"indicates non-networked controller\r\n", _logId);
|
||||
}
|
||||
_tmpFrame.NetworkTotalDcInputPowerMilliWatts = { 0, 0 };
|
||||
return true; // we know what to do with it, and we decided to ignore the value
|
||||
}
|
||||
|
||||
_tmpFrame.NetworkTotalDcInputPowerMilliWatts =
|
||||
{ millis(), data.value * 10 };
|
||||
|
||||
if (_verboseLogging) {
|
||||
_msgOut->printf("%s Hex Data: Network Total DC Power (0x%04X): %.2fW\r\n",
|
||||
_logId, regLog,
|
||||
_tmpFrame.NetworkTotalDcInputPowerMilliWatts.second / 1000.0);
|
||||
}
|
||||
return true;
|
||||
break;
|
||||
|
||||
#ifdef PROCESS_NETWORK_STATE
|
||||
case VeDirectHexRegister::NetworkInfo:
|
||||
_tmpFrame.NetworkInfo =
|
||||
{ millis(), static_cast<uint8_t>(data.value) };
|
||||
|
||||
if (_verboseLogging) {
|
||||
_msgOut->printf("%s Hex Data: Network Info (0x%04X): 0x%X\r\n",
|
||||
_logId, regLog, data.value);
|
||||
}
|
||||
return true;
|
||||
break;
|
||||
|
||||
case VeDirectHexRegister::NetworkMode:
|
||||
_tmpFrame.NetworkMode =
|
||||
{ millis(), static_cast<uint8_t>(data.value) };
|
||||
|
||||
if (_verboseLogging) {
|
||||
_msgOut->printf("%s Hex Data: Network Mode (0x%04X): 0x%X\r\n",
|
||||
_logId, regLog, data.value);
|
||||
}
|
||||
return true;
|
||||
break;
|
||||
|
||||
case VeDirectHexRegister::NetworkStatus:
|
||||
_tmpFrame.NetworkStatus =
|
||||
{ millis(), static_cast<uint8_t>(data.value) };
|
||||
|
||||
if (_verboseLogging) {
|
||||
_msgOut->printf("%s Hex Data: Network Status (0x%04X): 0x%X\r\n",
|
||||
_logId, regLog, data.value);
|
||||
}
|
||||
return true;
|
||||
break;
|
||||
#endif // PROCESS_NETWORK_STATE
|
||||
|
||||
default:
|
||||
return false;
|
||||
break;
|
||||
}
|
||||
|
||||
if constexpr(MODUL_DEBUG == 1)
|
||||
_msgOut->printf("[VE.Direct] debug: hexDataHandler(): rsp: %i, id: 0x%04X, value: %i[0x%08X], text: %s\r\n",
|
||||
data.rsp, data.id, data.value, data.value, data.text);
|
||||
|
||||
if (_verboseLogging && state)
|
||||
_msgOut->printf("[VE.Direct] MPPT hex message: rsp: %i, id: 0x%04X, value: %i[0x%08X], text: %s\r\n",
|
||||
data.rsp, data.id, data.value, data.value, data.text);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -44,24 +44,11 @@ public:
|
||||
|
||||
using data_t = veMpptStruct;
|
||||
|
||||
virtual void loop() final; // main loop to read ve.direct data
|
||||
|
||||
struct veMPPTExStruct {
|
||||
int32_t T; // temperature [m°C] from internal MPPT sensor
|
||||
unsigned long Tts; // time of last recieved value
|
||||
int32_t TSBS; // temperature [m°C] from the "Smart Battery Sense"
|
||||
unsigned long TSBSts; // time of last recieved value
|
||||
uint32_t TDCP; // total DC input power [mW]
|
||||
unsigned long TDCPts; // time of last recieved value
|
||||
};
|
||||
veMPPTExStruct _ExData{};
|
||||
veMPPTExStruct const *getExData() const { return &_ExData; }
|
||||
void loop() final;
|
||||
|
||||
private:
|
||||
void hexDataHandler(VeDirectHexData const &data) final;
|
||||
bool hexDataHandler(VeDirectHexData const &data) final;
|
||||
bool processTextDataDerived(std::string const& name, std::string const& value) final;
|
||||
void frameValidEvent() final;
|
||||
MovingAverage<float, 5> _efficiency;
|
||||
unsigned long _lastPingTime = 0L; // time of last device PING/GET hex command
|
||||
bool _veMaster = true; // MPPT is instance master
|
||||
};
|
||||
|
||||
@ -146,57 +146,64 @@ void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root, bool ful
|
||||
}
|
||||
|
||||
void WebApiWsVedirectLiveClass::populateJson(const JsonObject &root, const VeDirectMpptController::data_t &mpptData) {
|
||||
// device info
|
||||
root["device"]["PID"] = mpptData.getPidAsString();
|
||||
root["device"]["SER"] = String(mpptData.SER);
|
||||
root["device"]["FW"] = String(mpptData.FW);
|
||||
root["device"]["LOAD"] = mpptData.LOAD ? "ON" : "OFF";
|
||||
root["device"]["CS"] = mpptData.getCsAsString();
|
||||
root["device"]["ERR"] = mpptData.getErrAsString();
|
||||
root["device"]["OR"] = mpptData.getOrAsString();
|
||||
root["device"]["MPPT"] = mpptData.getMpptAsString();
|
||||
root["device"]["HSDS"]["v"] = mpptData.HSDS;
|
||||
root["device"]["HSDS"]["u"] = "d";
|
||||
root["product_id"] = mpptData.getPidAsString();
|
||||
root["firmware_version"] = String(mpptData.FW);
|
||||
|
||||
// battery info
|
||||
root["output"]["P"]["v"] = mpptData.P;
|
||||
root["output"]["P"]["u"] = "W";
|
||||
root["output"]["P"]["d"] = 0;
|
||||
root["output"]["V"]["v"] = mpptData.V;
|
||||
root["output"]["V"]["u"] = "V";
|
||||
root["output"]["V"]["d"] = 2;
|
||||
root["output"]["I"]["v"] = mpptData.I;
|
||||
root["output"]["I"]["u"] = "A";
|
||||
root["output"]["I"]["d"] = 2;
|
||||
root["output"]["E"]["v"] = mpptData.E;
|
||||
root["output"]["E"]["u"] = "%";
|
||||
root["output"]["E"]["d"] = 1;
|
||||
const JsonObject &values = root.createNestedObject("values");
|
||||
|
||||
// panel info
|
||||
root["input"]["PPV"]["v"] = mpptData.PPV;
|
||||
root["input"]["PPV"]["u"] = "W";
|
||||
root["input"]["PPV"]["d"] = 0;
|
||||
root["input"]["VPV"]["v"] = mpptData.VPV;
|
||||
root["input"]["VPV"]["u"] = "V";
|
||||
root["input"]["VPV"]["d"] = 2;
|
||||
root["input"]["IPV"]["v"] = mpptData.IPV;
|
||||
root["input"]["IPV"]["u"] = "A";
|
||||
root["input"]["IPV"]["d"] = 2;
|
||||
root["input"]["YieldToday"]["v"] = mpptData.H20;
|
||||
root["input"]["YieldToday"]["u"] = "kWh";
|
||||
root["input"]["YieldToday"]["d"] = 3;
|
||||
root["input"]["YieldYesterday"]["v"] = mpptData.H22;
|
||||
root["input"]["YieldYesterday"]["u"] = "kWh";
|
||||
root["input"]["YieldYesterday"]["d"] = 3;
|
||||
root["input"]["YieldTotal"]["v"] = mpptData.H19;
|
||||
root["input"]["YieldTotal"]["u"] = "kWh";
|
||||
root["input"]["YieldTotal"]["d"] = 3;
|
||||
root["input"]["MaximumPowerToday"]["v"] = mpptData.H21;
|
||||
root["input"]["MaximumPowerToday"]["u"] = "W";
|
||||
root["input"]["MaximumPowerToday"]["d"] = 0;
|
||||
root["input"]["MaximumPowerYesterday"]["v"] = mpptData.H23;
|
||||
root["input"]["MaximumPowerYesterday"]["u"] = "W";
|
||||
root["input"]["MaximumPowerYesterday"]["d"] = 0;
|
||||
const JsonObject &device = values.createNestedObject("device");
|
||||
device["LOAD"] = mpptData.LOAD ? "ON" : "OFF";
|
||||
device["CS"] = mpptData.getCsAsString();
|
||||
device["MPPT"] = mpptData.getMpptAsString();
|
||||
device["OR"] = mpptData.getOrAsString();
|
||||
device["ERR"] = mpptData.getErrAsString();
|
||||
device["HSDS"]["v"] = mpptData.HSDS;
|
||||
device["HSDS"]["u"] = "d";
|
||||
if (mpptData.MpptTemperatureMilliCelsius.first > 0) {
|
||||
device["MpptTemperature"]["v"] = mpptData.MpptTemperatureMilliCelsius.second / 1000.0;
|
||||
device["MpptTemperature"]["u"] = "°C";
|
||||
device["MpptTemperature"]["d"] = "1";
|
||||
}
|
||||
|
||||
const JsonObject &output = values.createNestedObject("output");
|
||||
output["P"]["v"] = mpptData.P;
|
||||
output["P"]["u"] = "W";
|
||||
output["P"]["d"] = 0;
|
||||
output["V"]["v"] = mpptData.V;
|
||||
output["V"]["u"] = "V";
|
||||
output["V"]["d"] = 2;
|
||||
output["I"]["v"] = mpptData.I;
|
||||
output["I"]["u"] = "A";
|
||||
output["I"]["d"] = 2;
|
||||
output["E"]["v"] = mpptData.E;
|
||||
output["E"]["u"] = "%";
|
||||
output["E"]["d"] = 1;
|
||||
|
||||
const JsonObject &input = values.createNestedObject("input");
|
||||
input["PPV"]["v"] = mpptData.PPV;
|
||||
input["PPV"]["u"] = "W";
|
||||
input["PPV"]["d"] = 0;
|
||||
input["VPV"]["v"] = mpptData.VPV;
|
||||
input["VPV"]["u"] = "V";
|
||||
input["VPV"]["d"] = 2;
|
||||
input["IPV"]["v"] = mpptData.IPV;
|
||||
input["IPV"]["u"] = "A";
|
||||
input["IPV"]["d"] = 2;
|
||||
input["YieldToday"]["v"] = mpptData.H20;
|
||||
input["YieldToday"]["u"] = "kWh";
|
||||
input["YieldToday"]["d"] = 3;
|
||||
input["YieldYesterday"]["v"] = mpptData.H22;
|
||||
input["YieldYesterday"]["u"] = "kWh";
|
||||
input["YieldYesterday"]["d"] = 3;
|
||||
input["YieldTotal"]["v"] = mpptData.H19;
|
||||
input["YieldTotal"]["u"] = "kWh";
|
||||
input["YieldTotal"]["d"] = 3;
|
||||
input["MaximumPowerToday"]["v"] = mpptData.H21;
|
||||
input["MaximumPowerToday"]["u"] = "W";
|
||||
input["MaximumPowerToday"]["d"] = 0;
|
||||
input["MaximumPowerYesterday"]["v"] = mpptData.H23;
|
||||
input["MaximumPowerYesterday"]["u"] = "W";
|
||||
input["MaximumPowerYesterday"]["d"] = 0;
|
||||
}
|
||||
|
||||
void WebApiWsVedirectLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len)
|
||||
|
||||
@ -18,13 +18,13 @@
|
||||
<div class="p-1 flex-grow-1">
|
||||
<div class="d-flex flex-wrap">
|
||||
<div style="padding-right: 2em;">
|
||||
{{ item.device.PID }}
|
||||
{{ item.product_id }}
|
||||
</div>
|
||||
<div style="padding-right: 2em;">
|
||||
{{ $t('vedirecthome.SerialNumber') }} {{ item.device.SER }}
|
||||
{{ $t('vedirecthome.SerialNumber') }} {{ serial }}
|
||||
</div>
|
||||
<div style="padding-right: 2em;">
|
||||
{{ $t('vedirecthome.FirmwareNumber') }} {{ item.device.FW }}
|
||||
{{ $t('vedirecthome.FirmwareNumber') }} {{ item.firmware_version }}
|
||||
</div>
|
||||
<div style="padding-right: 2em;">
|
||||
{{ $t('vedirecthome.DataAge') }} {{ $t('vedirecthome.Seconds', {'val': Math.floor(item.data_age_ms / 1000)}) }}
|
||||
@ -55,9 +55,9 @@
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row flex-row flex-wrap align-items-start g-3">
|
||||
<div class="col order-0">
|
||||
<div class="card" :class="{ 'border-info': true }">
|
||||
<div class="card-header text-bg-info">{{ $t('vedirecthome.DeviceInfo') }}</div>
|
||||
<div v-for="(values, section) in item.values" v-bind:key="section" class="col order-0">
|
||||
<div class="card" :class="{ 'border-info': (section === 'device') }">
|
||||
<div :class="(section === 'device')?'card-header text-bg-info':'card-header'">{{ $t('vedirecthome.section_' + section) }}</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
@ -69,95 +69,21 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('vedirecthome.LoadOutputState') }}</th>
|
||||
<td style="text-align: right">{{item.device.LOAD}}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('vedirecthome.StateOfOperation') }}</th>
|
||||
<td style="text-align: right">{{item.device.CS}}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('vedirecthome.TrackerOperationMode') }}</th>
|
||||
<td style="text-align: right">{{item.device.MPPT}}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('vedirecthome.OffReason') }}</th>
|
||||
<td style="text-align: right">{{item.device.OR}}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('vedirecthome.ErrorCode') }}</th>
|
||||
<td style="text-align: right">{{item.device.ERR}}</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{{ $t('vedirecthome.DaySequenceNumber') }}</th>
|
||||
<td style="text-align: right">{{item.device.HSDS.v}}</td>
|
||||
<td>{{item.device.HSDS.u}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col order-1">
|
||||
<div class="card" :class="{ 'border-info': false }">
|
||||
<div class="card-header">{{ $t('vedirecthome.Battery') }}</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{ $t('vedirecthome.Property') }}</th>
|
||||
<th style="text-align: right" scope="col">{{ $t('vedirecthome.Value') }}</th>
|
||||
<th scope="col">{{ $t('vedirecthome.Unit') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(prop, key) in item.output" v-bind:key="key">
|
||||
<th scope="row">{{ $t('vedirecthome.output.' + key) }}</th>
|
||||
<tr v-for="(prop, key) in values" v-bind:key="key">
|
||||
<th scope="row">{{ $t('vedirecthome.' + section + '.' + key) }}</th>
|
||||
<td style="text-align: right">
|
||||
{{ $n(prop.v, 'decimal', {
|
||||
<template v-if="typeof prop === 'string'">
|
||||
{{ prop }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $n(prop.v, 'decimal', {
|
||||
minimumFractionDigits: prop.d,
|
||||
maximumFractionDigits: prop.d})
|
||||
}}
|
||||
}}
|
||||
</template>
|
||||
</td>
|
||||
<td>{{prop.u}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col order-2">
|
||||
<div class="card" :class="{ 'border-info': false }">
|
||||
<div class="card-header">{{ $t('vedirecthome.Panel') }}</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{ $t('vedirecthome.Property') }}</th>
|
||||
<th style="text-align: right" scope="col">{{ $t('vedirecthome.Value') }}</th>
|
||||
<th scope="col">{{ $t('vedirecthome.Unit') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(prop, key) in item.input" v-bind:key="key">
|
||||
<th scope="row">{{ $t('vedirecthome.input.' + key) }}</th>
|
||||
<td style="text-align: right">
|
||||
{{ $n(prop.v, 'decimal', {
|
||||
minimumFractionDigits: prop.d,
|
||||
maximumFractionDigits: prop.d})
|
||||
}}
|
||||
</td>
|
||||
<td>{{prop.u}}</td>
|
||||
<td v-if="typeof prop === 'string'"></td>
|
||||
<td v-else>{{prop.u}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@ -153,24 +153,27 @@
|
||||
"FirmwareNumber": "Firmware Version: ",
|
||||
"DataAge": "letzte Aktualisierung: ",
|
||||
"Seconds": "vor {val} Sekunden",
|
||||
"DeviceInfo": "Geräteinformation",
|
||||
"Property": "Eigenschaft",
|
||||
"Value": "Wert",
|
||||
"Unit": "Einheit",
|
||||
"LoadOutputState": "Status Ladeausgang",
|
||||
"StateOfOperation": "Betriebszustand",
|
||||
"TrackerOperationMode": "Betriebszustand des Trackers",
|
||||
"OffReason": "Grund für das Ausschalten",
|
||||
"ErrorCode": "Fehlerbeschreibung",
|
||||
"DaySequenceNumber": "Anzahl der Tage (0..364)",
|
||||
"Battery": "Ausgang (Batterie)",
|
||||
"section_device": "Geräteinformation",
|
||||
"device": {
|
||||
"LOAD": "Status Ladeausgang",
|
||||
"CS": "Betriebszustand",
|
||||
"MPPT": "Betriebszustand des Trackers",
|
||||
"OR": "Grund für das Ausschalten",
|
||||
"ERR": "Fehlerbeschreibung",
|
||||
"HSDS": "Anzahl der Tage (0..364)",
|
||||
"MpptTemperature": "Ladereglertemperatur"
|
||||
},
|
||||
"section_output": "Ausgang (Batterie)",
|
||||
"output": {
|
||||
"P": "Leistung (berechnet)",
|
||||
"V": "Spannung",
|
||||
"I": "Strom",
|
||||
"E": "Effizienz (berechnet)"
|
||||
},
|
||||
"Panel": "Eingang (Solarpanele)",
|
||||
"section_input": "Eingang (Solarpanele)",
|
||||
"input": {
|
||||
"PPV": "Leistung",
|
||||
"VPV": "Spannung",
|
||||
|
||||
@ -153,24 +153,27 @@
|
||||
"FirmwareNumber": "Firmware Number: ",
|
||||
"DataAge": "Data Age: ",
|
||||
"Seconds": "{val} seconds",
|
||||
"DeviceInfo": "Device Info",
|
||||
"Property": "Property",
|
||||
"Value": "Value",
|
||||
"Unit": "Unit",
|
||||
"LoadOutputState": "Load output state",
|
||||
"StateOfOperation": "State of operation",
|
||||
"TrackerOperationMode": "Tracker operation mode",
|
||||
"OffReason": "Off reason",
|
||||
"ErrorCode": "Error code",
|
||||
"DaySequenceNumber": "Day sequence number (0..364)",
|
||||
"Battery": "Output (Battery)",
|
||||
"section_device": "Device Info",
|
||||
"device": {
|
||||
"LOAD": "Load output state",
|
||||
"CS": "State of operation",
|
||||
"MPPT": "Tracker operation mode",
|
||||
"OR": "Off reason",
|
||||
"ERR": "Error code",
|
||||
"HSDS": "Day sequence number (0..364)",
|
||||
"MpptTemperature": "Charge controller temperature"
|
||||
},
|
||||
"section_output": "Output (Battery)",
|
||||
"output": {
|
||||
"P": "Power (calculated)",
|
||||
"V": "Voltage",
|
||||
"I": "Current",
|
||||
"E": "Efficiency (calculated)"
|
||||
},
|
||||
"Panel": "Input (Solar Panels)",
|
||||
"section_input": "Input (Solar Panels)",
|
||||
"input": {
|
||||
"PPV": "Power",
|
||||
"VPV": "Voltage",
|
||||
|
||||
@ -153,24 +153,27 @@
|
||||
"FirmwareNumber": "Firmware Number: ",
|
||||
"DataAge": "Data Age: ",
|
||||
"Seconds": "{val} seconds",
|
||||
"DeviceInfo": "Device Info",
|
||||
"Property": "Property",
|
||||
"Value": "Value",
|
||||
"Unit": "Unit",
|
||||
"LoadOutputState": "Load output state",
|
||||
"StateOfOperation": "State of operation",
|
||||
"TrackerOperationMode": "Tracker operation mode",
|
||||
"OffReason": "Off reason",
|
||||
"ErrorCode": "Error code",
|
||||
"DaySequenceNumber": "Day sequence number (0..364)",
|
||||
"Battery": "Output (Battery)",
|
||||
"section_device": "Device Info",
|
||||
"device": {
|
||||
"LOAD": "Load output state",
|
||||
"CS": "State of operation",
|
||||
"MPPT": "Tracker operation mode",
|
||||
"OR": "Off reason",
|
||||
"ERR": "Error code",
|
||||
"HSDS": "Day sequence number (0..364)",
|
||||
"MpptTemperature": "Charge controller temperature"
|
||||
},
|
||||
"section_output": "Output (Battery)",
|
||||
"output": {
|
||||
"P": "Power (calculated)",
|
||||
"V": "Voltage",
|
||||
"I": "Current",
|
||||
"E": "Efficiency (calculated)"
|
||||
},
|
||||
"Panel": "Input (Solar Panels)",
|
||||
"section_input": "Input (Solar Panels)",
|
||||
"input": {
|
||||
"PPV": "Power",
|
||||
"VPV": "Voltage",
|
||||
|
||||
@ -10,39 +10,11 @@ export interface Vedirect {
|
||||
instances: { [key: string]: VedirectInstance };
|
||||
}
|
||||
|
||||
type MpptData = (ValueObject | string)[];
|
||||
|
||||
export interface VedirectInstance {
|
||||
data_age_ms: number;
|
||||
device: VedirectDevice;
|
||||
output: VedirectOutput;
|
||||
input: VedirectInput;
|
||||
}
|
||||
|
||||
export interface VedirectDevice {
|
||||
SER: string;
|
||||
PID: string;
|
||||
FW: string;
|
||||
LOAD: ValueObject;
|
||||
CS: ValueObject;
|
||||
MPPT: ValueObject;
|
||||
OR: ValueObject;
|
||||
ERR: ValueObject;
|
||||
HSDS: ValueObject;
|
||||
}
|
||||
|
||||
export interface VedirectOutput {
|
||||
P: ValueObject;
|
||||
V: ValueObject;
|
||||
I: ValueObject;
|
||||
E: ValueObject;
|
||||
}
|
||||
|
||||
export interface VedirectInput {
|
||||
PPV: ValueObject;
|
||||
VPV: ValueObject;
|
||||
IPV: ValueObject;
|
||||
H19: ValueObject;
|
||||
H20: ValueObject;
|
||||
H21: ValueObject;
|
||||
H22: ValueObject;
|
||||
H23: ValueObject;
|
||||
product_id: string;
|
||||
firmware_version: string;
|
||||
values: { [key: string]: MpptData };
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user