From e29b86e4dc86dbd2d190eb252b3121dfdeb77b43 Mon Sep 17 00:00:00 2001 From: Thomas Basler Date: Wed, 16 Oct 2024 21:25:29 +0200 Subject: [PATCH] Add API endpoint to retrieve custom languages and complete language pack --- include/I18n.h | 27 +++++++++++++++ include/Utils.h | 1 + include/WebApi.h | 2 ++ include/WebApi_i18n.h | 14 ++++++++ include/defaults.h | 2 ++ src/I18n.cpp | 72 ++++++++++++++++++++++++++++++++++++++ src/Utils.cpp | 32 +++++++++++++++++ src/WebApi.cpp | 1 + src/WebApi_i18n.cpp | 81 +++++++++++++++++++++++++++++++++++++++++++ src/main.cpp | 8 ++++- 10 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 include/I18n.h create mode 100644 include/WebApi_i18n.h create mode 100644 src/I18n.cpp create mode 100644 src/WebApi_i18n.cpp diff --git a/include/I18n.h b/include/I18n.h new file mode 100644 index 0000000..07e8945 --- /dev/null +++ b/include/I18n.h @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include +#include + +struct LanguageInfo_t { + String code; + String name; + String filename; +}; + +class I18nClass { +public: + I18nClass(); + void init(Scheduler& scheduler); + std::list getAvailableLanguages(); + +private: + void readLangPacks(); + void readConfig(String file); + + std::list _availLanguages; +}; + +extern I18nClass I18n; diff --git a/include/Utils.h b/include/Utils.h index 6645f49..759402b 100644 --- a/include/Utils.h +++ b/include/Utils.h @@ -11,4 +11,5 @@ public: static int getTimezoneOffset(); static bool checkJsonAlloc(const JsonDocument& doc, const char* function, const uint16_t line); static void removeAllFiles(); + static String generateMd5FromFile(String file); }; diff --git a/include/WebApi.h b/include/WebApi.h index 2e53058..6e85baf 100644 --- a/include/WebApi.h +++ b/include/WebApi.h @@ -9,6 +9,7 @@ #include "WebApi_file.h" #include "WebApi_firmware.h" #include "WebApi_gridprofile.h" +#include "WebApi_i18n.h" #include "WebApi_inverter.h" #include "WebApi_limit.h" #include "WebApi_maintenance.h" @@ -53,6 +54,7 @@ private: WebApiFileClass _webApiFile; WebApiFirmwareClass _webApiFirmware; WebApiGridProfileClass _webApiGridprofile; + WebApiI18nClass _webApiI18n; WebApiInverterClass _webApiInverter; WebApiLimitClass _webApiLimit; WebApiMaintenanceClass _webApiMaintenance; diff --git a/include/WebApi_i18n.h b/include/WebApi_i18n.h new file mode 100644 index 0000000..237e447 --- /dev/null +++ b/include/WebApi_i18n.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include + +class WebApiI18nClass { +public: + void init(AsyncWebServer& server, Scheduler& scheduler); + +private: + void onI18nLanguages(AsyncWebServerRequest* request); + void onI18nLanguage(AsyncWebServerRequest* request); +}; diff --git a/include/defaults.h b/include/defaults.h index ee3f7b2..5655d72 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -108,3 +108,5 @@ #define LED_BRIGHTNESS 100U #define MAX_INVERTER_LIMIT 2250 + +#define LANG_PACK_SUFFIX ".lang.json" diff --git a/src/I18n.cpp b/src/I18n.cpp new file mode 100644 index 0000000..4df7256 --- /dev/null +++ b/src/I18n.cpp @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2024 Thomas Basler and others + */ +#include "I18n.h" +#include "MessageOutput.h" +#include "Utils.h" +#include "defaults.h" +#include +#include + +I18nClass I18n; + +I18nClass::I18nClass() +{ +} + +void I18nClass::init(Scheduler& scheduler) +{ + readLangPacks(); +} + +std::list I18nClass::getAvailableLanguages() +{ + return _availLanguages; +} + +void I18nClass::readLangPacks() +{ + auto root = LittleFS.open("/"); + auto file = root.getNextFileName(); + + while (file != "") { + if (file.endsWith(LANG_PACK_SUFFIX)) { + MessageOutput.printf("Read File %s\r\n", file.c_str()); + readConfig(file); + } + file = root.getNextFileName(); + } + root.close(); +} + +void I18nClass::readConfig(String file) +{ + JsonDocument filter; + filter["meta"] = true; + + File f = LittleFS.open(file, "r", false); + + JsonDocument doc; + + // Deserialize the JSON document + const DeserializationError error = deserializeJson(doc, f, DeserializationOption::Filter(filter)); + if (error) { + MessageOutput.printf("Failed to read file %s\r\n", file.c_str()); + f.close(); + return; + } + + if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) { + return; + } + + LanguageInfo_t lang; + lang.code = String(doc["meta"]["code"]); + lang.name = String(doc["meta"]["name"]); + lang.filename = file; + + _availLanguages.push_back(lang); + + f.close(); +} diff --git a/src/Utils.cpp b/src/Utils.cpp index 16b6398..bcbb49c 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -7,6 +7,7 @@ #include "MessageOutput.h" #include "PinMapping.h" #include +#include uint32_t Utils::getChipId() { @@ -80,3 +81,34 @@ void Utils::removeAllFiles() file = root.getNextFileName(); } } + +String Utils::generateMd5FromFile(String file) +{ + if (!LittleFS.exists(file)) { + return String(); + } + + File f = LittleFS.open(file, "r"); + if (!file) { + return String(); + } + + MD5Builder md5; + md5.begin(); + + // Read the file in chunks to avoid using too much memory + const size_t bufferSize = 512; + uint8_t buffer[bufferSize]; + + while (f.available()) { + size_t bytesRead = f.read(buffer, bufferSize); + md5.add(buffer, bytesRead); + } + + // Finalize and calculate the MD5 hash + md5.calculate(); + + f.close(); + + return md5.toString(); +} diff --git a/src/WebApi.cpp b/src/WebApi.cpp index 35b1fda..7b78d98 100644 --- a/src/WebApi.cpp +++ b/src/WebApi.cpp @@ -22,6 +22,7 @@ void WebApiClass::init(Scheduler& scheduler) _webApiFile.init(_server, scheduler); _webApiFirmware.init(_server, scheduler); _webApiGridprofile.init(_server, scheduler); + _webApiI18n.init(_server, scheduler); _webApiInverter.init(_server, scheduler); _webApiLimit.init(_server, scheduler); _webApiMaintenance.init(_server, scheduler); diff --git a/src/WebApi_i18n.cpp b/src/WebApi_i18n.cpp new file mode 100644 index 0000000..1673501 --- /dev/null +++ b/src/WebApi_i18n.cpp @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2024 Thomas Basler and others + */ +#include "WebApi_i18n.h" +#include "I18n.h" +#include "Utils.h" +#include "WebApi.h" +#include +#include + +#include "MessageOutput.h" + +void WebApiI18nClass::init(AsyncWebServer& server, Scheduler& scheduler) +{ + using std::placeholders::_1; + + server.on("/api/i18n/languages", HTTP_GET, std::bind(&WebApiI18nClass::onI18nLanguages, this, _1)); + server.on("/api/i18n/language", HTTP_GET, std::bind(&WebApiI18nClass::onI18nLanguage, this, _1)); +} + +void WebApiI18nClass::onI18nLanguages(AsyncWebServerRequest* request) +{ + AsyncJsonResponse* response = new AsyncJsonResponse(true); + auto& root = response->getRoot(); + const auto& languages = I18n.getAvailableLanguages(); + + for (auto& language : languages) { + auto jsonLang = root.add(); + + jsonLang["code"] = language.code; + jsonLang["name"] = language.name; + } + + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); +} + +void WebApiI18nClass::onI18nLanguage(AsyncWebServerRequest* request) +{ + if (request->hasParam("code")) { + String code = request->getParam("code")->value(); + + const auto& languages = I18n.getAvailableLanguages(); + auto it = std::find_if(languages.begin(), languages.end(), [code](const LanguageInfo_t& elem) { + return elem.code == code; + }); + + if (it != languages.end()) { + String md5 = Utils::generateMd5FromFile(it->filename); + + String expectedEtag; + expectedEtag = "\""; + expectedEtag += md5; + expectedEtag += "\""; + + bool eTagMatch = false; + if (request->hasHeader("If-None-Match")) { + const AsyncWebHeader* h = request->getHeader("If-None-Match"); + eTagMatch = h->value().equals(expectedEtag); + } + + // begin response 200 or 304 + AsyncWebServerResponse* response; + if (eTagMatch) { + response = request->beginResponse(304); + } else { + response = request->beginResponse(LittleFS, it->filename, asyncsrv::T_application_json); + } + + // HTTP requires cache headers in 200 and 304 to be identical + response->addHeader("Cache-Control", "public, must-revalidate"); + response->addHeader("ETag", expectedEtag); + + request->send(response); + return; + } + } + + request->send(404); + return; +} diff --git a/src/main.cpp b/src/main.cpp index 92e35da..7f4ac56 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -5,6 +5,7 @@ #include "Configuration.h" #include "Datastore.h" #include "Display_Graphic.h" +#include "I18n.h" #include "InverterSettings.h" #include "Led_Single.h" #include "MessageOutput.h" @@ -24,9 +25,9 @@ #include "defaults.h" #include #include +#include #include #include -#include #include @@ -83,6 +84,11 @@ void setup() auto& config = Configuration.get(); MessageOutput.println("done"); + // Read languate pack + MessageOutput.print("Reading language pack... "); + I18n.init(scheduler); + MessageOutput.println("done"); + // Load PinMapping MessageOutput.print("Reading PinMapping... "); if (PinMapping.init(String(Configuration.get().Dev_PinMapping))) {