Add API endpoint to retrieve custom languages and complete language pack

This commit is contained in:
Thomas Basler 2024-10-16 21:25:29 +02:00
parent 8257eb7aa2
commit e29b86e4dc
10 changed files with 239 additions and 1 deletions

27
include/I18n.h Normal file
View File

@ -0,0 +1,27 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <TaskSchedulerDeclarations.h>
#include <WString.h>
#include <list>
struct LanguageInfo_t {
String code;
String name;
String filename;
};
class I18nClass {
public:
I18nClass();
void init(Scheduler& scheduler);
std::list<LanguageInfo_t> getAvailableLanguages();
private:
void readLangPacks();
void readConfig(String file);
std::list<LanguageInfo_t> _availLanguages;
};
extern I18nClass I18n;

View File

@ -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);
};

View File

@ -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;

14
include/WebApi_i18n.h Normal file
View File

@ -0,0 +1,14 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <ESPAsyncWebServer.h>
#include <TaskSchedulerDeclarations.h>
class WebApiI18nClass {
public:
void init(AsyncWebServer& server, Scheduler& scheduler);
private:
void onI18nLanguages(AsyncWebServerRequest* request);
void onI18nLanguage(AsyncWebServerRequest* request);
};

View File

@ -108,3 +108,5 @@
#define LED_BRIGHTNESS 100U
#define MAX_INVERTER_LIMIT 2250
#define LANG_PACK_SUFFIX ".lang.json"

72
src/I18n.cpp Normal file
View File

@ -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 <ArduinoJson.h>
#include <LittleFS.h>
I18nClass I18n;
I18nClass::I18nClass()
{
}
void I18nClass::init(Scheduler& scheduler)
{
readLangPacks();
}
std::list<LanguageInfo_t> 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();
}

View File

@ -7,6 +7,7 @@
#include "MessageOutput.h"
#include "PinMapping.h"
#include <LittleFS.h>
#include <MD5Builder.h>
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();
}

View File

@ -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);

81
src/WebApi_i18n.cpp Normal file
View File

@ -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 <AsyncJson.h>
#include <LittleFS.h>
#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<JsonObject>();
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;
}

View File

@ -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 <Arduino.h>
#include <LittleFS.h>
#include <SpiManager.h>
#include <TaskScheduler.h>
#include <esp_heap_caps.h>
#include <SpiManager.h>
#include <driver/uart.h>
@ -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))) {