From 59c84bcb85d09ef1eaae984bc382760294ab88fa Mon Sep 17 00:00:00 2001 From: MalteSchm Date: Sun, 2 Apr 2023 13:00:46 +0200 Subject: [PATCH] Webapi and websocket api for Battery --- include/WebApi.h | 4 + include/WebApi_Pylontech.h | 17 ++++ include/WebApi_ws_Pylontech.h | 23 ++++++ src/WebApi.cpp | 4 + src/WebApi_Pylontech.cpp | 87 ++++++++++++++++++++ src/WebApi_ws_Pylontech.cpp | 146 ++++++++++++++++++++++++++++++++++ 6 files changed, 281 insertions(+) create mode 100644 include/WebApi_Pylontech.h create mode 100644 include/WebApi_ws_Pylontech.h create mode 100644 src/WebApi_Pylontech.cpp create mode 100644 src/WebApi_ws_Pylontech.cpp diff --git a/include/WebApi.h b/include/WebApi.h index 1449332d..e3af5cf6 100644 --- a/include/WebApi.h +++ b/include/WebApi.h @@ -27,6 +27,8 @@ #include "WebApi_vedirect.h" #include "WebApi_ws_Huawei.h" #include "WebApi_Huawei.h" +#include "WebApi_ws_Pylontech.h" +#include "WebApi_Pylontech.h" #include class WebApiClass { @@ -70,6 +72,8 @@ private: WebApiVedirectClass _webApiVedirect; WebApiHuaweiClass _webApiHuaweiClass; WebApiWsHuaweiLiveClass _webApiWsHuaweiLive; + WebApiPylontechClass _webApiPylontechClass; + WebApiWsPylontechLiveClass _webApiWsPylontechLive; }; diff --git a/include/WebApi_Pylontech.h b/include/WebApi_Pylontech.h new file mode 100644 index 00000000..26f61e26 --- /dev/null +++ b/include/WebApi_Pylontech.h @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include + +class WebApiPylontechClass { +public: + void init(AsyncWebServer* server); + void loop(); + void getJsonData(JsonObject& root); + +private: + void onStatus(AsyncWebServerRequest* request); + + AsyncWebServer* _server; +}; \ No newline at end of file diff --git a/include/WebApi_ws_Pylontech.h b/include/WebApi_ws_Pylontech.h new file mode 100644 index 00000000..9651d2fb --- /dev/null +++ b/include/WebApi_ws_Pylontech.h @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "ArduinoJson.h" +#include + +class WebApiWsPylontechLiveClass { +public: + WebApiWsPylontechLiveClass(); + 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 _lastWsCleanup = 0; + uint32_t _lastUpdateCheck = 0; +}; \ No newline at end of file diff --git a/src/WebApi.cpp b/src/WebApi.cpp index 4e5fb4a2..161558e4 100644 --- a/src/WebApi.cpp +++ b/src/WebApi.cpp @@ -43,6 +43,8 @@ void WebApiClass::init() _webApiVedirect.init(&_server); _webApiWsHuaweiLive.init(&_server); _webApiHuaweiClass.init(&_server); + _webApiWsPylontechLive.init(&_server); + _webApiPylontechClass.init(&_server); _server.begin(); } @@ -74,6 +76,8 @@ void WebApiClass::loop() _webApiVedirect.loop(); _webApiWsHuaweiLive.loop(); _webApiHuaweiClass.loop(); + _webApiWsPylontechLive.loop(); + _webApiPylontechClass.loop(); } bool WebApiClass::checkCredentials(AsyncWebServerRequest* request) diff --git a/src/WebApi_Pylontech.cpp b/src/WebApi_Pylontech.cpp new file mode 100644 index 00000000..d5338e57 --- /dev/null +++ b/src/WebApi_Pylontech.cpp @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ +#include "WebApi_Pylontech.h" +#include "Battery.h" +#include "Configuration.h" +#include "MessageOutput.h" +#include "WebApi.h" +#include "WebApi_errors.h" +#include +#include + +void WebApiPylontechClass::init(AsyncWebServer* server) +{ + using std::placeholders::_1; + + _server = server; + + _server->on("/api/battery/livedata", HTTP_GET, std::bind(&WebApiPylontechClass::onStatus, this, _1)); +} + +void WebApiPylontechClass::loop() +{ +} + +void WebApiPylontechClass::getJsonData(JsonObject& root) { + + root["data_age"] = (millis() - Battery.lastUpdate) / 1000; + + root[F("chargeVoltage")]["v"] = Battery.chargeVoltage ; + root[F("chargeVoltage")]["u"] = "V"; + root[F("chargeCurrentLimitation")]["v"] = Battery.chargeCurrentLimitation ; + root[F("chargeCurrentLimitation")]["u"] = "A"; + root[F("dischargeCurrentLimitation")]["v"] = Battery.dischargeCurrentLimitation ; + root[F("dischargeCurrentLimitation")]["u"] = "A"; + root[F("stateOfCharge")]["v"] = Battery.stateOfCharge ; + root[F("stateOfCharge")]["u"] = "%"; + root[F("stateOfHealth")]["v"] = Battery.stateOfHealth ; + root[F("stateOfHealth")]["u"] = "%"; + root[F("voltage")]["v"] = Battery.voltage; + root[F("voltage")]["u"] = "V"; + root[F("current")]["v"] = Battery.current ; + root[F("current")]["u"] = "A"; + root[F("temperature")]["v"] = Battery.temperature ; + root[F("temperature")]["u"] = "°C"; + + // Alarms + root["alarms"][F("dischargeCurrent")] = Battery.alarmOverCurrentDischarge ; + root["alarms"][F("chargeCurrent")] = Battery.alarmOverCurrentCharge ; + root["alarms"][F("lowTemperature")] = Battery.alarmUnderTemperature ; + root["alarms"][F("highTemperature")] = Battery.alarmOverTemperature ; + root["alarms"][F("lowVoltage")] = Battery.alarmUnderVoltage ; + root["alarms"][F("highVoltage")] = Battery.alarmOverVoltage ; + root["alarms"][F("bmsInternal")] = Battery.alarmBmsInternal ; + + // Warnings + root["warnings"][F("dischargeCurrent")] = Battery.warningHighCurrentDischarge ; + root["warnings"][F("chargeCurrent")] = Battery.warningHighCurrentCharge ; + root["warnings"][F("lowTemperature")] = Battery.warningLowTemperature ; + root["warnings"][F("highTemperature")] = Battery.warningHighTemperature ; + root["warnings"][F("lowVoltage")] = Battery.warningLowVoltage ; + root["warnings"][F("highVoltage")] = Battery.warningHighVoltage ; + root["warnings"][F("bmsInternal")] = Battery.warningBmsInternal ; + + // Misc + root[F("manufacturer")] = Battery.manufacturer ; + root[F("chargeEnabled")] = Battery.chargeEnabled ; + root[F("dischargeEnabled")] = Battery.dischargeEnabled ; + root[F("chargeImmediately")] = Battery.chargeImmediately ; + +} + +void WebApiPylontechClass::onStatus(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentialsReadonly(request)) { + return; + } + + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonObject root = response->getRoot(); + getJsonData(root); + + response->setLength(); + request->send(response); +} + diff --git a/src/WebApi_ws_Pylontech.cpp b/src/WebApi_ws_Pylontech.cpp new file mode 100644 index 00000000..eb1aa90c --- /dev/null +++ b/src/WebApi_ws_Pylontech.cpp @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ +#include "WebApi_ws_Pylontech.h" +#include "AsyncJson.h" +#include "Configuration.h" +#include "Battery.h" +#include "MessageOutput.h" +#include "WebApi.h" +#include "defaults.h" + +WebApiWsPylontechLiveClass::WebApiWsPylontechLiveClass() + : _ws("/Pylontechlivedata") +{ +} + +void WebApiWsPylontechLiveClass::init(AsyncWebServer* server) +{ + using std::placeholders::_1; + using std::placeholders::_2; + using std::placeholders::_3; + using std::placeholders::_4; + using std::placeholders::_5; + using std::placeholders::_6; + + _server = server; + _server->on("/api/batterylivedata/status", HTTP_GET, std::bind(&WebApiWsPylontechLiveClass::onLivedataStatus, this, _1)); + + _server->addHandler(&_ws); + _ws.onEvent(std::bind(&WebApiWsPylontechLiveClass::onWebsocketEvent, this, _1, _2, _3, _4, _5, _6)); +} + +void WebApiWsPylontechLiveClass::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() - _lastUpdateCheck < 1000) { + return; + } + _lastUpdateCheck = millis(); + + DynamicJsonDocument root(1024); + JsonVariant var = root; + generateJsonResponse(var); + + String buffer; + if (buffer) { + serializeJson(root, buffer); + + if (Configuration.get().Security_AllowReadonly) { + _ws.setAuthentication("", ""); + } else { + _ws.setAuthentication(AUTH_USERNAME, Configuration.get().Security_Password); + } + + _ws.textAll(buffer); + } + + +} + +void WebApiWsPylontechLiveClass::generateJsonResponse(JsonVariant& root) +{ + root["data_age"] = (millis() - Battery.lastUpdate) / 1000; + + root[F("chargeVoltage")]["v"] = Battery.chargeVoltage ; + root[F("chargeVoltage")]["u"] = "V"; + root[F("chargeCurrentLimitation")]["v"] = Battery.chargeCurrentLimitation ; + root[F("chargeCurrentLimitation")]["u"] = "A"; + root[F("dischargeCurrentLimitation")]["v"] = Battery.dischargeCurrentLimitation ; + root[F("dischargeCurrentLimitation")]["u"] = "A"; + root[F("stateOfCharge")]["v"] = Battery.stateOfCharge ; + root[F("stateOfCharge")]["u"] = "%"; + root[F("stateOfHealth")]["v"] = Battery.stateOfHealth ; + root[F("stateOfHealth")]["u"] = "%"; + root[F("voltage")]["v"] = Battery.voltage; + root[F("voltage")]["u"] = "V"; + root[F("current")]["v"] = Battery.current ; + root[F("current")]["u"] = "A"; + root[F("temperature")]["v"] = Battery.temperature ; + root[F("temperature")]["u"] = "°C"; + + // Alarms + root["alarms"][F("dischargeCurrent")] = Battery.alarmOverCurrentDischarge ; + root["alarms"][F("chargeCurrent")] = Battery.alarmOverCurrentCharge ; + root["alarms"][F("lowTemperature")] = Battery.alarmUnderTemperature ; + root["alarms"][F("highTemperature")] = Battery.alarmOverTemperature ; + root["alarms"][F("lowVoltage")] = Battery.alarmUnderVoltage ; + root["alarms"][F("highVoltage")] = Battery.alarmOverVoltage ; + root["alarms"][F("bmsInternal")] = Battery.alarmBmsInternal ; + + // Warnings + root["warnings"][F("dischargeCurrent")] = Battery.warningHighCurrentDischarge ; + root["warnings"][F("chargeCurrent")] = Battery.warningHighCurrentCharge ; + root["warnings"][F("lowTemperature")] = Battery.warningLowTemperature ; + root["warnings"][F("highTemperature")] = Battery.warningHighTemperature ; + root["warnings"][F("lowVoltage")] = Battery.warningLowVoltage ; + root["warnings"][F("highVoltage")] = Battery.warningHighVoltage ; + root["warnings"][F("bmsInternal")] = Battery.warningBmsInternal ; + + // Misc + root[F("manufacturer")] = Battery.manufacturer ; + root[F("chargeEnabled")] = Battery.chargeEnabled ; + root[F("dischargeEnabled")] = Battery.dischargeEnabled ; + root[F("chargeImmediately")] = Battery.chargeImmediately ; + + +} + +void WebApiWsPylontechLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len) +{ + if (type == WS_EVT_CONNECT) { + char str[64]; + snprintf(str, sizeof(str), "Websocket: [%s][%u] connect", server->url(), client->id()); + Serial.println(str); + MessageOutput.println(str); + } else if (type == WS_EVT_DISCONNECT) { + char str[64]; + snprintf(str, sizeof(str), "Websocket: [%s][%u] disconnect", server->url(), client->id()); + Serial.println(str); + MessageOutput.println(str); + } +} + +void WebApiWsPylontechLiveClass::onLivedataStatus(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentialsReadonly(request)) { + return; + } + AsyncJsonResponse* response = new AsyncJsonResponse(false, 1024U); + JsonVariant root = response->getRoot().as(); + generateJsonResponse(root); + + response->setLength(); + request->send(response); +} \ No newline at end of file