From 92abfdfaa6859d11a8fdd7d43298925f4236bbec Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Wed, 15 Jun 2022 23:46:22 +0200 Subject: [PATCH] Separated WebApi into different files --- include/WebApi.h | 41 +- include/WebApi_dtu.h | 15 + include/WebApi_firmware.h | 15 + include/WebApi_inverter.h | 17 + include/WebApi_mqtt.h | 16 + include/WebApi_network.h | 16 + include/WebApi_ntp.h | 16 + include/WebApi_sysstatus.h | 14 + include/WebApi_webapp.h | 12 + src/WebApi.cpp | 973 +------------------------------------ src/WebApi_dtu.cpp | 117 +++++ src/WebApi_firmware.cpp | 71 +++ src/WebApi_inverter.cpp | 267 ++++++++++ src/WebApi_mqtt.cpp | 178 +++++++ src/WebApi_network.cpp | 200 ++++++++ src/WebApi_ntp.cpp | 135 +++++ src/WebApi_sysstatus.cpp | 59 +++ src/WebApi_webapp.cpp | 51 ++ 18 files changed, 1233 insertions(+), 980 deletions(-) create mode 100644 include/WebApi_dtu.h create mode 100644 include/WebApi_firmware.h create mode 100644 include/WebApi_inverter.h create mode 100644 include/WebApi_mqtt.h create mode 100644 include/WebApi_network.h create mode 100644 include/WebApi_ntp.h create mode 100644 include/WebApi_sysstatus.h create mode 100644 include/WebApi_webapp.h create mode 100644 src/WebApi_dtu.cpp create mode 100644 src/WebApi_firmware.cpp create mode 100644 src/WebApi_inverter.cpp create mode 100644 src/WebApi_mqtt.cpp create mode 100644 src/WebApi_network.cpp create mode 100644 src/WebApi_ntp.cpp create mode 100644 src/WebApi_sysstatus.cpp create mode 100644 src/WebApi_webapp.cpp diff --git a/include/WebApi.h b/include/WebApi.h index be0b241..34f0036 100644 --- a/include/WebApi.h +++ b/include/WebApi.h @@ -1,5 +1,13 @@ #pragma once +#include "WebApi_dtu.h" +#include "WebApi_firmware.h" +#include "WebApi_inverter.h" +#include "WebApi_mqtt.h" +#include "WebApi_network.h" +#include "WebApi_ntp.h" +#include "WebApi_sysstatus.h" +#include "WebApi_webapp.h" #include class WebApiClass { @@ -13,33 +21,18 @@ private: AsyncWebSocket _ws; AsyncEventSource _events; + WebApiDtuClass _webApiDtu; + WebApiFirmwareClass _webApiFirmware; + WebApiInverterClass _webApiInverter; + WebApiMqttClass _webApiMqtt; + WebApiNetworkClass _webApiNetwork; + WebApiNtpClass _webApiNtp; + WebApiSysstatusClass _webApiSysstatus; + WebApiWebappClass _webApiWebapp; + void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len); void onNotFound(AsyncWebServerRequest* request); - void onSystemStatus(AsyncWebServerRequest* request); - - void onNetworkStatus(AsyncWebServerRequest* request); - void onNetworkAdminGet(AsyncWebServerRequest* request); - void onNetworkAdminPost(AsyncWebServerRequest* request); - - void onNtpStatus(AsyncWebServerRequest* request); - void onNtpAdminGet(AsyncWebServerRequest* request); - void onNtpAdminPost(AsyncWebServerRequest* request); - - void onMqttStatus(AsyncWebServerRequest* request); - void onMqttAdminGet(AsyncWebServerRequest* request); - void onMqttAdminPost(AsyncWebServerRequest* request); - - void onInverterList(AsyncWebServerRequest* request); - void onInverterAdd(AsyncWebServerRequest* request); - void onInverterEdit(AsyncWebServerRequest* request); - void onInverterDelete(AsyncWebServerRequest* request); - - void onDtuAdminGet(AsyncWebServerRequest* request); - void onDtuAdminPost(AsyncWebServerRequest* request); - - void onFirmwareUpdateFinish(AsyncWebServerRequest* request); - void onFirmwareUpdateUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t *data, size_t len, bool final); }; extern WebApiClass WebApi; \ No newline at end of file diff --git a/include/WebApi_dtu.h b/include/WebApi_dtu.h new file mode 100644 index 0000000..0d31ae4 --- /dev/null +++ b/include/WebApi_dtu.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +class WebApiDtuClass { +public: + void init(AsyncWebServer* server); + void loop(); + +private: + void onDtuAdminGet(AsyncWebServerRequest* request); + void onDtuAdminPost(AsyncWebServerRequest* request); + + AsyncWebServer* _server; +}; \ No newline at end of file diff --git a/include/WebApi_firmware.h b/include/WebApi_firmware.h new file mode 100644 index 0000000..45718ea --- /dev/null +++ b/include/WebApi_firmware.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +class WebApiFirmwareClass { +public: + void init(AsyncWebServer* server); + void loop(); + +private: + void onFirmwareUpdateFinish(AsyncWebServerRequest* request); + void onFirmwareUpdateUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final); + + AsyncWebServer* _server; +}; \ No newline at end of file diff --git a/include/WebApi_inverter.h b/include/WebApi_inverter.h new file mode 100644 index 0000000..3ea1585 --- /dev/null +++ b/include/WebApi_inverter.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +class WebApiInverterClass { +public: + void init(AsyncWebServer* server); + void loop(); + +private: + void onInverterList(AsyncWebServerRequest* request); + void onInverterAdd(AsyncWebServerRequest* request); + void onInverterEdit(AsyncWebServerRequest* request); + void onInverterDelete(AsyncWebServerRequest* request); + + AsyncWebServer* _server; +}; \ No newline at end of file diff --git a/include/WebApi_mqtt.h b/include/WebApi_mqtt.h new file mode 100644 index 0000000..8bdb897 --- /dev/null +++ b/include/WebApi_mqtt.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +class WebApiMqttClass { +public: + void init(AsyncWebServer* server); + void loop(); + +private: + void onMqttStatus(AsyncWebServerRequest* request); + void onMqttAdminGet(AsyncWebServerRequest* request); + void onMqttAdminPost(AsyncWebServerRequest* request); + + AsyncWebServer* _server; +}; \ No newline at end of file diff --git a/include/WebApi_network.h b/include/WebApi_network.h new file mode 100644 index 0000000..9c848bd --- /dev/null +++ b/include/WebApi_network.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +class WebApiNetworkClass { +public: + void init(AsyncWebServer* server); + void loop(); + +private: + void onNetworkStatus(AsyncWebServerRequest* request); + void onNetworkAdminGet(AsyncWebServerRequest* request); + void onNetworkAdminPost(AsyncWebServerRequest* request); + + AsyncWebServer* _server; +}; \ No newline at end of file diff --git a/include/WebApi_ntp.h b/include/WebApi_ntp.h new file mode 100644 index 0000000..f8155f0 --- /dev/null +++ b/include/WebApi_ntp.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +class WebApiNtpClass { +public: + void init(AsyncWebServer* server); + void loop(); + +private: + void onNtpStatus(AsyncWebServerRequest* request); + void onNtpAdminGet(AsyncWebServerRequest* request); + void onNtpAdminPost(AsyncWebServerRequest* request); + + AsyncWebServer* _server; +}; \ No newline at end of file diff --git a/include/WebApi_sysstatus.h b/include/WebApi_sysstatus.h new file mode 100644 index 0000000..a9b5a50 --- /dev/null +++ b/include/WebApi_sysstatus.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +class WebApiSysstatusClass { +public: + void init(AsyncWebServer* server); + void loop(); + +private: + void onSystemStatus(AsyncWebServerRequest* request); + + AsyncWebServer* _server; +}; \ No newline at end of file diff --git a/include/WebApi_webapp.h b/include/WebApi_webapp.h new file mode 100644 index 0000000..1dfc1f3 --- /dev/null +++ b/include/WebApi_webapp.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +class WebApiWebappClass { +public: + void init(AsyncWebServer* server); + void loop(); + +private: + AsyncWebServer* _server; +}; \ No newline at end of file diff --git a/src/WebApi.cpp b/src/WebApi.cpp index 84dc679..e889ceb 100644 --- a/src/WebApi.cpp +++ b/src/WebApi.cpp @@ -1,16 +1,7 @@ #include "WebApi.h" #include "ArduinoJson.h" #include "AsyncJson.h" -#include "Configuration.h" -#include "Hoymiles.h" -#include "MqttSettings.h" -#include "NtpSettings.h" -#include "Update.h" -#include "WiFiSettings.h" #include "defaults.h" -#include "helper.h" -#include -#include WebApiClass::WebApiClass() : _server(HTTP_PORT) @@ -19,16 +10,6 @@ WebApiClass::WebApiClass() { } -extern const uint8_t file_index_html_start[] asm("_binary_data_index_html_gz_start"); -extern const uint8_t file_favicon_ico_start[] asm("_binary_data_favicon_ico_start"); -extern const uint8_t file_zones_json_start[] asm("_binary_data_zones_json_gz_start"); -extern const uint8_t file_app_js_start[] asm("_binary_data_js_app_js_gz_start"); - -extern const uint8_t file_index_html_end[] asm("_binary_data_index_html_gz_end"); -extern const uint8_t file_favicon_ico_end[] asm("_binary_data_favicon_ico_end"); -extern const uint8_t file_zones_json_end[] asm("_binary_data_zones_json_gz_end"); -extern const uint8_t file_app_js_end[] asm("_binary_data_js_app_js_gz_end"); - void WebApiClass::init() { using namespace std::placeholders; @@ -38,60 +19,14 @@ void WebApiClass::init() _ws.onEvent(std::bind(&WebApiClass::onWebsocketEvent, this, _1, _2, _3, _4, _5, _6)); - _server.on("/api/system/status", HTTP_GET, std::bind(&WebApiClass::onSystemStatus, this, _1)); - - _server.on("/api/network/status", HTTP_GET, std::bind(&WebApiClass::onNetworkStatus, this, _1)); - _server.on("/api/network/config", HTTP_GET, std::bind(&WebApiClass::onNetworkAdminGet, this, _1)); - _server.on("/api/network/config", HTTP_POST, std::bind(&WebApiClass::onNetworkAdminPost, this, _1)); - - _server.on("/api/ntp/status", HTTP_GET, std::bind(&WebApiClass::onNtpStatus, this, _1)); - _server.on("/api/ntp/config", HTTP_GET, std::bind(&WebApiClass::onNtpAdminGet, this, _1)); - _server.on("/api/ntp/config", HTTP_POST, std::bind(&WebApiClass::onNtpAdminPost, this, _1)); - - _server.on("/api/mqtt/status", HTTP_GET, std::bind(&WebApiClass::onMqttStatus, this, _1)); - _server.on("/api/mqtt/config", HTTP_GET, std::bind(&WebApiClass::onMqttAdminGet, this, _1)); - _server.on("/api/mqtt/config", HTTP_POST, std::bind(&WebApiClass::onMqttAdminPost, this, _1)); - - _server.on("/api/inverter/list", HTTP_GET, std::bind(&WebApiClass::onInverterList, this, _1)); - _server.on("/api/inverter/add", HTTP_POST, std::bind(&WebApiClass::onInverterAdd, this, _1)); - _server.on("/api/inverter/edit", HTTP_POST, std::bind(&WebApiClass::onInverterEdit, this, _1)); - _server.on("/api/inverter/del", HTTP_POST, std::bind(&WebApiClass::onInverterDelete, this, _1)); - - _server.on("/api/dtu/config", HTTP_GET, std::bind(&WebApiClass::onDtuAdminGet, this, _1)); - _server.on("/api/dtu/config", HTTP_POST, std::bind(&WebApiClass::onDtuAdminPost, this, _1)); - - _server.on("/api/firmware/update", HTTP_POST, - std::bind(&WebApiClass::onFirmwareUpdateFinish, this, _1), - std::bind(&WebApiClass::onFirmwareUpdateUpload, this, _1, _2, _3, _4, _5, _6)); - - _server.on("/", HTTP_GET, [](AsyncWebServerRequest* request) { - AsyncWebServerResponse* response = request->beginResponse_P(200, "text/html", file_index_html_start, file_index_html_end - file_index_html_start); - response->addHeader("Content-Encoding", "gzip"); - request->send(response); - }); - - _server.on("/index.html", HTTP_GET, [](AsyncWebServerRequest* request) { - AsyncWebServerResponse* response = request->beginResponse_P(200, "text/html", file_index_html_start, file_index_html_end - file_index_html_start); - response->addHeader("Content-Encoding", "gzip"); - request->send(response); - }); - - _server.on("/favicon.ico", HTTP_GET, [](AsyncWebServerRequest* request) { - AsyncWebServerResponse* response = request->beginResponse_P(200, "image/x-icon", file_favicon_ico_start, file_favicon_ico_end - file_favicon_ico_start); - request->send(response); - }); - - _server.on("/zones.json", HTTP_GET, [](AsyncWebServerRequest* request) { - AsyncWebServerResponse* response = request->beginResponse_P(200, "application/json", file_zones_json_start, file_zones_json_end - file_zones_json_start); - response->addHeader("Content-Encoding", "gzip"); - request->send(response); - }); - - _server.on("/js/app.js", HTTP_GET, [](AsyncWebServerRequest* request) { - AsyncWebServerResponse* response = request->beginResponse_P(200, "text/javascript", file_app_js_start, file_app_js_end - file_app_js_start); - response->addHeader("Content-Encoding", "gzip"); - request->send(response); - }); + _webApiDtu.init(&_server); + _webApiFirmware.init(&_server); + _webApiInverter.init(&_server); + _webApiMqtt.init(&_server); + _webApiNetwork.init(&_server); + _webApiNtp.init(&_server); + _webApiSysstatus.init(&_server); + _webApiWebapp.init(&_server); _server.onNotFound(std::bind(&WebApiClass::onNotFound, this, _1)); _server.begin(); @@ -99,6 +34,15 @@ void WebApiClass::init() void WebApiClass::loop() { + _webApiDtu.loop(); + _webApiFirmware.loop(); + _webApiInverter.loop(); + _webApiMqtt.loop(); + _webApiNetwork.loop(); + _webApiNtp.loop(); + _webApiSysstatus.loop(); + _webApiWebapp.loop(); + // see: https://github.com/me-no-dev/ESPAsyncWebServer#limiting-the-number-of-web-socket-clients _ws.cleanupClients(); } @@ -122,887 +66,4 @@ void WebApiClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* } } -void WebApiClass::onNetworkStatus(AsyncWebServerRequest* request) -{ - AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject root = response->getRoot(); - - root[F("sta_status")] = ((WiFi.getMode() & WIFI_STA) != 0); - root[F("sta_ssid")] = WiFi.SSID(); - root[F("sta_ip")] = WiFi.localIP().toString(); - root[F("sta_netmask")] = WiFi.subnetMask().toString(); - root[F("sta_gateway")] = WiFi.gatewayIP().toString(); - root[F("sta_dns1")] = WiFi.dnsIP(0).toString(); - root[F("sta_dns2")] = WiFi.dnsIP(1).toString(); - root[F("sta_mac")] = WiFi.macAddress(); - root[F("sta_rssi")] = WiFi.RSSI(); - root[F("ap_status")] = ((WiFi.getMode() & WIFI_AP) != 0); - root[F("ap_ssid")] = WiFiSettings.getApName(); - root[F("ap_ip")] = WiFi.softAPIP().toString(); - root[F("ap_mac")] = WiFi.softAPmacAddress(); - root[F("ap_stationnum")] = WiFi.softAPgetStationNum(); - - response->setLength(); - request->send(response); -} - -void WebApiClass::onSystemStatus(AsyncWebServerRequest* request) -{ - AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject root = response->getRoot(); - - root[F("hostname")] = WiFi.getHostname(); - - root[F("sdkversion")] = ESP.getSdkVersion(); - root[F("cpufreq")] = ESP.getCpuFreqMHz(); - - root[F("heap_total")] = ESP.getHeapSize(); - root[F("heap_used")] = ESP.getHeapSize() - ESP.getFreeHeap(); - root[F("sketch_total")] = ESP.getFreeSketchSpace(); - root[F("sketch_used")] = ESP.getSketchSize(); - root[F("littlefs_total")] = LittleFS.totalBytes(); - root[F("littlefs_used")] = LittleFS.usedBytes(); - - root[F("chiprevision")] = ESP.getChipRevision(); - root[F("chipmodel")] = ESP.getChipModel(); - root[F("chipcores")] = ESP.getChipCores(); - - String reason; - reason = ResetReason.get_reset_reason_verbose(0); - root[F("resetreason_0")] = reason; - - reason = ResetReason.get_reset_reason_verbose(1); - root[F("resetreason_1")] = reason; - - root[F("cfgsavecount")] = Configuration.get().Cfg_SaveCount; - - char version[16]; - sprintf(version, "%d.%d.%d", CONFIG_VERSION >> 24 & 0xff, CONFIG_VERSION >> 16 & 0xff, CONFIG_VERSION >> 8 & 0xff); - root[F("firmware_version")] = version; - - root[F("uptime")] = esp_timer_get_time() / 1000000; - - response->setLength(); - request->send(response); -} - -void WebApiClass::onNetworkAdminGet(AsyncWebServerRequest* request) -{ - AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject root = response->getRoot(); - CONFIG_T& config = Configuration.get(); - - root[F("hostname")] = config.WiFi_Hostname; - root[F("dhcp")] = config.WiFi_Dhcp; - root[F("ipaddress")] = IPAddress(config.WiFi_Ip).toString(); - root[F("netmask")] = IPAddress(config.WiFi_Netmask).toString(); - root[F("gateway")] = IPAddress(config.WiFi_Gateway).toString(); - root[F("dns1")] = IPAddress(config.WiFi_Dns1).toString(); - root[F("dns2")] = IPAddress(config.WiFi_Dns2).toString(); - root[F("ssid")] = config.WiFi_Ssid; - root[F("password")] = config.WiFi_Password; - - response->setLength(); - request->send(response); -} - -void WebApiClass::onNetworkAdminPost(AsyncWebServerRequest* request) -{ - AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject retMsg = response->getRoot(); - retMsg[F("type")] = F("warning"); - - if (!request->hasParam("data", true)) { - retMsg[F("message")] = F("No values found!"); - response->setLength(); - request->send(response); - return; - } - - String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg[F("message")] = F("Data too large!"); - response->setLength(); - request->send(response); - return; - } - - DynamicJsonDocument root(1024); - DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg[F("message")] = F("Failed to parse data!"); - response->setLength(); - request->send(response); - return; - } - - if (!(root.containsKey("ssid") && root.containsKey("password") && root.containsKey("hostname") && root.containsKey("dhcp") && root.containsKey("ipaddress") && root.containsKey("netmask") && root.containsKey("gateway") && root.containsKey("dns1") && root.containsKey("dns2"))) { - retMsg[F("message")] = F("Values are missing!"); - response->setLength(); - request->send(response); - return; - } - - IPAddress ipaddress; - if (!ipaddress.fromString(root[F("ipaddress")].as())) { - retMsg[F("message")] = F("IP address is invalid!"); - response->setLength(); - request->send(response); - return; - } - IPAddress netmask; - if (!netmask.fromString(root[F("netmask")].as())) { - retMsg[F("message")] = F("Netmask is invalid!"); - response->setLength(); - request->send(response); - return; - } - IPAddress gateway; - if (!gateway.fromString(root[F("gateway")].as())) { - retMsg[F("message")] = F("Gateway is invalid!"); - response->setLength(); - request->send(response); - return; - } - IPAddress dns1; - if (!dns1.fromString(root[F("dns1")].as())) { - retMsg[F("message")] = F("DNS Server IP 1 is invalid!"); - response->setLength(); - request->send(response); - return; - } - IPAddress dns2; - if (!dns2.fromString(root[F("dns2")].as())) { - retMsg[F("message")] = F("DNS Server IP 2 is invalid!"); - response->setLength(); - request->send(response); - return; - } - - if (root[F("hostname")].as().length() == 0 || root[F("hostname")].as().length() > WIFI_MAX_HOSTNAME_STRLEN) { - retMsg[F("message")] = F("Hostname must between 1 and " STR(WIFI_MAX_HOSTNAME_STRLEN) " characters long!"); - response->setLength(); - request->send(response); - return; - } - if (root[F("ssid")].as().length() == 0 || root[F("ssid")].as().length() > WIFI_MAX_SSID_STRLEN) { - retMsg[F("message")] = F("SSID must between 1 and " STR(WIFI_MAX_SSID_STRLEN) " characters long!"); - response->setLength(); - request->send(response); - return; - } - if (root[F("password")].as().length() > WIFI_MAX_PASSWORD_STRLEN - 1) { - retMsg[F("message")] = F("Password must not be longer than " STR(WIFI_MAX_PASSWORD_STRLEN) " characters long!"); - response->setLength(); - request->send(response); - return; - } - - CONFIG_T& config = Configuration.get(); - config.WiFi_Ip[0] = ipaddress[0]; - config.WiFi_Ip[1] = ipaddress[1]; - config.WiFi_Ip[2] = ipaddress[2]; - config.WiFi_Ip[3] = ipaddress[3]; - config.WiFi_Netmask[0] = netmask[0]; - config.WiFi_Netmask[1] = netmask[1]; - config.WiFi_Netmask[2] = netmask[2]; - config.WiFi_Netmask[3] = netmask[3]; - config.WiFi_Gateway[0] = gateway[0]; - config.WiFi_Gateway[1] = gateway[1]; - config.WiFi_Gateway[2] = gateway[2]; - config.WiFi_Gateway[3] = gateway[3]; - config.WiFi_Dns1[0] = dns1[0]; - config.WiFi_Dns1[1] = dns1[1]; - config.WiFi_Dns1[2] = dns1[2]; - config.WiFi_Dns1[3] = dns1[3]; - config.WiFi_Dns2[0] = dns2[0]; - config.WiFi_Dns2[1] = dns2[1]; - config.WiFi_Dns2[2] = dns2[2]; - config.WiFi_Dns2[3] = dns2[3]; - strcpy(config.WiFi_Ssid, root[F("ssid")].as().c_str()); - strcpy(config.WiFi_Password, root[F("password")].as().c_str()); - strcpy(config.WiFi_Hostname, root[F("hostname")].as().c_str()); - if (root[F("dhcp")].as()) { - config.WiFi_Dhcp = true; - } else { - config.WiFi_Dhcp = false; - } - Configuration.write(); - - retMsg[F("type")] = F("success"); - retMsg[F("message")] = F("Settings saved!"); - - response->setLength(); - request->send(response); - - WiFiSettings.enableAdminMode(); - WiFiSettings.applyConfig(); -} - -void WebApiClass::onNtpStatus(AsyncWebServerRequest* request) -{ - AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject root = response->getRoot(); - CONFIG_T& config = Configuration.get(); - - root[F("ntp_server")] = config.Ntp_Server; - root[F("ntp_timezone")] = config.Ntp_Timezone; - root[F("ntp_timezone_descr")] = config.Ntp_TimezoneDescr; - - struct tm timeinfo; - if (!getLocalTime(&timeinfo)) { - root[F("ntp_status")] = false; - } else { - root[F("ntp_status")] = true; - } - char timeStringBuff[50]; - strftime(timeStringBuff, sizeof(timeStringBuff), "%A, %B %d %Y %H:%M:%S", &timeinfo); - root[F("ntp_localtime")] = timeStringBuff; - - response->setLength(); - request->send(response); -} - -void WebApiClass::onNtpAdminGet(AsyncWebServerRequest* request) -{ - AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject root = response->getRoot(); - CONFIG_T& config = Configuration.get(); - - root[F("ntp_server")] = config.Ntp_Server; - root[F("ntp_timezone")] = config.Ntp_Timezone; - root[F("ntp_timezone_descr")] = config.Ntp_TimezoneDescr; - - response->setLength(); - request->send(response); -} - -void WebApiClass::onNtpAdminPost(AsyncWebServerRequest* request) -{ - AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject retMsg = response->getRoot(); - retMsg[F("type")] = F("warning"); - - if (!request->hasParam("data", true)) { - retMsg[F("message")] = F("No values found!"); - response->setLength(); - request->send(response); - return; - } - - String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg[F("message")] = F("Data too large!"); - response->setLength(); - request->send(response); - return; - } - - DynamicJsonDocument root(1024); - DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg[F("message")] = F("Failed to parse data!"); - response->setLength(); - request->send(response); - return; - } - - if (!(root.containsKey("ntp_server") && root.containsKey("ntp_timezone"))) { - retMsg[F("message")] = F("Values are missing!"); - response->setLength(); - request->send(response); - return; - } - - if (root[F("ntp_server")].as().length() == 0 || root[F("ntp_server")].as().length() > NTP_MAX_SERVER_STRLEN) { - retMsg[F("message")] = F("NTP Server must between 1 and " STR(NTP_MAX_SERVER_STRLEN) " characters long!"); - response->setLength(); - request->send(response); - return; - } - - if (root[F("ntp_timezone")].as().length() == 0 || root[F("ntp_timezone")].as().length() > NTP_MAX_TIMEZONE_STRLEN) { - retMsg[F("message")] = F("Timezone must between 1 and " STR(NTP_MAX_TIMEZONE_STRLEN) " characters long!"); - response->setLength(); - request->send(response); - return; - } - - if (root[F("ntp_timezone_descr")].as().length() == 0 || root[F("ntp_timezone_descr")].as().length() > NTP_MAX_TIMEZONEDESCR_STRLEN) { - retMsg[F("message")] = F("Timezone description must between 1 and " STR(NTP_MAX_TIMEZONEDESCR_STRLEN) " characters long!"); - response->setLength(); - request->send(response); - return; - } - - CONFIG_T& config = Configuration.get(); - strcpy(config.Ntp_Server, root[F("ntp_server")].as().c_str()); - strcpy(config.Ntp_Timezone, root[F("ntp_timezone")].as().c_str()); - strcpy(config.Ntp_TimezoneDescr, root[F("ntp_timezone_descr")].as().c_str()); - Configuration.write(); - - retMsg[F("type")] = F("success"); - retMsg[F("message")] = F("Settings saved!"); - - response->setLength(); - request->send(response); - - NtpSettings.setServer(); - NtpSettings.setTimezone(); -} - -void WebApiClass::onMqttStatus(AsyncWebServerRequest* request) -{ - AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject root = response->getRoot(); - CONFIG_T& config = Configuration.get(); - - root[F("mqtt_enabled")] = config.Mqtt_Enabled; - root[F("mqtt_hostname")] = config.Mqtt_Hostname; - root[F("mqtt_port")] = config.Mqtt_Port; - root[F("mqtt_username")] = config.Mqtt_Username; - root[F("mqtt_topic")] = config.Mqtt_Topic; - root[F("mqtt_connected")] = MqttSettings.getConnected(); - root[F("mqtt_retain")] = config.Mqtt_Retain; - root[F("mqtt_lwt_topic")] = String(config.Mqtt_Topic) + config.Mqtt_LwtTopic; - - response->setLength(); - request->send(response); -} - -void WebApiClass::onMqttAdminGet(AsyncWebServerRequest* request) -{ - AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject root = response->getRoot(); - CONFIG_T& config = Configuration.get(); - - root[F("mqtt_enabled")] = config.Mqtt_Enabled; - root[F("mqtt_hostname")] = config.Mqtt_Hostname; - root[F("mqtt_port")] = config.Mqtt_Port; - root[F("mqtt_username")] = config.Mqtt_Username; - root[F("mqtt_password")] = config.Mqtt_Password; - root[F("mqtt_topic")] = config.Mqtt_Topic; - root[F("mqtt_retain")] = config.Mqtt_Retain; - root[F("mqtt_lwt_topic")] = config.Mqtt_LwtTopic; - root[F("mqtt_lwt_online")] = config.Mqtt_LwtValue_Online; - root[F("mqtt_lwt_offline")] = config.Mqtt_LwtValue_Offline; - - response->setLength(); - request->send(response); -} - -void WebApiClass::onMqttAdminPost(AsyncWebServerRequest* request) -{ - AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject retMsg = response->getRoot(); - retMsg[F("type")] = F("warning"); - - if (!request->hasParam("data", true)) { - retMsg[F("message")] = F("No values found!"); - response->setLength(); - request->send(response); - return; - } - - String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg[F("message")] = F("Data too large!"); - response->setLength(); - request->send(response); - return; - } - - DynamicJsonDocument root(1024); - DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg[F("message")] = F("Failed to parse data!"); - response->setLength(); - request->send(response); - return; - } - - if (!(root.containsKey("mqtt_enabled") && root.containsKey("mqtt_hostname") && root.containsKey("mqtt_port") && root.containsKey("mqtt_username") && root.containsKey("mqtt_password") && root.containsKey("mqtt_topic") && root.containsKey("mqtt_retain") && root.containsKey("mqtt_lwt_topic") && root.containsKey("mqtt_lwt_online") && root.containsKey("mqtt_lwt_offline"))) { - retMsg[F("message")] = F("Values are missing!"); - response->setLength(); - request->send(response); - return; - } - - if (root[F("mqtt_enabled")].as()) { - if (root[F("mqtt_hostname")].as().length() == 0 || root[F("mqtt_hostname")].as().length() > MQTT_MAX_HOSTNAME_STRLEN) { - retMsg[F("message")] = F("MqTT Server must between 1 and " STR(MQTT_MAX_HOSTNAME_STRLEN) " characters long!"); - response->setLength(); - request->send(response); - return; - } - - if (root[F("mqtt_username")].as().length() > MQTT_MAX_USERNAME_STRLEN) { - retMsg[F("message")] = F("Username must not longer then " STR(MQTT_MAX_USERNAME_STRLEN) " characters!"); - response->setLength(); - request->send(response); - return; - } - if (root[F("mqtt_password")].as().length() > MQTT_MAX_PASSWORD_STRLEN) { - retMsg[F("message")] = F("Password must not longer then " STR(MQTT_MAX_PASSWORD_STRLEN) " characters!"); - response->setLength(); - request->send(response); - return; - } - if (root[F("mqtt_topic")].as().length() > MQTT_MAX_TOPIC_STRLEN) { - retMsg[F("message")] = F("Topic must not longer then " STR(MQTT_MAX_TOPIC_STRLEN) " characters!"); - response->setLength(); - request->send(response); - return; - } - - if (root[F("mqtt_port")].as() == 0 || root[F("mqtt_port")].as() > 65535) { - retMsg[F("message")] = F("Port must be a number between 1 and 65535!"); - response->setLength(); - request->send(response); - return; - } - - if (root[F("mqtt_lwt_topic")].as().length() > MQTT_MAX_TOPIC_STRLEN) { - retMsg[F("message")] = F("LWT topic must not longer then " STR(MQTT_MAX_TOPIC_STRLEN) " characters!"); - response->setLength(); - request->send(response); - return; - } - - if (root[F("mqtt_lwt_online")].as().length() > MQTT_MAX_LWTVALUE_STRLEN) { - retMsg[F("message")] = F("LWT online value must not longer then " STR(MQTT_MAX_LWTVALUE_STRLEN) " characters!"); - response->setLength(); - request->send(response); - return; - } - - if (root[F("mqtt_lwt_offline")].as().length() > MQTT_MAX_LWTVALUE_STRLEN) { - retMsg[F("message")] = F("LWT offline value must not longer then " STR(MQTT_MAX_LWTVALUE_STRLEN) " characters!"); - response->setLength(); - request->send(response); - return; - } - } - - CONFIG_T& config = Configuration.get(); - config.Mqtt_Enabled = root[F("mqtt_enabled")].as(); - config.Mqtt_Retain = root[F("mqtt_retain")].as(); - config.Mqtt_Port = root[F("mqtt_port")].as(); - strcpy(config.Mqtt_Hostname, root[F("mqtt_hostname")].as().c_str()); - strcpy(config.Mqtt_Username, root[F("mqtt_username")].as().c_str()); - strcpy(config.Mqtt_Password, root[F("mqtt_password")].as().c_str()); - strcpy(config.Mqtt_Topic, root[F("mqtt_topic")].as().c_str()); - strcpy(config.Mqtt_LwtTopic, root[F("mqtt_lwt_topic")].as().c_str()); - strcpy(config.Mqtt_LwtValue_Online, root[F("mqtt_lwt_online")].as().c_str()); - strcpy(config.Mqtt_LwtValue_Offline, root[F("mqtt_lwt_offline")].as().c_str()); - Configuration.write(); - - retMsg[F("type")] = F("success"); - retMsg[F("message")] = F("Settings saved!"); - - response->setLength(); - request->send(response); - - MqttSettings.performReconnect(); -} - -void WebApiClass::onInverterList(AsyncWebServerRequest* request) -{ - AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject root = response->getRoot(); - JsonArray data = root.createNestedArray(F("inverter")); - - CONFIG_T& config = Configuration.get(); - - for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { - if (config.Inverter[i].Serial > 0) { - JsonObject obj = data.createNestedObject(); - obj[F("id")] = i; - obj[F("name")] = String(config.Inverter[i].Name); - - // Inverter Serial is read as HEX - char buffer[sizeof(uint64_t) * 8 + 1]; - sprintf(buffer, "%0lx%08lx", - ((uint32_t)((config.Inverter[i].Serial >> 32) & 0xFFFFFFFF)), - ((uint32_t)(config.Inverter[i].Serial & 0xFFFFFFFF))); - obj[F("serial")] = buffer; - } - } - - response->setLength(); - request->send(response); -} - -void WebApiClass::onInverterAdd(AsyncWebServerRequest* request) -{ - AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject retMsg = response->getRoot(); - retMsg[F("type")] = F("warning"); - - if (!request->hasParam("data", true)) { - retMsg[F("message")] = F("No values found!"); - response->setLength(); - request->send(response); - return; - } - - String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg[F("message")] = F("Data too large!"); - response->setLength(); - request->send(response); - return; - } - - DynamicJsonDocument root(1024); - DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg[F("message")] = F("Failed to parse data!"); - response->setLength(); - request->send(response); - return; - } - - if (!(root.containsKey("serial") && root.containsKey("name"))) { - retMsg[F("message")] = F("Values are missing!"); - response->setLength(); - request->send(response); - return; - } - - if (root[F("serial")].as() == 0) { - retMsg[F("message")] = F("Serial must be a number > 0!"); - response->setLength(); - request->send(response); - return; - } - - if (root[F("name")].as().length() == 0 || root[F("name")].as().length() > INV_MAX_NAME_STRLEN) { - retMsg[F("message")] = F("Name must between 1 and " STR(INV_MAX_NAME_STRLEN) " characters long!"); - response->setLength(); - request->send(response); - return; - } - - INVERTER_CONFIG_T* inverter = Configuration.getFreeInverterSlot(); - - if (!inverter) { - retMsg[F("message")] = F("Only " STR(INV_MAX_COUNT) " inverters are supported!"); - response->setLength(); - request->send(response); - return; - } - - char* t; - // Interpret the string as a hex value and convert it to uint64_t - inverter->Serial = strtoll(root[F("serial")].as().c_str(), &t, 16); - - strncpy(inverter->Name, root[F("name")].as().c_str(), INV_MAX_NAME_STRLEN); - Configuration.write(); - - retMsg[F("type")] = F("success"); - retMsg[F("message")] = F("Inverter created!"); - - response->setLength(); - request->send(response); - - Hoymiles.addInverter(inverter->Name, inverter->Serial); -} - -void WebApiClass::onInverterEdit(AsyncWebServerRequest* request) -{ - AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject retMsg = response->getRoot(); - retMsg[F("type")] = F("warning"); - - if (!request->hasParam("data", true)) { - retMsg[F("message")] = F("No values found!"); - response->setLength(); - request->send(response); - return; - } - - String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg[F("message")] = F("Data too large!"); - response->setLength(); - request->send(response); - return; - } - - DynamicJsonDocument root(1024); - DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg[F("message")] = F("Failed to parse data!"); - response->setLength(); - request->send(response); - return; - } - - if (!(root.containsKey("id") && root.containsKey("serial") && root.containsKey("name"))) { - retMsg[F("message")] = F("Values are missing!"); - response->setLength(); - request->send(response); - return; - } - - if (root[F("id")].as() > INV_MAX_COUNT - 1) { - retMsg[F("message")] = F("Invalid ID specified!"); - response->setLength(); - request->send(response); - return; - } - - if (root[F("serial")].as() == 0) { - retMsg[F("message")] = F("Serial must be a number > 0!"); - response->setLength(); - request->send(response); - return; - } - - if (root[F("name")].as().length() == 0 || root[F("name")].as().length() > INV_MAX_NAME_STRLEN) { - retMsg[F("message")] = F("Name must between 1 and " STR(INV_MAX_NAME_STRLEN) " characters long!"); - response->setLength(); - request->send(response); - return; - } - - INVERTER_CONFIG_T& inverter = Configuration.get().Inverter[root[F("id")].as()]; - - char* t; - // Interpret the string as a hex value and convert it to uint64_t - inverter.Serial = strtoll(root[F("serial")].as().c_str(), &t, 16); - strncpy(inverter.Name, root[F("name")].as().c_str(), INV_MAX_NAME_STRLEN); - Configuration.write(); - - retMsg[F("type")] = F("success"); - retMsg[F("message")] = F("Inverter changed!"); - - response->setLength(); - request->send(response); - - std::shared_ptr inv = Hoymiles.getInverterByPos(root[F("id")].as()); - inv->setName(inverter.Name); - inv->setSerial(inverter.Serial); -} - -void WebApiClass::onInverterDelete(AsyncWebServerRequest* request) -{ - AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject retMsg = response->getRoot(); - retMsg[F("type")] = F("warning"); - - if (!request->hasParam("data", true)) { - retMsg[F("message")] = F("No values found!"); - response->setLength(); - request->send(response); - return; - } - - String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg[F("message")] = F("Data too large!"); - response->setLength(); - request->send(response); - return; - } - - DynamicJsonDocument root(1024); - DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg[F("message")] = F("Failed to parse data!"); - response->setLength(); - request->send(response); - return; - } - - if (!(root.containsKey("id"))) { - retMsg[F("message")] = F("Values are missing!"); - response->setLength(); - request->send(response); - return; - } - - if (root[F("id")].as() > INV_MAX_COUNT - 1) { - retMsg[F("message")] = F("Invalid ID specified!"); - response->setLength(); - request->send(response); - return; - } - - uint8_t inverter_id = root[F("id")].as(); - INVERTER_CONFIG_T& inverter = Configuration.get().Inverter[inverter_id]; - inverter.Serial = 0; - strncpy(inverter.Name, "", 0); - Configuration.write(); - - retMsg[F("type")] = F("success"); - retMsg[F("message")] = F("Inverter deleted!"); - - response->setLength(); - request->send(response); - - Hoymiles.removeInverterByPos(inverter_id); -} - -void WebApiClass::onFirmwareUpdateFinish(AsyncWebServerRequest* request) -{ - // the request handler is triggered after the upload has finished... - // create the response, add header, and send response - - AsyncWebServerResponse* response = request->beginResponse((Update.hasError()) ? 500 : 200, "text/plain", (Update.hasError()) ? "FAIL" : "OK"); - response->addHeader("Connection", "close"); - response->addHeader("Access-Control-Allow-Origin", "*"); - request->send(response); - yield(); - delay(1000); - yield(); - ESP.restart(); -} - -void WebApiClass::onFirmwareUpdateUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final) -{ - // Upload handler chunks in data - if (!index) { - if (!request->hasParam("MD5", true)) { - return request->send(400, "text/plain", "MD5 parameter missing"); - } - - if (!Update.setMD5(request->getParam("MD5", true)->value().c_str())) { - return request->send(400, "text/plain", "MD5 parameter invalid"); - } - - if (!Update.begin(UPDATE_SIZE_UNKNOWN, U_FLASH)) { // Start with max available size - Update.printError(Serial); - return request->send(400, "text/plain", "OTA could not begin"); - } - } - - // Write chunked data to the free sketch space - if (len) { - if (Update.write(data, len) != len) { - return request->send(400, "text/plain", "OTA could not begin"); - } - } - - if (final) { // if the final flag is set then this is the last frame of data - if (!Update.end(true)) { // true to set the size to the current progress - Update.printError(Serial); - return request->send(400, "text/plain", "Could not end OTA"); - } - } else { - return; - } -} - -void WebApiClass::onDtuAdminGet(AsyncWebServerRequest* request) -{ - AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject root = response->getRoot(); - CONFIG_T& config = Configuration.get(); - - // DTU Serial is read as HEX - char buffer[sizeof(uint64_t) * 8 + 1]; - sprintf(buffer, "%0lx%08lx", - ((uint32_t)((config.Dtu_Serial >> 32) & 0xFFFFFFFF)), - ((uint32_t)(config.Dtu_Serial & 0xFFFFFFFF))); - root[F("dtu_serial")] = buffer; - root[F("dtu_pollinterval")] = config.Dtu_PollInterval; - root[F("dtu_palevel")] = config.Dtu_PaLevel; - - response->setLength(); - request->send(response); -} - -void WebApiClass::onDtuAdminPost(AsyncWebServerRequest* request) -{ - AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonObject retMsg = response->getRoot(); - retMsg[F("type")] = F("warning"); - - if (!request->hasParam("data", true)) { - retMsg[F("message")] = F("No values found!"); - response->setLength(); - request->send(response); - return; - } - - String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg[F("message")] = F("Data too large!"); - response->setLength(); - request->send(response); - return; - } - - DynamicJsonDocument root(1024); - DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg[F("message")] = F("Failed to parse data!"); - response->setLength(); - request->send(response); - return; - } - - if (!(root.containsKey("dtu_serial") && root.containsKey("dtu_pollinterval") && root.containsKey("dtu_palevel"))) { - retMsg[F("message")] = F("Values are missing!"); - response->setLength(); - request->send(response); - return; - } - - if (root[F("dtu_serial")].as() == 0) { - retMsg[F("message")] = F("Serial cannot be zero!"); - response->setLength(); - request->send(response); - return; - } - - if (root[F("dtu_pollinterval")].as() == 0) { - retMsg[F("message")] = F("Poll interval must be greater zero!"); - response->setLength(); - request->send(response); - return; - } - - if (root[F("dtu_palevel")].as() > 3) { - retMsg[F("message")] = F("Invalid power level setting!"); - response->setLength(); - request->send(response); - return; - } - - CONFIG_T& config = Configuration.get(); - char* t; - // Interpret the string as a hex value and convert it to uint64_t - config.Dtu_Serial = strtoll(root[F("dtu_serial")].as().c_str(), &t, 16); - config.Dtu_PollInterval = root[F("dtu_pollinterval")].as(); - config.Dtu_PaLevel = root[F("dtu_palevel")].as(); - Configuration.write(); - - retMsg[F("type")] = F("success"); - retMsg[F("message")] = F("Settings saved!"); - - response->setLength(); - request->send(response); - - Hoymiles.getRadio()->setPALevel((rf24_pa_dbm_e)config.Dtu_PaLevel); - Hoymiles.getRadio()->setDtuSerial(config.Dtu_Serial); - Hoymiles.setPollInterval(config.Dtu_PollInterval); -} - WebApiClass WebApi; \ No newline at end of file diff --git a/src/WebApi_dtu.cpp b/src/WebApi_dtu.cpp new file mode 100644 index 0000000..75a7aed --- /dev/null +++ b/src/WebApi_dtu.cpp @@ -0,0 +1,117 @@ +#include "WebApi_dtu.h" +#include "ArduinoJson.h" +#include "AsyncJson.h" +#include "Configuration.h" +#include "Hoymiles.h" + +void WebApiDtuClass::init(AsyncWebServer* server) +{ + using namespace std::placeholders; + + _server = server; + + _server->on("/api/dtu/config", HTTP_GET, std::bind(&WebApiDtuClass::onDtuAdminGet, this, _1)); + _server->on("/api/dtu/config", HTTP_POST, std::bind(&WebApiDtuClass::onDtuAdminPost, this, _1)); +} + +void WebApiDtuClass::loop() +{ +} + +void WebApiDtuClass::onDtuAdminGet(AsyncWebServerRequest* request) +{ + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonObject root = response->getRoot(); + CONFIG_T& config = Configuration.get(); + + // DTU Serial is read as HEX + char buffer[sizeof(uint64_t) * 8 + 1]; + sprintf(buffer, "%0lx%08lx", + ((uint32_t)((config.Dtu_Serial >> 32) & 0xFFFFFFFF)), + ((uint32_t)(config.Dtu_Serial & 0xFFFFFFFF))); + root[F("dtu_serial")] = buffer; + root[F("dtu_pollinterval")] = config.Dtu_PollInterval; + root[F("dtu_palevel")] = config.Dtu_PaLevel; + + response->setLength(); + request->send(response); +} + +void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) +{ + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonObject retMsg = response->getRoot(); + retMsg[F("type")] = F("warning"); + + if (!request->hasParam("data", true)) { + retMsg[F("message")] = F("No values found!"); + response->setLength(); + request->send(response); + return; + } + + String json = request->getParam("data", true)->value(); + + if (json.length() > 1024) { + retMsg[F("message")] = F("Data too large!"); + response->setLength(); + request->send(response); + return; + } + + DynamicJsonDocument root(1024); + DeserializationError error = deserializeJson(root, json); + + if (error) { + retMsg[F("message")] = F("Failed to parse data!"); + response->setLength(); + request->send(response); + return; + } + + if (!(root.containsKey("dtu_serial") && root.containsKey("dtu_pollinterval") && root.containsKey("dtu_palevel"))) { + retMsg[F("message")] = F("Values are missing!"); + response->setLength(); + request->send(response); + return; + } + + if (root[F("dtu_serial")].as() == 0) { + retMsg[F("message")] = F("Serial cannot be zero!"); + response->setLength(); + request->send(response); + return; + } + + if (root[F("dtu_pollinterval")].as() == 0) { + retMsg[F("message")] = F("Poll interval must be greater zero!"); + response->setLength(); + request->send(response); + return; + } + + if (root[F("dtu_palevel")].as() > 3) { + retMsg[F("message")] = F("Invalid power level setting!"); + response->setLength(); + request->send(response); + return; + } + + CONFIG_T& config = Configuration.get(); + char* t; + // Interpret the string as a hex value and convert it to uint64_t + config.Dtu_Serial = strtoll(root[F("dtu_serial")].as().c_str(), &t, 16); + config.Dtu_PollInterval = root[F("dtu_pollinterval")].as(); + config.Dtu_PaLevel = root[F("dtu_palevel")].as(); + Configuration.write(); + + retMsg[F("type")] = F("success"); + retMsg[F("message")] = F("Settings saved!"); + + response->setLength(); + request->send(response); + + Hoymiles.getRadio()->setPALevel((rf24_pa_dbm_e)config.Dtu_PaLevel); + Hoymiles.getRadio()->setDtuSerial(config.Dtu_Serial); + Hoymiles.setPollInterval(config.Dtu_PollInterval); +} \ No newline at end of file diff --git a/src/WebApi_firmware.cpp b/src/WebApi_firmware.cpp new file mode 100644 index 0000000..e2d6d77 --- /dev/null +++ b/src/WebApi_firmware.cpp @@ -0,0 +1,71 @@ +#include "WebApi_firmware.h" +#include "ArduinoJson.h" +#include "AsyncJson.h" +#include "Configuration.h" +#include "Update.h" +#include "helper.h" + +void WebApiFirmwareClass::init(AsyncWebServer* server) +{ + using namespace std::placeholders; + + _server = server; + + _server->on("/api/firmware/update", HTTP_POST, + std::bind(&WebApiFirmwareClass::onFirmwareUpdateFinish, this, _1), + std::bind(&WebApiFirmwareClass::onFirmwareUpdateUpload, this, _1, _2, _3, _4, _5, _6)); +} + +void WebApiFirmwareClass::loop() +{ +} + +void WebApiFirmwareClass::onFirmwareUpdateFinish(AsyncWebServerRequest* request) +{ + // the request handler is triggered after the upload has finished... + // create the response, add header, and send response + + AsyncWebServerResponse* response = request->beginResponse((Update.hasError()) ? 500 : 200, "text/plain", (Update.hasError()) ? "FAIL" : "OK"); + response->addHeader("Connection", "close"); + response->addHeader("Access-Control-Allow-Origin", "*"); + request->send(response); + yield(); + delay(1000); + yield(); + ESP.restart(); +} + +void WebApiFirmwareClass::onFirmwareUpdateUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final) +{ + // Upload handler chunks in data + if (!index) { + if (!request->hasParam("MD5", true)) { + return request->send(400, "text/plain", "MD5 parameter missing"); + } + + if (!Update.setMD5(request->getParam("MD5", true)->value().c_str())) { + return request->send(400, "text/plain", "MD5 parameter invalid"); + } + + if (!Update.begin(UPDATE_SIZE_UNKNOWN, U_FLASH)) { // Start with max available size + Update.printError(Serial); + return request->send(400, "text/plain", "OTA could not begin"); + } + } + + // Write chunked data to the free sketch space + if (len) { + if (Update.write(data, len) != len) { + return request->send(400, "text/plain", "OTA could not begin"); + } + } + + if (final) { // if the final flag is set then this is the last frame of data + if (!Update.end(true)) { // true to set the size to the current progress + Update.printError(Serial); + return request->send(400, "text/plain", "Could not end OTA"); + } + } else { + return; + } +} \ No newline at end of file diff --git a/src/WebApi_inverter.cpp b/src/WebApi_inverter.cpp new file mode 100644 index 0000000..c7a31f3 --- /dev/null +++ b/src/WebApi_inverter.cpp @@ -0,0 +1,267 @@ +#include "WebApi_inverter.h" +#include "ArduinoJson.h" +#include "AsyncJson.h" +#include "Configuration.h" +#include "Hoymiles.h" +#include "helper.h" + +void WebApiInverterClass::init(AsyncWebServer* server) +{ + using namespace std::placeholders; + + _server = server; + + _server->on("/api/inverter/list", HTTP_GET, std::bind(&WebApiInverterClass::onInverterList, this, _1)); + _server->on("/api/inverter/add", HTTP_POST, std::bind(&WebApiInverterClass::onInverterAdd, this, _1)); + _server->on("/api/inverter/edit", HTTP_POST, std::bind(&WebApiInverterClass::onInverterEdit, this, _1)); + _server->on("/api/inverter/del", HTTP_POST, std::bind(&WebApiInverterClass::onInverterDelete, this, _1)); +} + +void WebApiInverterClass::loop() +{ +} + +void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request) +{ + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonObject root = response->getRoot(); + JsonArray data = root.createNestedArray(F("inverter")); + + CONFIG_T& config = Configuration.get(); + + for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { + if (config.Inverter[i].Serial > 0) { + JsonObject obj = data.createNestedObject(); + obj[F("id")] = i; + obj[F("name")] = String(config.Inverter[i].Name); + + // Inverter Serial is read as HEX + char buffer[sizeof(uint64_t) * 8 + 1]; + sprintf(buffer, "%0lx%08lx", + ((uint32_t)((config.Inverter[i].Serial >> 32) & 0xFFFFFFFF)), + ((uint32_t)(config.Inverter[i].Serial & 0xFFFFFFFF))); + obj[F("serial")] = buffer; + } + } + + response->setLength(); + request->send(response); +} + +void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) +{ + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonObject retMsg = response->getRoot(); + retMsg[F("type")] = F("warning"); + + if (!request->hasParam("data", true)) { + retMsg[F("message")] = F("No values found!"); + response->setLength(); + request->send(response); + return; + } + + String json = request->getParam("data", true)->value(); + + if (json.length() > 1024) { + retMsg[F("message")] = F("Data too large!"); + response->setLength(); + request->send(response); + return; + } + + DynamicJsonDocument root(1024); + DeserializationError error = deserializeJson(root, json); + + if (error) { + retMsg[F("message")] = F("Failed to parse data!"); + response->setLength(); + request->send(response); + return; + } + + if (!(root.containsKey("serial") && root.containsKey("name"))) { + retMsg[F("message")] = F("Values are missing!"); + response->setLength(); + request->send(response); + return; + } + + if (root[F("serial")].as() == 0) { + retMsg[F("message")] = F("Serial must be a number > 0!"); + response->setLength(); + request->send(response); + return; + } + + if (root[F("name")].as().length() == 0 || root[F("name")].as().length() > INV_MAX_NAME_STRLEN) { + retMsg[F("message")] = F("Name must between 1 and " STR(INV_MAX_NAME_STRLEN) " characters long!"); + response->setLength(); + request->send(response); + return; + } + + INVERTER_CONFIG_T* inverter = Configuration.getFreeInverterSlot(); + + if (!inverter) { + retMsg[F("message")] = F("Only " STR(INV_MAX_COUNT) " inverters are supported!"); + response->setLength(); + request->send(response); + return; + } + + char* t; + // Interpret the string as a hex value and convert it to uint64_t + inverter->Serial = strtoll(root[F("serial")].as().c_str(), &t, 16); + + strncpy(inverter->Name, root[F("name")].as().c_str(), INV_MAX_NAME_STRLEN); + Configuration.write(); + + retMsg[F("type")] = F("success"); + retMsg[F("message")] = F("Inverter created!"); + + response->setLength(); + request->send(response); + + Hoymiles.addInverter(inverter->Name, inverter->Serial); +} + +void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) +{ + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonObject retMsg = response->getRoot(); + retMsg[F("type")] = F("warning"); + + if (!request->hasParam("data", true)) { + retMsg[F("message")] = F("No values found!"); + response->setLength(); + request->send(response); + return; + } + + String json = request->getParam("data", true)->value(); + + if (json.length() > 1024) { + retMsg[F("message")] = F("Data too large!"); + response->setLength(); + request->send(response); + return; + } + + DynamicJsonDocument root(1024); + DeserializationError error = deserializeJson(root, json); + + if (error) { + retMsg[F("message")] = F("Failed to parse data!"); + response->setLength(); + request->send(response); + return; + } + + if (!(root.containsKey("id") && root.containsKey("serial") && root.containsKey("name"))) { + retMsg[F("message")] = F("Values are missing!"); + response->setLength(); + request->send(response); + return; + } + + if (root[F("id")].as() > INV_MAX_COUNT - 1) { + retMsg[F("message")] = F("Invalid ID specified!"); + response->setLength(); + request->send(response); + return; + } + + if (root[F("serial")].as() == 0) { + retMsg[F("message")] = F("Serial must be a number > 0!"); + response->setLength(); + request->send(response); + return; + } + + if (root[F("name")].as().length() == 0 || root[F("name")].as().length() > INV_MAX_NAME_STRLEN) { + retMsg[F("message")] = F("Name must between 1 and " STR(INV_MAX_NAME_STRLEN) " characters long!"); + response->setLength(); + request->send(response); + return; + } + + INVERTER_CONFIG_T& inverter = Configuration.get().Inverter[root[F("id")].as()]; + + char* t; + // Interpret the string as a hex value and convert it to uint64_t + inverter.Serial = strtoll(root[F("serial")].as().c_str(), &t, 16); + strncpy(inverter.Name, root[F("name")].as().c_str(), INV_MAX_NAME_STRLEN); + Configuration.write(); + + retMsg[F("type")] = F("success"); + retMsg[F("message")] = F("Inverter changed!"); + + response->setLength(); + request->send(response); + + std::shared_ptr inv = Hoymiles.getInverterByPos(root[F("id")].as()); + inv->setName(inverter.Name); + inv->setSerial(inverter.Serial); +} + +void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request) +{ + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonObject retMsg = response->getRoot(); + retMsg[F("type")] = F("warning"); + + if (!request->hasParam("data", true)) { + retMsg[F("message")] = F("No values found!"); + response->setLength(); + request->send(response); + return; + } + + String json = request->getParam("data", true)->value(); + + if (json.length() > 1024) { + retMsg[F("message")] = F("Data too large!"); + response->setLength(); + request->send(response); + return; + } + + DynamicJsonDocument root(1024); + DeserializationError error = deserializeJson(root, json); + + if (error) { + retMsg[F("message")] = F("Failed to parse data!"); + response->setLength(); + request->send(response); + return; + } + + if (!(root.containsKey("id"))) { + retMsg[F("message")] = F("Values are missing!"); + response->setLength(); + request->send(response); + return; + } + + if (root[F("id")].as() > INV_MAX_COUNT - 1) { + retMsg[F("message")] = F("Invalid ID specified!"); + response->setLength(); + request->send(response); + return; + } + + uint8_t inverter_id = root[F("id")].as(); + INVERTER_CONFIG_T& inverter = Configuration.get().Inverter[inverter_id]; + inverter.Serial = 0; + strncpy(inverter.Name, "", 0); + Configuration.write(); + + retMsg[F("type")] = F("success"); + retMsg[F("message")] = F("Inverter deleted!"); + + response->setLength(); + request->send(response); + + Hoymiles.removeInverterByPos(inverter_id); +} \ No newline at end of file diff --git a/src/WebApi_mqtt.cpp b/src/WebApi_mqtt.cpp new file mode 100644 index 0000000..2356c01 --- /dev/null +++ b/src/WebApi_mqtt.cpp @@ -0,0 +1,178 @@ +#include "WebApi_mqtt.h" +#include "ArduinoJson.h" +#include "AsyncJson.h" +#include "Configuration.h" +#include "MqttSettings.h" +#include "helper.h" + +void WebApiMqttClass::init(AsyncWebServer* server) +{ + using namespace std::placeholders; + + _server = server; + + _server->on("/api/mqtt/status", HTTP_GET, std::bind(&WebApiMqttClass::onMqttStatus, this, _1)); + _server->on("/api/mqtt/config", HTTP_GET, std::bind(&WebApiMqttClass::onMqttAdminGet, this, _1)); + _server->on("/api/mqtt/config", HTTP_POST, std::bind(&WebApiMqttClass::onMqttAdminPost, this, _1)); +} + +void WebApiMqttClass::loop() +{ +} + +void WebApiMqttClass::onMqttStatus(AsyncWebServerRequest* request) +{ + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonObject root = response->getRoot(); + CONFIG_T& config = Configuration.get(); + + root[F("mqtt_enabled")] = config.Mqtt_Enabled; + root[F("mqtt_hostname")] = config.Mqtt_Hostname; + root[F("mqtt_port")] = config.Mqtt_Port; + root[F("mqtt_username")] = config.Mqtt_Username; + root[F("mqtt_topic")] = config.Mqtt_Topic; + root[F("mqtt_connected")] = MqttSettings.getConnected(); + root[F("mqtt_retain")] = config.Mqtt_Retain; + root[F("mqtt_lwt_topic")] = String(config.Mqtt_Topic) + config.Mqtt_LwtTopic; + + response->setLength(); + request->send(response); +} + +void WebApiMqttClass::onMqttAdminGet(AsyncWebServerRequest* request) +{ + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonObject root = response->getRoot(); + CONFIG_T& config = Configuration.get(); + + root[F("mqtt_enabled")] = config.Mqtt_Enabled; + root[F("mqtt_hostname")] = config.Mqtt_Hostname; + root[F("mqtt_port")] = config.Mqtt_Port; + root[F("mqtt_username")] = config.Mqtt_Username; + root[F("mqtt_password")] = config.Mqtt_Password; + root[F("mqtt_topic")] = config.Mqtt_Topic; + root[F("mqtt_retain")] = config.Mqtt_Retain; + root[F("mqtt_lwt_topic")] = config.Mqtt_LwtTopic; + root[F("mqtt_lwt_online")] = config.Mqtt_LwtValue_Online; + root[F("mqtt_lwt_offline")] = config.Mqtt_LwtValue_Offline; + + response->setLength(); + request->send(response); +} + +void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) +{ + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonObject retMsg = response->getRoot(); + retMsg[F("type")] = F("warning"); + + if (!request->hasParam("data", true)) { + retMsg[F("message")] = F("No values found!"); + response->setLength(); + request->send(response); + return; + } + + String json = request->getParam("data", true)->value(); + + if (json.length() > 1024) { + retMsg[F("message")] = F("Data too large!"); + response->setLength(); + request->send(response); + return; + } + + DynamicJsonDocument root(1024); + DeserializationError error = deserializeJson(root, json); + + if (error) { + retMsg[F("message")] = F("Failed to parse data!"); + response->setLength(); + request->send(response); + return; + } + + if (!(root.containsKey("mqtt_enabled") && root.containsKey("mqtt_hostname") && root.containsKey("mqtt_port") && root.containsKey("mqtt_username") && root.containsKey("mqtt_password") && root.containsKey("mqtt_topic") && root.containsKey("mqtt_retain") && root.containsKey("mqtt_lwt_topic") && root.containsKey("mqtt_lwt_online") && root.containsKey("mqtt_lwt_offline"))) { + retMsg[F("message")] = F("Values are missing!"); + response->setLength(); + request->send(response); + return; + } + + if (root[F("mqtt_enabled")].as()) { + if (root[F("mqtt_hostname")].as().length() == 0 || root[F("mqtt_hostname")].as().length() > MQTT_MAX_HOSTNAME_STRLEN) { + retMsg[F("message")] = F("MqTT Server must between 1 and " STR(MQTT_MAX_HOSTNAME_STRLEN) " characters long!"); + response->setLength(); + request->send(response); + return; + } + + if (root[F("mqtt_username")].as().length() > MQTT_MAX_USERNAME_STRLEN) { + retMsg[F("message")] = F("Username must not longer then " STR(MQTT_MAX_USERNAME_STRLEN) " characters!"); + response->setLength(); + request->send(response); + return; + } + if (root[F("mqtt_password")].as().length() > MQTT_MAX_PASSWORD_STRLEN) { + retMsg[F("message")] = F("Password must not longer then " STR(MQTT_MAX_PASSWORD_STRLEN) " characters!"); + response->setLength(); + request->send(response); + return; + } + if (root[F("mqtt_topic")].as().length() > MQTT_MAX_TOPIC_STRLEN) { + retMsg[F("message")] = F("Topic must not longer then " STR(MQTT_MAX_TOPIC_STRLEN) " characters!"); + response->setLength(); + request->send(response); + return; + } + + if (root[F("mqtt_port")].as() == 0 || root[F("mqtt_port")].as() > 65535) { + retMsg[F("message")] = F("Port must be a number between 1 and 65535!"); + response->setLength(); + request->send(response); + return; + } + + if (root[F("mqtt_lwt_topic")].as().length() > MQTT_MAX_TOPIC_STRLEN) { + retMsg[F("message")] = F("LWT topic must not longer then " STR(MQTT_MAX_TOPIC_STRLEN) " characters!"); + response->setLength(); + request->send(response); + return; + } + + if (root[F("mqtt_lwt_online")].as().length() > MQTT_MAX_LWTVALUE_STRLEN) { + retMsg[F("message")] = F("LWT online value must not longer then " STR(MQTT_MAX_LWTVALUE_STRLEN) " characters!"); + response->setLength(); + request->send(response); + return; + } + + if (root[F("mqtt_lwt_offline")].as().length() > MQTT_MAX_LWTVALUE_STRLEN) { + retMsg[F("message")] = F("LWT offline value must not longer then " STR(MQTT_MAX_LWTVALUE_STRLEN) " characters!"); + response->setLength(); + request->send(response); + return; + } + } + + CONFIG_T& config = Configuration.get(); + config.Mqtt_Enabled = root[F("mqtt_enabled")].as(); + config.Mqtt_Retain = root[F("mqtt_retain")].as(); + config.Mqtt_Port = root[F("mqtt_port")].as(); + strcpy(config.Mqtt_Hostname, root[F("mqtt_hostname")].as().c_str()); + strcpy(config.Mqtt_Username, root[F("mqtt_username")].as().c_str()); + strcpy(config.Mqtt_Password, root[F("mqtt_password")].as().c_str()); + strcpy(config.Mqtt_Topic, root[F("mqtt_topic")].as().c_str()); + strcpy(config.Mqtt_LwtTopic, root[F("mqtt_lwt_topic")].as().c_str()); + strcpy(config.Mqtt_LwtValue_Online, root[F("mqtt_lwt_online")].as().c_str()); + strcpy(config.Mqtt_LwtValue_Offline, root[F("mqtt_lwt_offline")].as().c_str()); + Configuration.write(); + + retMsg[F("type")] = F("success"); + retMsg[F("message")] = F("Settings saved!"); + + response->setLength(); + request->send(response); + + MqttSettings.performReconnect(); +} \ No newline at end of file diff --git a/src/WebApi_network.cpp b/src/WebApi_network.cpp new file mode 100644 index 0000000..1dc1c69 --- /dev/null +++ b/src/WebApi_network.cpp @@ -0,0 +1,200 @@ +#include "WebApi_network.h" +#include "ArduinoJson.h" +#include "AsyncJson.h" +#include "Configuration.h" +#include "WiFiSettings.h" +#include "helper.h" + +void WebApiNetworkClass::init(AsyncWebServer* server) +{ + using namespace std::placeholders; + + _server = server; + + _server->on("/api/network/status", HTTP_GET, std::bind(&WebApiNetworkClass::onNetworkStatus, this, _1)); + _server->on("/api/network/config", HTTP_GET, std::bind(&WebApiNetworkClass::onNetworkAdminGet, this, _1)); + _server->on("/api/network/config", HTTP_POST, std::bind(&WebApiNetworkClass::onNetworkAdminPost, this, _1)); +} + +void WebApiNetworkClass::loop() +{ +} + +void WebApiNetworkClass::onNetworkStatus(AsyncWebServerRequest* request) +{ + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonObject root = response->getRoot(); + + root[F("sta_status")] = ((WiFi.getMode() & WIFI_STA) != 0); + root[F("sta_ssid")] = WiFi.SSID(); + root[F("sta_ip")] = WiFi.localIP().toString(); + root[F("sta_netmask")] = WiFi.subnetMask().toString(); + root[F("sta_gateway")] = WiFi.gatewayIP().toString(); + root[F("sta_dns1")] = WiFi.dnsIP(0).toString(); + root[F("sta_dns2")] = WiFi.dnsIP(1).toString(); + root[F("sta_mac")] = WiFi.macAddress(); + root[F("sta_rssi")] = WiFi.RSSI(); + root[F("ap_status")] = ((WiFi.getMode() & WIFI_AP) != 0); + root[F("ap_ssid")] = WiFiSettings.getApName(); + root[F("ap_ip")] = WiFi.softAPIP().toString(); + root[F("ap_mac")] = WiFi.softAPmacAddress(); + root[F("ap_stationnum")] = WiFi.softAPgetStationNum(); + + response->setLength(); + request->send(response); +} + +void WebApiNetworkClass::onNetworkAdminGet(AsyncWebServerRequest* request) +{ + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonObject root = response->getRoot(); + CONFIG_T& config = Configuration.get(); + + root[F("hostname")] = config.WiFi_Hostname; + root[F("dhcp")] = config.WiFi_Dhcp; + root[F("ipaddress")] = IPAddress(config.WiFi_Ip).toString(); + root[F("netmask")] = IPAddress(config.WiFi_Netmask).toString(); + root[F("gateway")] = IPAddress(config.WiFi_Gateway).toString(); + root[F("dns1")] = IPAddress(config.WiFi_Dns1).toString(); + root[F("dns2")] = IPAddress(config.WiFi_Dns2).toString(); + root[F("ssid")] = config.WiFi_Ssid; + root[F("password")] = config.WiFi_Password; + + response->setLength(); + request->send(response); +} + +void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request) +{ + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonObject retMsg = response->getRoot(); + retMsg[F("type")] = F("warning"); + + if (!request->hasParam("data", true)) { + retMsg[F("message")] = F("No values found!"); + response->setLength(); + request->send(response); + return; + } + + String json = request->getParam("data", true)->value(); + + if (json.length() > 1024) { + retMsg[F("message")] = F("Data too large!"); + response->setLength(); + request->send(response); + return; + } + + DynamicJsonDocument root(1024); + DeserializationError error = deserializeJson(root, json); + + if (error) { + retMsg[F("message")] = F("Failed to parse data!"); + response->setLength(); + request->send(response); + return; + } + + if (!(root.containsKey("ssid") && root.containsKey("password") && root.containsKey("hostname") && root.containsKey("dhcp") && root.containsKey("ipaddress") && root.containsKey("netmask") && root.containsKey("gateway") && root.containsKey("dns1") && root.containsKey("dns2"))) { + retMsg[F("message")] = F("Values are missing!"); + response->setLength(); + request->send(response); + return; + } + + IPAddress ipaddress; + if (!ipaddress.fromString(root[F("ipaddress")].as())) { + retMsg[F("message")] = F("IP address is invalid!"); + response->setLength(); + request->send(response); + return; + } + IPAddress netmask; + if (!netmask.fromString(root[F("netmask")].as())) { + retMsg[F("message")] = F("Netmask is invalid!"); + response->setLength(); + request->send(response); + return; + } + IPAddress gateway; + if (!gateway.fromString(root[F("gateway")].as())) { + retMsg[F("message")] = F("Gateway is invalid!"); + response->setLength(); + request->send(response); + return; + } + IPAddress dns1; + if (!dns1.fromString(root[F("dns1")].as())) { + retMsg[F("message")] = F("DNS Server IP 1 is invalid!"); + response->setLength(); + request->send(response); + return; + } + IPAddress dns2; + if (!dns2.fromString(root[F("dns2")].as())) { + retMsg[F("message")] = F("DNS Server IP 2 is invalid!"); + response->setLength(); + request->send(response); + return; + } + + if (root[F("hostname")].as().length() == 0 || root[F("hostname")].as().length() > WIFI_MAX_HOSTNAME_STRLEN) { + retMsg[F("message")] = F("Hostname must between 1 and " STR(WIFI_MAX_HOSTNAME_STRLEN) " characters long!"); + response->setLength(); + request->send(response); + return; + } + if (root[F("ssid")].as().length() == 0 || root[F("ssid")].as().length() > WIFI_MAX_SSID_STRLEN) { + retMsg[F("message")] = F("SSID must between 1 and " STR(WIFI_MAX_SSID_STRLEN) " characters long!"); + response->setLength(); + request->send(response); + return; + } + if (root[F("password")].as().length() > WIFI_MAX_PASSWORD_STRLEN - 1) { + retMsg[F("message")] = F("Password must not be longer than " STR(WIFI_MAX_PASSWORD_STRLEN) " characters long!"); + response->setLength(); + request->send(response); + return; + } + + CONFIG_T& config = Configuration.get(); + config.WiFi_Ip[0] = ipaddress[0]; + config.WiFi_Ip[1] = ipaddress[1]; + config.WiFi_Ip[2] = ipaddress[2]; + config.WiFi_Ip[3] = ipaddress[3]; + config.WiFi_Netmask[0] = netmask[0]; + config.WiFi_Netmask[1] = netmask[1]; + config.WiFi_Netmask[2] = netmask[2]; + config.WiFi_Netmask[3] = netmask[3]; + config.WiFi_Gateway[0] = gateway[0]; + config.WiFi_Gateway[1] = gateway[1]; + config.WiFi_Gateway[2] = gateway[2]; + config.WiFi_Gateway[3] = gateway[3]; + config.WiFi_Dns1[0] = dns1[0]; + config.WiFi_Dns1[1] = dns1[1]; + config.WiFi_Dns1[2] = dns1[2]; + config.WiFi_Dns1[3] = dns1[3]; + config.WiFi_Dns2[0] = dns2[0]; + config.WiFi_Dns2[1] = dns2[1]; + config.WiFi_Dns2[2] = dns2[2]; + config.WiFi_Dns2[3] = dns2[3]; + strcpy(config.WiFi_Ssid, root[F("ssid")].as().c_str()); + strcpy(config.WiFi_Password, root[F("password")].as().c_str()); + strcpy(config.WiFi_Hostname, root[F("hostname")].as().c_str()); + if (root[F("dhcp")].as()) { + config.WiFi_Dhcp = true; + } else { + config.WiFi_Dhcp = false; + } + Configuration.write(); + + retMsg[F("type")] = F("success"); + retMsg[F("message")] = F("Settings saved!"); + + response->setLength(); + request->send(response); + + WiFiSettings.enableAdminMode(); + WiFiSettings.applyConfig(); +} \ No newline at end of file diff --git a/src/WebApi_ntp.cpp b/src/WebApi_ntp.cpp new file mode 100644 index 0000000..91ef31b --- /dev/null +++ b/src/WebApi_ntp.cpp @@ -0,0 +1,135 @@ +#include "WebApi_ntp.h" +#include "ArduinoJson.h" +#include "AsyncJson.h" +#include "Configuration.h" +#include "helper.h" +#include "NtpSettings.h" + +void WebApiNtpClass::init(AsyncWebServer* server) +{ + using namespace std::placeholders; + + _server = server; + + _server->on("/api/ntp/status", HTTP_GET, std::bind(&WebApiNtpClass::onNtpStatus, this, _1)); + _server->on("/api/ntp/config", HTTP_GET, std::bind(&WebApiNtpClass::onNtpAdminGet, this, _1)); + _server->on("/api/ntp/config", HTTP_POST, std::bind(&WebApiNtpClass::onNtpAdminPost, this, _1)); +} + +void WebApiNtpClass::loop() +{ +} + +void WebApiNtpClass::onNtpStatus(AsyncWebServerRequest* request) +{ + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonObject root = response->getRoot(); + CONFIG_T& config = Configuration.get(); + + root[F("ntp_server")] = config.Ntp_Server; + root[F("ntp_timezone")] = config.Ntp_Timezone; + root[F("ntp_timezone_descr")] = config.Ntp_TimezoneDescr; + + struct tm timeinfo; + if (!getLocalTime(&timeinfo)) { + root[F("ntp_status")] = false; + } else { + root[F("ntp_status")] = true; + } + char timeStringBuff[50]; + strftime(timeStringBuff, sizeof(timeStringBuff), "%A, %B %d %Y %H:%M:%S", &timeinfo); + root[F("ntp_localtime")] = timeStringBuff; + + response->setLength(); + request->send(response); +} + +void WebApiNtpClass::onNtpAdminGet(AsyncWebServerRequest* request) +{ + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonObject root = response->getRoot(); + CONFIG_T& config = Configuration.get(); + + root[F("ntp_server")] = config.Ntp_Server; + root[F("ntp_timezone")] = config.Ntp_Timezone; + root[F("ntp_timezone_descr")] = config.Ntp_TimezoneDescr; + + response->setLength(); + request->send(response); +} + +void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) +{ + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonObject retMsg = response->getRoot(); + retMsg[F("type")] = F("warning"); + + if (!request->hasParam("data", true)) { + retMsg[F("message")] = F("No values found!"); + response->setLength(); + request->send(response); + return; + } + + String json = request->getParam("data", true)->value(); + + if (json.length() > 1024) { + retMsg[F("message")] = F("Data too large!"); + response->setLength(); + request->send(response); + return; + } + + DynamicJsonDocument root(1024); + DeserializationError error = deserializeJson(root, json); + + if (error) { + retMsg[F("message")] = F("Failed to parse data!"); + response->setLength(); + request->send(response); + return; + } + + if (!(root.containsKey("ntp_server") && root.containsKey("ntp_timezone"))) { + retMsg[F("message")] = F("Values are missing!"); + response->setLength(); + request->send(response); + return; + } + + if (root[F("ntp_server")].as().length() == 0 || root[F("ntp_server")].as().length() > NTP_MAX_SERVER_STRLEN) { + retMsg[F("message")] = F("NTP Server must between 1 and " STR(NTP_MAX_SERVER_STRLEN) " characters long!"); + response->setLength(); + request->send(response); + return; + } + + if (root[F("ntp_timezone")].as().length() == 0 || root[F("ntp_timezone")].as().length() > NTP_MAX_TIMEZONE_STRLEN) { + retMsg[F("message")] = F("Timezone must between 1 and " STR(NTP_MAX_TIMEZONE_STRLEN) " characters long!"); + response->setLength(); + request->send(response); + return; + } + + if (root[F("ntp_timezone_descr")].as().length() == 0 || root[F("ntp_timezone_descr")].as().length() > NTP_MAX_TIMEZONEDESCR_STRLEN) { + retMsg[F("message")] = F("Timezone description must between 1 and " STR(NTP_MAX_TIMEZONEDESCR_STRLEN) " characters long!"); + response->setLength(); + request->send(response); + return; + } + + CONFIG_T& config = Configuration.get(); + strcpy(config.Ntp_Server, root[F("ntp_server")].as().c_str()); + strcpy(config.Ntp_Timezone, root[F("ntp_timezone")].as().c_str()); + strcpy(config.Ntp_TimezoneDescr, root[F("ntp_timezone_descr")].as().c_str()); + Configuration.write(); + + retMsg[F("type")] = F("success"); + retMsg[F("message")] = F("Settings saved!"); + + response->setLength(); + request->send(response); + + NtpSettings.setServer(); + NtpSettings.setTimezone(); +} \ No newline at end of file diff --git a/src/WebApi_sysstatus.cpp b/src/WebApi_sysstatus.cpp new file mode 100644 index 0000000..65175e9 --- /dev/null +++ b/src/WebApi_sysstatus.cpp @@ -0,0 +1,59 @@ +#include "WebApi_sysstatus.h" +#include "ArduinoJson.h" +#include "AsyncJson.h" +#include "Configuration.h" +#include +#include + +void WebApiSysstatusClass::init(AsyncWebServer* server) +{ + using namespace std::placeholders; + + _server = server; + + _server->on("/api/system/status", HTTP_GET, std::bind(&WebApiSysstatusClass::onSystemStatus, this, _1)); +} + +void WebApiSysstatusClass::loop() +{ +} + +void WebApiSysstatusClass::onSystemStatus(AsyncWebServerRequest* request) +{ + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonObject root = response->getRoot(); + + root[F("hostname")] = WiFi.getHostname(); + + root[F("sdkversion")] = ESP.getSdkVersion(); + root[F("cpufreq")] = ESP.getCpuFreqMHz(); + + root[F("heap_total")] = ESP.getHeapSize(); + root[F("heap_used")] = ESP.getHeapSize() - ESP.getFreeHeap(); + root[F("sketch_total")] = ESP.getFreeSketchSpace(); + root[F("sketch_used")] = ESP.getSketchSize(); + root[F("littlefs_total")] = LittleFS.totalBytes(); + root[F("littlefs_used")] = LittleFS.usedBytes(); + + root[F("chiprevision")] = ESP.getChipRevision(); + root[F("chipmodel")] = ESP.getChipModel(); + root[F("chipcores")] = ESP.getChipCores(); + + String reason; + reason = ResetReason.get_reset_reason_verbose(0); + root[F("resetreason_0")] = reason; + + reason = ResetReason.get_reset_reason_verbose(1); + root[F("resetreason_1")] = reason; + + root[F("cfgsavecount")] = Configuration.get().Cfg_SaveCount; + + char version[16]; + sprintf(version, "%d.%d.%d", CONFIG_VERSION >> 24 & 0xff, CONFIG_VERSION >> 16 & 0xff, CONFIG_VERSION >> 8 & 0xff); + root[F("firmware_version")] = version; + + root[F("uptime")] = esp_timer_get_time() / 1000000; + + response->setLength(); + request->send(response); +} \ No newline at end of file diff --git a/src/WebApi_webapp.cpp b/src/WebApi_webapp.cpp new file mode 100644 index 0000000..8c100da --- /dev/null +++ b/src/WebApi_webapp.cpp @@ -0,0 +1,51 @@ +#include "WebApi_webapp.h" + +extern const uint8_t file_index_html_start[] asm("_binary_data_index_html_gz_start"); +extern const uint8_t file_favicon_ico_start[] asm("_binary_data_favicon_ico_start"); +extern const uint8_t file_zones_json_start[] asm("_binary_data_zones_json_gz_start"); +extern const uint8_t file_app_js_start[] asm("_binary_data_js_app_js_gz_start"); + +extern const uint8_t file_index_html_end[] asm("_binary_data_index_html_gz_end"); +extern const uint8_t file_favicon_ico_end[] asm("_binary_data_favicon_ico_end"); +extern const uint8_t file_zones_json_end[] asm("_binary_data_zones_json_gz_end"); +extern const uint8_t file_app_js_end[] asm("_binary_data_js_app_js_gz_end"); + +void WebApiWebappClass::init(AsyncWebServer* server) +{ + using namespace std::placeholders; + + _server = server; + + _server->on("/", HTTP_GET, [](AsyncWebServerRequest* request) { + AsyncWebServerResponse* response = request->beginResponse_P(200, "text/html", file_index_html_start, file_index_html_end - file_index_html_start); + response->addHeader("Content-Encoding", "gzip"); + request->send(response); + }); + + _server->on("/index.html", HTTP_GET, [](AsyncWebServerRequest* request) { + AsyncWebServerResponse* response = request->beginResponse_P(200, "text/html", file_index_html_start, file_index_html_end - file_index_html_start); + response->addHeader("Content-Encoding", "gzip"); + request->send(response); + }); + + _server->on("/favicon.ico", HTTP_GET, [](AsyncWebServerRequest* request) { + AsyncWebServerResponse* response = request->beginResponse_P(200, "image/x-icon", file_favicon_ico_start, file_favicon_ico_end - file_favicon_ico_start); + request->send(response); + }); + + _server->on("/zones.json", HTTP_GET, [](AsyncWebServerRequest* request) { + AsyncWebServerResponse* response = request->beginResponse_P(200, "application/json", file_zones_json_start, file_zones_json_end - file_zones_json_start); + response->addHeader("Content-Encoding", "gzip"); + request->send(response); + }); + + _server->on("/js/app.js", HTTP_GET, [](AsyncWebServerRequest* request) { + AsyncWebServerResponse* response = request->beginResponse_P(200, "text/javascript", file_app_js_start, file_app_js_end - file_app_js_start); + response->addHeader("Content-Encoding", "gzip"); + request->send(response); + }); +} + +void WebApiWebappClass::loop() +{ +} \ No newline at end of file