diff --git a/include/Configuration.h b/include/Configuration.h index f4dc09f..4253964 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -3,7 +3,7 @@ #include #define CONFIG_FILENAME "/config.bin" -#define CONFIG_VERSION 0x00010700 // 0.1.7 // make sure to clean all after change +#define CONFIG_VERSION 0x00010800 // 0.1.8 // make sure to clean all after change #define WIFI_MAX_SSID_STRLEN 31 #define WIFI_MAX_PASSWORD_STRLEN 31 @@ -19,6 +19,14 @@ #define MQTT_MAX_TOPIC_STRLEN 32 #define MQTT_MAX_LWTVALUE_STRLEN 20 +#define INV_MAX_NAME_STRLEN 31 +#define INV_MAX_COUNT 10 + +struct INVERTER_CONFIG_T { + uint64_t Serial; + char Name[INV_MAX_NAME_STRLEN + 1]; +}; + struct CONFIG_T { uint32_t Cfg_Version; uint Cfg_SaveCount; @@ -47,6 +55,8 @@ struct CONFIG_T { char Mqtt_LwtTopic[MQTT_MAX_TOPIC_STRLEN + 1]; char Mqtt_LwtValue_Online[MQTT_MAX_LWTVALUE_STRLEN + 1]; char Mqtt_LwtValue_Offline[MQTT_MAX_LWTVALUE_STRLEN + 1]; + + INVERTER_CONFIG_T Inverter[INV_MAX_COUNT]; }; class ConfigurationClass { @@ -56,6 +66,8 @@ public: bool write(); void migrate(); CONFIG_T& get(); + + INVERTER_CONFIG_T* getFreeInverterSlot(); }; extern ConfigurationClass Configuration; \ No newline at end of file diff --git a/include/WebApi.h b/include/WebApi.h index da512b3..b6ce46b 100644 --- a/include/WebApi.h +++ b/include/WebApi.h @@ -28,6 +28,11 @@ private: 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); }; extern WebApiClass WebApi; \ No newline at end of file diff --git a/src/Configuration.cpp b/src/Configuration.cpp index 6988179..a68f560 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -32,6 +32,11 @@ void ConfigurationClass::init() strlcpy(config.Mqtt_LwtTopic, MQTT_LWT_TOPIC, sizeof(config.Mqtt_LwtTopic)); strlcpy(config.Mqtt_LwtValue_Online, MQTT_LWT_ONLINE, sizeof(config.Mqtt_LwtValue_Online)); strlcpy(config.Mqtt_LwtValue_Offline, MQTT_LWT_OFFLINE, sizeof(config.Mqtt_LwtValue_Offline)); + + for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { + config.Inverter[i].Serial = 0; + strlcpy(config.Inverter[i].Name, "", 0); + } } bool ConfigurationClass::write() @@ -90,6 +95,13 @@ void ConfigurationClass::migrate() strlcpy(config.Mqtt_LwtValue_Offline, MQTT_LWT_OFFLINE, sizeof(config.Mqtt_LwtValue_Offline)); } + if (config.Cfg_Version < 0x00010800) { + for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { + config.Inverter[i].Serial = 0; + strlcpy(config.Inverter[i].Name, "", 0); + } + } + config.Cfg_Version = CONFIG_VERSION; write(); } @@ -99,4 +111,15 @@ CONFIG_T& ConfigurationClass::get() return config; } +INVERTER_CONFIG_T* ConfigurationClass::getFreeInverterSlot() +{ + for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { + if (config.Inverter[i].Serial == 0) { + return &config.Inverter[i]; + } + } + + return NULL; +} + ConfigurationClass Configuration; \ No newline at end of file diff --git a/src/WebApi.cpp b/src/WebApi.cpp index 2211f4a..d076ee3 100644 --- a/src/WebApi.cpp +++ b/src/WebApi.cpp @@ -40,6 +40,11 @@ void WebApiClass::init() _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.serveStatic("/", LittleFS, "/", "max-age=86400").setDefaultFile("index.html"); _server.onNotFound(std::bind(&WebApiClass::onNotFound, this, _1)); _server.begin(); @@ -554,4 +559,228 @@ void WebApiClass::onMqttAdminPost(AsyncWebServerRequest* request) 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("serial")] = config.Inverter[i].Serial; + obj[F("name")] = String(config.Inverter[i].Name); + } + } + + 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; + } + + inverter->Serial = root[F("serial")].as(); + 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); +} + +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()]; + inverter.Serial = root[F("serial")].as(); + 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); +} + +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; + } + + INVERTER_CONFIG_T& inverter = Configuration.get().Inverter[root[F("id")].as()]; + 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); +} + WebApiClass WebApi; \ No newline at end of file