diff --git a/include/WebApi.h b/include/WebApi.h index f24e6ae7..d492783b 100644 --- a/include/WebApi.h +++ b/include/WebApi.h @@ -12,6 +12,7 @@ #include "WebApi_sysstatus.h" #include "WebApi_webapp.h" #include "WebApi_ws_live.h" +#include "WebApi_ws_vedirect_live.h" #include "WebApi_vedirect.h" #include @@ -36,6 +37,7 @@ private: WebApiSysstatusClass _webApiSysstatus; WebApiWebappClass _webApiWebapp; WebApiWsLiveClass _webApiWsLive; + WebApiWsVedirectLiveClass _webApiWsVedirectLive; WebApiVedirectClass _webApiVedirect; }; diff --git a/include/WebApi_ws_vedirect_live.h b/include/WebApi_ws_vedirect_live.h new file mode 100644 index 00000000..36f80c8b --- /dev/null +++ b/include/WebApi_ws_vedirect_live.h @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "ArduinoJson.h" +#include +#include + +class WebApiWsVedirectLiveClass { +public: + WebApiWsVedirectLiveClass(); + void init(AsyncWebServer* server); + void loop(); + +private: + void generateJsonResponse(JsonVariant& root); + void onLivedataStatus(AsyncWebServerRequest* request); + void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len); + + AsyncWebServer* _server; + AsyncWebSocket _ws; + + uint32_t _lastWsPublish = 0; + uint32_t _lastVedirectUpdateCheck = 0; + unsigned long _lastWsCleanup = 0; + uint32_t _newestVedirectTimestamp = 0; +}; \ No newline at end of file diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp index dd39d0f1..13c39e34 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp +++ b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp @@ -206,6 +206,7 @@ void VeDirectFrameHandler::frameEndEvent(bool valid) { } } } + setLastUpdate(); } frameIndex = 0; // reset frame } @@ -223,9 +224,436 @@ void VeDirectFrameHandler::logE(const char * module, const char * error) { } /* - * hexRxEvent - * This function included for continuity and possible future use. + * getLastUpdate + * This function returns the timestamp of the last succesful read of a ve.direct frame. */ bool VeDirectFrameHandler::hexRxEvent(uint8_t inbyte) { return true; // stubbed out for future +} + +uint32_t VeDirectFrameHandler::getLastUpdate() +{ + return _lastUpdate; +} + +/* + * setLastUpdate + * This function is called every time a new ve.direct frame was read. + */ +void VeDirectFrameHandler::setLastUpdate() +{ + _lastUpdate = millis(); +} + +/* + * getPidAsString + * This function returns the product id (PID) as readable text. + */ +String VeDirectFrameHandler::getPidAsString(const char* pid) +{ + String strPID =""; + + long lPID = strtol(pid, nullptr, 0); + switch(lPID) { + case 0x0300: + strPID = "BlueSolar MPPT 70|15"; + break; + case 0xA040: + strPID = "BlueSolar MPPT 75|50"; + break; + case 0xA041: + strPID = "BlueSolar MPPT 150|35"; + break; + case 0xA042: + strPID = "BlueSolar MPPT 75|15"; + break; + case 0xA043: + strPID = "BlueSolar MPPT 100|15"; + break; + case 0xA044: + strPID = "BlueSolar MPPT 100|30"; + break; + case 0xA045: + strPID = "BlueSolar MPPT 100|50"; + break; + case 0xA046: + strPID = "BlueSolar MPPT 100|70"; + break; + case 0xA047: + strPID = "BlueSolar MPPT 150|100"; + break; + case 0xA049: + strPID = "BlueSolar MPPT 100|50 rev2"; + break; + case 0xA04A: + strPID = "BlueSolar MPPT 100|30 rev2"; + break; + case 0xA04B: + strPID = "BlueSolar MPPT 150|35 rev2"; + break; + case 0XA04C: + strPID = "BlueSolar MPPT 75|10"; + break; + case 0XA04D: + strPID = "BlueSolar MPPT 150|45"; + break; + case 0XA04E: + strPID = "BlueSolar MPPT 150|60"; + break; + case 0XA04F: + strPID = "BlueSolar MPPT 150|85"; + break; + case 0XA050: + strPID = "SmartSolar MPPT 250|100"; + break; + case 0XA051: + strPID = "SmartSolar MPPT 150|100"; + break; + case 0XA052: + strPID = "SmartSolar MPPT 150|85"; + break; + case 0XA053: + strPID = "SmartSolar MPPT 75|15"; + break; + case 0XA054: + strPID = "SmartSolar MPPT 75|10"; + break; + case 0XA055: + strPID = "SmartSolar MPPT 100|15"; + break; + case 0XA056: + strPID = "SmartSolar MPPT 100|30"; + break; + case 0XA057: + strPID = "SmartSolar MPPT 100|50"; + break; + case 0XA058: + strPID = "SmartSolar MPPT 100|35"; + break; + case 0XA059: + strPID = "SmartSolar MPPT 150|10 rev2"; + break; + case 0XA05A: + strPID = "SmartSolar MPPT 150|85 rev2"; + break; + case 0XA05B: + strPID = "SmartSolar MPPT 250|70"; + break; + case 0XA05C: + strPID = "SmartSolar MPPT 250|85"; + break; + case 0XA05D: + strPID = "SmartSolar MPPT 250|60"; + break; + case 0XA05E: + strPID = "SmartSolar MPPT 250|45"; + break; + case 0XA05F: + strPID = "SmartSolar MPPT 100|20"; + break; + case 0XA060: + strPID = "SmartSolar MPPT 100|20 48V"; + break; + case 0XA061: + strPID = "SmartSolar MPPT 150|45"; + break; + case 0XA062: + strPID = "SmartSolar MPPT 150|60"; + break; + case 0XA063: + strPID = "SmartSolar MPPT 150|70"; + break; + case 0XA064: + strPID = "SmartSolar MPPT 250|85 rev2"; + break; + case 0XA065: + strPID = "SmartSolar MPPT 250|100 rev2"; + break; + case 0XA066: + strPID = "BlueSolar MPPT 100|20"; + break; + case 0XA067: + strPID = "BlueSolar MPPT 100|20 48V"; + break; + case 0XA068: + strPID = "SmartSolar MPPT 250|60 rev2"; + break; + case 0XA069: + strPID = "SmartSolar MPPT 250|70 rev2"; + break; + case 0XA06A: + strPID = "SmartSolar MPPT 150|45 rev2"; + break; + case 0XA06B: + strPID = "SmartSolar MPPT 150|60 rev2"; + break; + case 0XA06C: + strPID = "SmartSolar MPPT 150|70 rev2"; + break; + case 0XA06D: + strPID = "SmartSolar MPPT 150|85 rev3"; + break; + case 0XA06E: + strPID = "SmartSolar MPPT 150|100 rev3"; + break; + case 0XA06F: + strPID = "BlueSolar MPPT 150|45 rev2"; + break; + case 0XA070: + strPID = "BlueSolar MPPT 150|60 rev2"; + break; + case 0XA071: + strPID = "BlueSolar MPPT 150|70 rev2"; + break; + case 0XA102: + strPID = "SmartSolar MPPT VE.Can 150|70"; + break; + case 0XA103: + strPID = "SmartSolar MPPT VE.Can 150|45"; + break; + case 0XA104: + strPID = "SmartSolar MPPT VE.Can 150|60"; + break; + case 0XA105: + strPID = "SmartSolar MPPT VE.Can 150|85"; + break; + case 0XA106: + strPID = "SmartSolar MPPT VE.Can 150|100"; + break; + case 0XA107: + strPID = "SmartSolar MPPT VE.Can 250|45"; + break; + case 0XA108: + strPID = "SmartSolar MPPT VE.Can 250|60"; + break; + case 0XA109: + strPID = "SmartSolar MPPT VE.Can 250|80"; + break; + case 0XA10A: + strPID = "SmartSolar MPPT VE.Can 250|85"; + break; + case 0XA10B: + strPID = "SmartSolar MPPT VE.Can 250|100"; + break; + case 0XA10C: + strPID = "SmartSolar MPPT VE.Can 150|70 rev2"; + break; + case 0XA10D: + strPID = "SmartSolar MPPT VE.Can 150|85 rev2"; + break; + case 0XA10E: + strPID = "SmartSolar MPPT VE.Can 150|100 rev2"; + break; + case 0XA10F: + strPID = "BlueSolar MPPT VE.Can 150|100"; + break; + case 0XA112: + strPID = "BlueSolar MPPT VE.Can 250|70"; + break; + case 0XA113: + strPID = "BlueSolar MPPT VE.Can 250|100"; + break; + case 0XA114: + strPID = "SmartSolar MPPT VE.Can 250|70 rev2"; + break; + case 0XA115: + strPID = "SmartSolar MPPT VE.Can 250|100 rev2"; + break; + case 0XA116: + strPID = "SmartSolar MPPT VE.Can 250|85 rev2"; + break; + default: + strPID = pid; + } + return strPID; +} + +/* + * getCsAsString + * This function returns the state of operations (CS) as readable text. + */ +String VeDirectFrameHandler::getCsAsString(const char* cs) +{ + String strCS =""; + + int iCS = atoi(cs); + switch(iCS) { + 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 + * This function returns error state (ERR) as readable text. + */ +String VeDirectFrameHandler::getErrAsString(const char* err) +{ + String strERR =""; + + int iERR = atoi(err); + switch(iERR) { + case 0: + strERR = "No error"; + break; + case 2: + strERR = "Battery voltage too high"; + break; + case 17: + strERR = "Charger temperature too high"; + break; + case 18: + strERR = "Charger over current"; + break; + case 19: + strERR = "Charger current reversed"; + break; + case 20: + strERR = "Bulk time limit exceeded"; + break; + case 21: + strERR = "Current sensor issue(sensor bias/sensor broken)"; + break; + case 26: + strERR = "Terminals overheated"; + break; + case 28: + strERR = "Converter issue (dual converter models only)"; + break; + case 33: + strERR = "Input voltage too high (solar panel)"; + break; + case 34: + strERR = "Input current too high (solar panel)"; + break; + case 38: + strERR = "Input shutdown (due to excessive battery voltage)"; + break; + case 39: + strERR = "Input shutdown (due to current flow during off mode)"; + break; + case 40: + strERR = "Input"; + break; + case 65: + strERR = "39Lost communication with one of devices"; + break; + case 67: + strERR = "Synchronisedcharging device configuration issue"; + break; + case 68: + strERR = "BMS connection lost"; + break; + case 116: + strERR = "Factory calibration data lost"; + break; + case 117: + strERR = "Invalid/incompatible firmware"; + break; + case 118: + strERR = "User settings invalid"; + break; + default: + strERR = err; + } + return strERR; +} + +/* + * getOrAsString + * This function returns the off reason (OR) as readable text. + */ +String VeDirectFrameHandler::getOrAsString(const char* offReason) +{ + String strOR =""; + + long lOR = strtol(offReason, nullptr, 0); + switch(lOR) { + 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(const char* mppt) +{ + String strMPPT =""; + + int iMPPT = atoi(mppt); + switch(iMPPT) { + 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; } \ No newline at end of file diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHandler.h b/lib/VeDirectFrameHandler/VeDirectFrameHandler.h index 91a920c8..13f1817d 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHandler.h +++ b/lib/VeDirectFrameHandler/VeDirectFrameHandler.h @@ -31,6 +31,13 @@ public: VeDirectFrameHandler(); void init(); void loop(); + uint32_t getLastUpdate(); + void setLastUpdate(); + String getPidAsString(const char* pid); + String getCsAsString(const char* pid); + String getErrAsString(const char* err); + String getOrAsString(const char* offReason); + String getMpptAsString(const char* mppt); char veName[buffLen][nameLen] = { }; // public buffer for received names char veValue[buffLen][valueLen] = { }; // public buffer for received values @@ -66,6 +73,7 @@ private: void frameEndEvent(bool); void logE(const char *, const char *); bool hexRxEvent(uint8_t); + uint32_t _lastUpdate = 0; }; extern VeDirectFrameHandler VeDirect; diff --git a/src/WebApi.cpp b/src/WebApi.cpp index 4b176e89..9d5f32ba 100644 --- a/src/WebApi.cpp +++ b/src/WebApi.cpp @@ -30,6 +30,7 @@ void WebApiClass::init() _webApiSysstatus.init(&_server); _webApiWebapp.init(&_server); _webApiWsLive.init(&_server); + _webApiWsVedirectLive.init(&_server); _webApiVedirect.init(&_server); _server.begin(); @@ -48,6 +49,7 @@ void WebApiClass::loop() _webApiSysstatus.loop(); _webApiWebapp.loop(); _webApiWsLive.loop(); + _webApiWsVedirectLive.loop(); _webApiVedirect.loop(); } diff --git a/src/WebApi_ws_vedirect_live.cpp b/src/WebApi_ws_vedirect_live.cpp new file mode 100644 index 00000000..b84e653b --- /dev/null +++ b/src/WebApi_ws_vedirect_live.cpp @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ +#include "WebApi_ws_vedirect_live.h" +#include "AsyncJson.h" +#include "Configuration.h" + +WebApiWsVedirectLiveClass::WebApiWsVedirectLiveClass() + : _ws("/vedirectlivedata") +{ +} + +void WebApiWsVedirectLiveClass::init(AsyncWebServer* server) +{ + using namespace std::placeholders; + + _server = server; + _server->on("/api/vedirectlivedata/status", HTTP_GET, std::bind(&WebApiWsVedirectLiveClass::onLivedataStatus, this, _1)); + + _server->addHandler(&_ws); + _ws.onEvent(std::bind(&WebApiWsVedirectLiveClass::onWebsocketEvent, this, _1, _2, _3, _4, _5, _6)); +} + +void WebApiWsVedirectLiveClass::loop() +{ + // see: https://github.com/me-no-dev/ESPAsyncWebServer#limiting-the-number-of-web-socket-clients + if (millis() - _lastWsCleanup > 1000) { + _ws.cleanupClients(); + _lastWsCleanup = millis(); + } + + // do nothing if no WS client is connected + if (_ws.count() == 0) { + return; + } + + if (millis() - _lastVedirectUpdateCheck < 1000) { + return; + } + _lastVedirectUpdateCheck = millis(); + + uint32_t maxTimeStamp = 0; + if (VeDirect.getLastUpdate() > maxTimeStamp) { + maxTimeStamp = VeDirect.getLastUpdate(); + } + + // Update on ve.direct change or at least after 10 seconds + if (millis() - _lastWsPublish > (10 * 1000) || (maxTimeStamp != _newestVedirectTimestamp)) { + + DynamicJsonDocument root(40960); + JsonVariant var = root; + generateJsonResponse(var); + + size_t len = measureJson(root); + AsyncWebSocketMessageBuffer* buffer = _ws.makeBuffer(len); // creates a buffer (len + 1) for you. + if (buffer) { + serializeJson(root, (char*)buffer->get(), len + 1); + _ws.textAll(buffer); + } + + _lastWsPublish = millis(); + } +} + +void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root) +{ + for ( int i = 0; i < VeDirect.veEnd; i++ ) { + if(strcmp(VeDirect.veName[i], "PID") == 0) { + root[F(VeDirect.veName[i])] = VeDirect.getPidAsString(VeDirect.veValue[i]); + } + else if(strcmp(VeDirect.veName[i], "CS") == 0) { + root[F(VeDirect.veName[i])] = VeDirect.getCsAsString(VeDirect.veValue[i]); + } + else if(strcmp(VeDirect.veName[i], "ERR") == 0) { + root[F(VeDirect.veName[i])] = VeDirect.getErrAsString(VeDirect.veValue[i]); + } + else if(strcmp(VeDirect.veName[i], "OR") == 0) { + root[F(VeDirect.veName[i])] = VeDirect.getOrAsString(VeDirect.veValue[i]); + } + else if(strcmp(VeDirect.veName[i], "MPPT") == 0) { + root[F(VeDirect.veName[i])] = VeDirect.getMpptAsString(VeDirect.veValue[i]); + } + else if((strcmp(VeDirect.veName[i], "V") == 0) || (strcmp(VeDirect.veName[i], "VPV") == 0)) { + root[F(VeDirect.veName[i])]["v"] = round(std::stod(VeDirect.veValue[i]) / 10.0) / 100.0; + root[F(VeDirect.veName[i])]["u"] = "V"; + } + else if(strcmp(VeDirect.veName[i], "I") == 0) { + root[F(VeDirect.veName[i])]["v"] = round(std::stod(VeDirect.veValue[i]) / 10.0) / 100.0; + root[F(VeDirect.veName[i])]["u"] = "A"; + } + else if((strcmp(VeDirect.veName[i], "PPV") == 0) || (strcmp(VeDirect.veName[i], "H21") == 0) || (strcmp(VeDirect.veName[i], "H23") == 0)){ + root[F(VeDirect.veName[i])]["v"] = std::stoi(VeDirect.veValue[i]); + root[F(VeDirect.veName[i])]["u"] = "W"; + } + else if((strcmp(VeDirect.veName[i], "H19") == 0) || (strcmp(VeDirect.veName[i], "H20") == 0) || (strcmp(VeDirect.veName[i], "H22") == 0)){ + root[F(VeDirect.veName[i])]["v"] = std::stod(VeDirect.veValue[i]) / 100.0; + root[F(VeDirect.veName[i])]["u"] = "kWh"; + } + else if(strcmp(VeDirect.veName[i], "HSDS") == 0){ + root[F(VeDirect.veName[i])]["v"] = std::stoi(VeDirect.veValue[i]); + root[F(VeDirect.veName[i])]["u"] = "Days"; + } + else { + root[F(VeDirect.veName[i])] = VeDirect.veValue[i]; + } + } + + if (VeDirect.getLastUpdate() > _newestVedirectTimestamp) { + _newestVedirectTimestamp = VeDirect.getLastUpdate(); + } +} + +void WebApiWsVedirectLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len) +{ + if (type == WS_EVT_CONNECT) { + char str[64]; + sprintf(str, "Websocket: [%s][%u] connect", server->url(), client->id()); + Serial.println(str); + } else if (type == WS_EVT_DISCONNECT) { + char str[64]; + sprintf(str, "Websocket: [%s][%u] disconnect", server->url(), client->id()); + Serial.println(str); + } +} + +void WebApiWsVedirectLiveClass::onLivedataStatus(AsyncWebServerRequest* request) +{ + AsyncJsonResponse* response = new AsyncJsonResponse(false, 40960U); + JsonVariant root = response->getRoot().as(); + generateJsonResponse(root); + + response->setLength(); + request->send(response); +} \ No newline at end of file