first version

This commit is contained in:
helgeerbe 2023-02-21 22:06:47 +01:00
parent 30440472f7
commit 1e7f6b8f0f
7 changed files with 239 additions and 136 deletions

View File

@ -19,7 +19,7 @@ public:
void init();
void loop();
private:
std::map<String, String> _kv_map;
veStruct _kvFrame;
uint32_t _lastPublish;
};

View File

@ -41,6 +41,16 @@ char MODULE[] = "VE.Frame"; // Victron seems to use this to find out where loggi
// The name of the record that contains the checksum.
static constexpr char checksumTagName[] = "CHECKSUM";
// state machine
enum States {
IDLE,
RECORD_BEGIN,
RECORD_NAME,
RECORD_VALUE,
CHECKSUM,
RECORD_HEX
};
HardwareSerial VedirectSerial(1);
VeDirectFrameHandler VeDirect;
@ -49,8 +59,10 @@ VeDirectFrameHandler::VeDirectFrameHandler() :
//mStop(false), // don't know what Victron uses this for, not using
_state(IDLE),
_checksum(0),
_textPointer(0),
_name(""),
_value(""),
_tmpFrame(),
_pollInterval(5),
_lastPoll(0)
{
@ -107,7 +119,8 @@ void VeDirectFrameHandler::rxData(uint8_t inbyte)
}
break;
case RECORD_BEGIN:
_name = (char) inbyte;
_textPointer = _name;
*_textPointer++ = inbyte;
_state = RECORD_NAME;
break;
case RECORD_NAME:
@ -115,18 +128,22 @@ void VeDirectFrameHandler::rxData(uint8_t inbyte)
switch(inbyte) {
case '\t':
// the Checksum record indicates a EOR
if (_name.equals(checksumTagName)) {
_state = CHECKSUM;
break;
if ( _textPointer < (_name + sizeof(_name)) ) {
*_textPointer = 0; /* Zero terminate */
if (strcmp(_name, checksumTagName) == 0) {
_state = CHECKSUM;
break;
}
}
_textPointer = _value; /* Reset value pointer */
_state = RECORD_VALUE;
_value = "";
break;
case '#': /* Ignore # from serial number*/
break;
default:
// add byte to name, but do no overflow
_name += (char) inbyte;
if ( _textPointer < (_name + sizeof(_name)) )
*_textPointer++ = inbyte;
break;
}
break;
@ -134,14 +151,18 @@ void VeDirectFrameHandler::rxData(uint8_t inbyte)
// The record value is being received. The \r indicates a new record.
switch(inbyte) {
case '\n':
_tmpMap[_name] = _value;
if ( _textPointer < (_value + sizeof(_value)) ) {
*_textPointer = 0; // make zero ended
textRxEvent(_name, _value);
}
_state = RECORD_BEGIN;
break;
case '\r': /* Skip */
break;
default:
// add byte to value, but do no overflow
_value += (char) inbyte;
if ( _textPointer < (_value + sizeof(_value)) )
*_textPointer++ = inbyte;
break;
}
break;
@ -164,6 +185,72 @@ 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) {
if (strcmp(name, "PID") == 0) {
_tmpFrame.PID = strtol(value, nullptr, 0);
}
else if (strcmp(name, "SER") == 0) {
strcpy(_tmpFrame.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);
}
else if (strcmp(name, "V") == 0) {
_tmpFrame.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);
}
}
/*
* 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.
@ -172,10 +259,10 @@ void VeDirectFrameHandler::rxData(uint8_t inbyte)
*/
void VeDirectFrameHandler::frameEndEvent(bool valid) {
if ( valid ) {
veMap = _tmpMap;
veFrame = _tmpFrame;
setLastUpdate();
}
_tmpMap.clear();
_tmpFrame = {};
}
/*
@ -199,15 +286,12 @@ bool VeDirectFrameHandler::hexRxEvent(uint8_t inbyte) {
}
bool VeDirectFrameHandler::isDataValid() {
if (veMap.empty()) {
return false;
}
if ((millis() - getLastUpdate()) / 1000 > _pollInterval * 5) {
return false;
}
if (veMap.find("SER") == veMap.end()) {
return false;
}
if (strlen(veFrame.SER) == 0) {
return false;
}
return true;
}
@ -229,12 +313,11 @@ void VeDirectFrameHandler::setLastUpdate()
* getPidAsString
* This function returns the product id (PID) as readable text.
*/
String VeDirectFrameHandler::getPidAsString(const char* pid)
String VeDirectFrameHandler::getPidAsString(uint16_t pid)
{
String strPID ="";
long lPID = strtol(pid, nullptr, 0);
switch(lPID) {
switch(pid) {
case 0x0300:
strPID = "BlueSolar MPPT 70|15";
break;
@ -452,12 +535,11 @@ String VeDirectFrameHandler::getPidAsString(const char* pid)
* getCsAsString
* This function returns the state of operations (CS) as readable text.
*/
String VeDirectFrameHandler::getCsAsString(const char* cs)
String VeDirectFrameHandler::getCsAsString(uint8_t cs)
{
String strCS ="";
int iCS = atoi(cs);
switch(iCS) {
switch(cs) {
case 0:
strCS = "OFF";
break;
@ -495,12 +577,11 @@ String VeDirectFrameHandler::getCsAsString(const char* cs)
* getErrAsString
* This function returns error state (ERR) as readable text.
*/
String VeDirectFrameHandler::getErrAsString(const char* err)
String VeDirectFrameHandler::getErrAsString(uint8_t err)
{
String strERR ="";
int iERR = atoi(err);
switch(iERR) {
switch(err) {
case 0:
strERR = "No error";
break;
@ -571,12 +652,11 @@ String VeDirectFrameHandler::getErrAsString(const char* err)
* getOrAsString
* This function returns the off reason (OR) as readable text.
*/
String VeDirectFrameHandler::getOrAsString(const char* offReason)
String VeDirectFrameHandler::getOrAsString(uint32_t offReason)
{
String strOR ="";
long lOR = strtol(offReason, nullptr, 0);
switch(lOR) {
switch(offReason) {
case 0x00000000:
strOR = "Not off";
break;
@ -617,14 +697,13 @@ String VeDirectFrameHandler::getOrAsString(const char* offReason)
* getMpptAsString
* This function returns the state of MPPT (MPPT) as readable text.
*/
String VeDirectFrameHandler::getMpptAsString(const char* mppt)
String VeDirectFrameHandler::getMpptAsString(uint8_t mppt)
{
String strMPPT ="";
int iMPPT = atoi(mppt);
switch(iMPPT) {
switch(mppt) {
case 0:
strMPPT = "Off";
strMPPT = "OFF";
break;
case 1:
strMPPT = "Voltage or current limited";

View File

@ -22,7 +22,29 @@
#define VICTRON_PIN_RX 22 // HardwareSerial RX Pin
#endif
#define VE_MAX_NAME_LEN 9 // VE.Direct Protocol: max name size is 9 including /0
#define VE_MAX_VALUE_LEN 33 // VE.Direct Protocol: max value size is 33 including /0
typedef struct {
uint16_t PID; // pruduct 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
uint16_t HSDS; // day sequence number 1...365
double V; // battery voltage in V
double I; // battery current in A
double VPV; // panel voltage in V
double PPV; // panel power in W
double H19; // yield total kWh
double H20; // yield today kWh
uint16_t H21; // maximum power today W
double H22; // yield yesterday kWh
uint16_t H23; // maximum power yesterday W
} veStruct;
class VeDirectFrameHandler {
@ -34,37 +56,29 @@ public:
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
String getPidAsString(const char* pid); // product id as string
String getCsAsString(const char* cs); // current state as string
String getErrAsString(const char* err); // errer state as string
String getOrAsString(const char* offReason); // off reason as string
String getMpptAsString(const char* mppt); // state of mppt as string
String getPidAsString(uint16_t pid); // product id as string
String getCsAsString(uint8_t cs); // current state as string
String getErrAsString(uint8_t err); // errer state as string
String getOrAsString(uint32_t offReason); // off reason as string
String getMpptAsString(uint8_t mppt); // state of mppt as string
std::map<String, String> veMap; // public map for received name and value pairs
veStruct veFrame; // public map for received name and value pairs
private:
void setLastUpdate(); // set timestampt after successful frame read
void rxData(uint8_t inbyte); // byte of serial data
void textRxEvent(char *, char *);
void frameEndEvent(bool); // copy temp map to public map
void logE(const char *, const char *);
bool hexRxEvent(uint8_t);
//bool mStop; // not sure what Victron uses this for, not using
enum States { // state machine
IDLE,
RECORD_BEGIN,
RECORD_NAME,
RECORD_VALUE,
CHECKSUM,
RECORD_HEX
};
int _state; // current state
uint8_t _checksum; // checksum value
String _name; // buffer for the field name
String _value; // buffer for the field value
std::map<String, String> _tmpMap; // private map for received name and value pairs
char * _textPointer; // pointer to the private buffer we're writing to, name or value
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
unsigned long _pollInterval;
unsigned long _lastPoll;
};

View File

@ -83,7 +83,7 @@ void MqttHandleVedirectHassClass::publishConfig()
void MqttHandleVedirectHassClass::publishSensor(const char* caption, const char* subTopic, const char* deviceClass, const char* stateClass, const char* unitOfMeasurement )
{
String serial = VeDirect.veMap["SER"];
String serial = VeDirect.veFrame.SER;
String sensorId = caption;
sensorId.replace(" ", "_");
@ -93,7 +93,10 @@ void MqttHandleVedirectHassClass::publishSensor(const char* caption, const char*
+ "/" + sensorId
+ "/config";
String statTopic = MqttSettings.getPrefix() + "victron/" + VeDirect.veMap["SER"] + "/" + subTopic;
String statTopic = MqttSettings.getPrefix() + "victron/";
statTopic.concat(VeDirect.veFrame.SER);
statTopic.concat("/");
statTopic.concat(subTopic);
DynamicJsonDocument root(1024);
root[F("name")] = caption;
@ -124,7 +127,7 @@ void MqttHandleVedirectHassClass::publishSensor(const char* caption, const char*
}
void MqttHandleVedirectHassClass::publishBinarySensor(const char* caption, const char* subTopic, const char* payload_on, const char* payload_off)
{
String serial = VeDirect.veMap["SER"];
String serial = VeDirect.veFrame.SER;
String sensorId = caption;
sensorId.replace(" ", "_");
@ -134,7 +137,10 @@ void MqttHandleVedirectHassClass::publishBinarySensor(const char* caption, const
+ "/" + sensorId
+ "/config";
String statTopic = MqttSettings.getPrefix() + "victron/" + VeDirect.veMap["SER"] + "/" + subTopic;
String statTopic = MqttSettings.getPrefix() + "victron/";
statTopic.concat(VeDirect.veFrame.SER);
statTopic.concat("/");
statTopic.concat(subTopic);
DynamicJsonDocument root(1024);
root[F("name")] = caption;
@ -153,11 +159,12 @@ void MqttHandleVedirectHassClass::publishBinarySensor(const char* caption, const
void MqttHandleVedirectHassClass::createDeviceInfo(JsonObject& object)
{
object[F("name")] = "Victron(" + VeDirect.veMap["SER"] + ")";
object[F("ids")] = VeDirect.veMap["SER"];
String serial = VeDirect.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.veMap["PID"].c_str());
object[F("mdl")] = VeDirect.getPidAsString(VeDirect.veFrame.PID);
object[F("sw")] = AUTO_GIT_HASH;
}

View File

@ -29,65 +29,69 @@ void MqttHandleVedirectClass::loop()
}
if (millis() - _lastPublish > (config.Mqtt_PublishInterval * 1000)) {
String key;
String value;
String mapedValue;
bool bChanged = false;
String serial = VeDirect.veMap["SER"];
String topic = "victron/";
topic.concat(VeDirect.veFrame.SER);
topic.concat("/");
String topic = "";
for (auto it = VeDirect.veMap.begin(); it != VeDirect.veMap.end(); ++it) {
key = it->first;
value = it->second;
if (config.Vedirect_UpdatesOnly){
// Mark changed values
auto a = _kv_map.find(key);
bChanged = true;
if (a != _kv_map.end()) {
if (a->first.equals(value)) {
bChanged = false;
}
}
}
// publish only changed key, values pairs
if (!config.Vedirect_UpdatesOnly || (bChanged && config.Vedirect_UpdatesOnly)) {
topic = "victron/" + serial + "/";
topic.concat(key);
if (key.equals("PID")) {
mapedValue = VeDirect.getPidAsString(value.c_str());
}
else if (key.equals("CS")) {
mapedValue = VeDirect.getCsAsString(value.c_str());
}
else if (key.equals("ERR")) {
mapedValue = VeDirect.getErrAsString(value.c_str());
}
else if (key.equals("OR")) {
mapedValue = VeDirect.getOrAsString(value.c_str());
}
else if (key.equals("MPPT")) {
mapedValue = VeDirect.getMpptAsString(value.c_str());
}
else if (key.equals("V") ||
key.equals("I") ||
key.equals("VPV")) {
mapedValue = round(value.toDouble() / 10.0) / 100.0;
}
else if (key.equals("H19") ||
key.equals("H20") ||
key.equals("H22")) {
mapedValue = value.toDouble() / 100.0;
}
else {
mapedValue = value;
}
MqttSettings.publish(topic.c_str(), mapedValue.c_str());
}
if (!config.Vedirect_UpdatesOnly || VeDirect.veFrame.PID != _kvFrame.PID)
MqttSettings.publish(topic + "PID", VeDirect.getPidAsString(VeDirect.veFrame.PID));
if (!config.Vedirect_UpdatesOnly || strcmp(VeDirect.veFrame.SER, _kvFrame.SER) != 0)
MqttSettings.publish(topic + "SER", VeDirect.veFrame.SER );
if (!config.Vedirect_UpdatesOnly || strcmp(VeDirect.veFrame.FW, _kvFrame.FW) != 0)
MqttSettings.publish(topic + "FW", VeDirect.veFrame.FW);
if (!config.Vedirect_UpdatesOnly || VeDirect.veFrame.LOAD != _kvFrame.LOAD)
MqttSettings.publish(topic + "LOAD", VeDirect.veFrame.LOAD == true ? "ON": "OFF");
if (!config.Vedirect_UpdatesOnly || VeDirect.veFrame.CS != _kvFrame.CS)
MqttSettings.publish(topic + "CS", VeDirect.getCsAsString(VeDirect.veFrame.CS));
if (!config.Vedirect_UpdatesOnly || VeDirect.veFrame.ERR != _kvFrame.ERR)
MqttSettings.publish(topic + "ERR", VeDirect.getErrAsString(VeDirect.veFrame.ERR));
if (!config.Vedirect_UpdatesOnly || VeDirect.veFrame.OR != _kvFrame.OR)
MqttSettings.publish(topic + "OR", VeDirect.getOrAsString(VeDirect.veFrame.OR));
if (!config.Vedirect_UpdatesOnly || VeDirect.veFrame.MPPT != _kvFrame.MPPT)
MqttSettings.publish(topic + "MPPT", VeDirect.getMpptAsString(VeDirect.veFrame.MPPT));
if (!config.Vedirect_UpdatesOnly || VeDirect.veFrame.HSDS != _kvFrame.HSDS) {
value = VeDirect.veFrame.HSDS;
MqttSettings.publish(topic + "HSDS", value);
}
if (!config.Vedirect_UpdatesOnly || VeDirect.veFrame.V != _kvFrame.V) {
value = VeDirect.veFrame.V;
MqttSettings.publish(topic + "V", value);
}
if (!config.Vedirect_UpdatesOnly || VeDirect.veFrame.I != _kvFrame.I) {
value = VeDirect.veFrame.I;
MqttSettings.publish(topic + "I", value);
}
if (!config.Vedirect_UpdatesOnly || VeDirect.veFrame.VPV != _kvFrame.VPV) {
value = VeDirect.veFrame.VPV;
MqttSettings.publish(topic + "VPV", value);
}
if (!config.Vedirect_UpdatesOnly || VeDirect.veFrame.PPV != _kvFrame.PPV) {
value = VeDirect.veFrame.PPV;
MqttSettings.publish(topic + "PPV", value);
}
if (!config.Vedirect_UpdatesOnly || VeDirect.veFrame.H19 != _kvFrame.H19) {
value = VeDirect.veFrame.H19;
MqttSettings.publish(topic + "H19", value);
}
if (!config.Vedirect_UpdatesOnly || VeDirect.veFrame.H20 != _kvFrame.H20) {
value = VeDirect.veFrame.H20;
MqttSettings.publish(topic + "H20", value);
}
if (!config.Vedirect_UpdatesOnly || VeDirect.veFrame.H21 != _kvFrame.H21) {
value = VeDirect.veFrame.H21;
MqttSettings.publish(topic + "H21", value);
}
if (!config.Vedirect_UpdatesOnly || VeDirect.veFrame.H22 != _kvFrame.H22) {
value = VeDirect.veFrame.H22;
MqttSettings.publish(topic + "H22", value);
}
if (!config.Vedirect_UpdatesOnly || VeDirect.veFrame.H23 != _kvFrame.H23) {
value = VeDirect.veFrame.H23;
MqttSettings.publish(topic + "H23", value);
}
if (config.Vedirect_UpdatesOnly){
_kv_map = VeDirect.veMap;
_kvFrame= VeDirect.veFrame;
}
_lastPublish = millis();
}

View File

@ -190,12 +190,11 @@ bool PowerLimiterClass::canUseDirectSolarPower()
CONFIG_T& config = Configuration.get();
if (!config.PowerLimiter_SolarPassTroughEnabled
|| !config.Vedirect_Enabled
|| !VeDirect.veMap.count("PPV")) {
|| !config.Vedirect_Enabled) {
return false;
}
if (VeDirect.veMap["PPV"].toInt() < 10) {
if (VeDirect.veFrame.PPV < 10.0) {
// Not enough power
return false;
}
@ -209,7 +208,7 @@ uint32_t PowerLimiterClass::getDirectSolarPower()
return 0;
}
return VeDirect.veMap["PPV"].toInt();
return (uint32_t) round(VeDirect.veFrame.PPV);
}
float PowerLimiterClass::getLoadCorrectedVoltage(std::shared_ptr<InverterAbstract> inverter)

View File

@ -82,37 +82,37 @@ void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root)
// device info
root["data_age"] = (millis() - VeDirect.getLastUpdate() ) / 1000;
root["age_critical"] = !VeDirect.isDataValid();
root["PID"] = VeDirect.getPidAsString(VeDirect.veMap["PID"].c_str());
root["SER"] = VeDirect.veMap["SER"];
root["FW"] = VeDirect.veMap["FW"];
root["LOAD"] = VeDirect.veMap["LOAD"];
root["CS"] = VeDirect.getCsAsString(VeDirect.veMap["CS"].c_str());
root["ERR"] = VeDirect.getErrAsString(VeDirect.veMap["ERR"].c_str());
root["OR"] = VeDirect.getOrAsString(VeDirect.veMap["OR"].c_str());
root["MPPT"] = VeDirect.getMpptAsString(VeDirect.veMap["MPPT"].c_str());
root["HSDS"]["v"] = VeDirect.veMap["HSDS"].toInt();
root["PID"] = VeDirect.getPidAsString(VeDirect.veFrame.PID);
root["SER"] = VeDirect.veFrame.SER;
root["FW"] = VeDirect.veFrame.FW;
root["LOAD"] = VeDirect.veFrame.LOAD == true ? "ON" : "OFF";
root["CS"] = VeDirect.getCsAsString(VeDirect.veFrame.CS);
root["ERR"] = VeDirect.getErrAsString(VeDirect.veFrame.ERR);
root["OR"] = VeDirect.getOrAsString(VeDirect.veFrame.OR);
root["MPPT"] = VeDirect.getMpptAsString(VeDirect.veFrame.MPPT);
root["HSDS"]["v"] = VeDirect.veFrame.HSDS;
root["HSDS"]["u"] = "Days";
// battery info
root["V"]["v"] = round(VeDirect.veMap["V"].toDouble() / 10.0) / 100.0;
root["V"]["v"] = VeDirect.veFrame.V;
root["V"]["u"] = "V";
root["I"]["v"] = round(VeDirect.veMap["I"].toDouble() / 10.0) / 100.0;
root["I"]["v"] = VeDirect.veFrame.I;
root["I"]["u"] = "A";
// panel info
root["VPV"]["v"] = round(VeDirect.veMap["VPV"].toDouble() / 10.0) / 100.0;
root["VPV"]["v"] = VeDirect.veFrame.VPV;
root["VPV"]["u"] = "V";
root["PPV"]["v"] = VeDirect.veMap["PPV"].toInt();
root["PPV"]["v"] = VeDirect.veFrame.PPV;
root["PPV"]["u"] = "W";
root["H19"]["v"] = VeDirect.veMap["H19"].toDouble() / 100.0;
root["H19"]["v"] = VeDirect.veFrame.H19;
root["H19"]["u"] = "kWh";
root["H20"]["v"] = VeDirect.veMap["H20"].toDouble() / 100.0;
root["H20"]["v"] = VeDirect.veFrame.H20;
root["H20"]["u"] = "kWh";
root["H21"]["v"] = VeDirect.veMap["H21"].toInt();
root["H21"]["v"] = VeDirect.veFrame.H21;
root["H21"]["u"] = "W";
root["H22"]["v"] = VeDirect.veMap["H22"].toDouble() / 100.0;
root["H22"]["v"] = VeDirect.veFrame.H22;
root["H22"]["u"] = "kWh";
root["H23"]["v"] = VeDirect.veMap["H23"].toInt();
root["H23"]["v"] = VeDirect.veFrame.H23;
root["H23"]["u"] = "W";
if (VeDirect.getLastUpdate() > _newestVedirectTimestamp) {