diff --git a/include/Configuration.h b/include/Configuration.h index 938fed08..cf2ce5b4 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -8,7 +8,7 @@ #include #define CONFIG_FILENAME "/config.json" -#define CONFIG_VERSION 0x00011d00 // 0.1.29 // make sure to clean all after change +#define CONFIG_VERSION 0x00011e00 // 0.1.30 // make sure to clean all after change #define WIFI_MAX_SSID_STRLEN 32 #define WIFI_MAX_PASSWORD_STRLEN 64 @@ -35,6 +35,9 @@ #define DEV_MAX_MAPPING_NAME_STRLEN 63 #define LOCALE_STRLEN 2 +#define LOG_MODULE_COUNT 16 +#define LOG_MODULE_NAME_STRLEN 32 + struct CHANNEL_CONFIG_T { uint16_t MaxChannelPower; char Name[CHAN_MAX_NAME_STRLEN]; @@ -161,6 +164,14 @@ struct CONFIG_T { INVERTER_CONFIG_T Inverter[INV_MAX_COUNT]; char Dev_PinMapping[DEV_MAX_MAPPING_NAME_STRLEN + 1]; + + struct { + int8_t Default; + struct { + char Name[LOG_MODULE_NAME_STRLEN + 1]; + int8_t Level; + } Modules[LOG_MODULE_COUNT]; + } Logging; }; class ConfigurationClass { @@ -187,6 +198,8 @@ public: INVERTER_CONFIG_T* getInverterConfig(const uint64_t serial); void deleteInverterById(const uint8_t id); + int8_t getIndexForLogModule(const String& moduleName) const; + private: void loop(); diff --git a/include/Logging.h b/include/Logging.h new file mode 100644 index 00000000..0607e766 --- /dev/null +++ b/include/Logging.h @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include + +class LoggingClass { +public: + LoggingClass(); + void applyLogLevels(); + const std::vector& getConfigurableModules() const; + +private: + std::vector _configurableModules; +}; + +extern LoggingClass Logging; diff --git a/include/WebApi.h b/include/WebApi.h index 6e85bafd..61f652ca 100644 --- a/include/WebApi.h +++ b/include/WebApi.h @@ -12,6 +12,7 @@ #include "WebApi_i18n.h" #include "WebApi_inverter.h" #include "WebApi_limit.h" +#include "WebApi_logging.h" #include "WebApi_maintenance.h" #include "WebApi_mqtt.h" #include "WebApi_network.h" @@ -57,6 +58,7 @@ private: WebApiI18nClass _webApiI18n; WebApiInverterClass _webApiInverter; WebApiLimitClass _webApiLimit; + WebApiLoggingClass _webApiLogging; WebApiMaintenanceClass _webApiMaintenance; WebApiMqttClass _webApiMqtt; WebApiNetworkClass _webApiNetwork; diff --git a/include/WebApi_logging.h b/include/WebApi_logging.h new file mode 100644 index 00000000..d42b3f4c --- /dev/null +++ b/include/WebApi_logging.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include + +class WebApiLoggingClass { +public: + void init(AsyncWebServer& server, Scheduler& scheduler); + +private: + void onLoggingAdminGet(AsyncWebServerRequest* request); + void onLoggingAdminPost(AsyncWebServerRequest* request); +}; diff --git a/lang/el.lang.json b/lang/el.lang.json index 417e3121..621175e9 100644 --- a/lang/el.lang.json +++ b/lang/el.lang.json @@ -22,6 +22,7 @@ "MQTTSettings": "MQTT Ρυθμίσεις", "InverterSettings": "Ρυθμίσεις Μετατροπέα", "SecuritySettings": "Ρυθμίσεις Ασφάλειας", + "LoggingSettings": "Logging Settings", "DTUSettings": "DTU Ρυθμίσεις", "DeviceManager": "Διαχείριση Συσκευών", "ConfigManagement": "Διαχείριση διαμόρφωσης", @@ -700,6 +701,19 @@ "format_herf_valid": "Μορφή E-Star HERF (θα αποθηκευτεί μετατρεπόμενη): {serial}", "format_herf_invalid": "Μορφή E-Star HERF: Μη έγκυρο άθροισμα ελέγχου", "format_unknown": "Άγνωστη μορφή" + }, + "loggingadmin": { + "LoggingSettings": "Logging Settings", + "LogLevel": "Log Level", + "DefaultLevel": "Default log level", + "Module": "Module", + "log_inherit": "Inherit from default", + "log_none": "None", + "log_error": "(E) Error", + "log_warn": "(W) Warning", + "log_info": "(I) Info", + "log_debug": "(D) Debug", + "log_verbose": "(V) Verbose" } } } diff --git a/lang/es.lang.json b/lang/es.lang.json index f5108ae1..2bb7ffe8 100644 --- a/lang/es.lang.json +++ b/lang/es.lang.json @@ -22,6 +22,7 @@ "MQTTSettings": "Ajustes MQTT", "InverterSettings": "Ajustes Inversor", "SecuritySettings": "Ajustes Seguridad", + "LoggingSettings": "Logging Settings", "DTUSettings": "Ajustes DTU", "DeviceManager": "Administrador Dispositivos", "ConfigManagement": "Gestión configuración", @@ -700,6 +701,19 @@ "format_herf_valid": "E-Star HERF format (will be saved converted): {serial}", "format_herf_invalid": "E-Star HERF format: Invalid checksum", "format_unknown": "Unknown format" + }, + "loggingadmin": { + "LoggingSettings": "Logging Settings", + "LogLevel": "Log Level", + "DefaultLevel": "Default log level", + "Module": "Module", + "log_inherit": "Inherit from default", + "log_none": "None", + "log_error": "(E) Error", + "log_warn": "(W) Warning", + "log_info": "(I) Info", + "log_debug": "(D) Debug", + "log_verbose": "(V) Verbose" } } } diff --git a/lang/it.lang.json b/lang/it.lang.json index 8b4a10bf..ebe9da74 100644 --- a/lang/it.lang.json +++ b/lang/it.lang.json @@ -22,6 +22,7 @@ "MQTTSettings": "Impostazioni MQTT", "InverterSettings": "Impostazioni Inverter", "SecuritySettings": "Impostazioni di Sicurezza", + "LoggingSettings": "Logging Settings", "DTUSettings": "Impostazioni DTU", "DeviceManager": "Gestione Dispositivi", "ConfigManagement": "Gestione Configurazione", @@ -700,6 +701,19 @@ "format_herf_valid": "E-Star HERF format (will be saved converted): {serial}", "format_herf_invalid": "E-Star HERF format: Invalid checksum", "format_unknown": "Unknown format" + }, + "loggingadmin": { + "LoggingSettings": "Logging Settings", + "LogLevel": "Log Level", + "DefaultLevel": "Default log level", + "Module": "Module", + "log_inherit": "Inherit from default", + "log_none": "None", + "log_error": "(E) Error", + "log_warn": "(W) Warning", + "log_info": "(I) Info", + "log_debug": "(D) Debug", + "log_verbose": "(V) Verbose" } } } diff --git a/lang/pl.lang.json b/lang/pl.lang.json index ae637b99..1c77ad1b 100644 --- a/lang/pl.lang.json +++ b/lang/pl.lang.json @@ -22,6 +22,7 @@ "MQTTSettings": "Ustawienia serwera MQTT", "InverterSettings": "Ustawienia falownika", "SecuritySettings": "Ustawienia zabezpieczeń", + "LoggingSettings": "Logging Settings", "DTUSettings": "Ustawienia DTU", "DeviceManager": "Zarządzanie urządzeniami", "ConfigManagement": "Zarządzanie konfiguracją", @@ -700,6 +701,19 @@ "format_herf_valid": "Format E-Star HERF (zostanie zapisany po konwersji): {serial}", "format_herf_invalid": "Format E-Star HERF: Nieprawidłowa suma kontrolna", "format_unknown": "Nieznany format" + }, + "loggingadmin": { + "LoggingSettings": "Logging Settings", + "LogLevel": "Log Level", + "DefaultLevel": "Default log level", + "Module": "Module", + "log_inherit": "Inherit from default", + "log_none": "None", + "log_error": "(E) Error", + "log_warn": "(W) Warning", + "log_info": "(I) Info", + "log_debug": "(D) Debug", + "log_verbose": "(V) Verbose" } } } diff --git a/src/Configuration.cpp b/src/Configuration.cpp index 6b74c019..e4f6748f 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -8,6 +8,7 @@ #include "defaults.h" #include #include +#include #include #undef TAG @@ -153,6 +154,15 @@ bool ConfigurationClass::write() } } + JsonObject logging = doc["logging"].to(); + logging["default"] = config.Logging.Default; + JsonArray modules = logging["modules"].to(); + for (uint8_t i = 0; i < LOG_MODULE_COUNT; i++) { + JsonObject module = modules.add(); + module["level"] = config.Logging.Modules[i].Level; + module["name"] = config.Logging.Modules[i].Name; + } + if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) { return false; } @@ -329,6 +339,15 @@ bool ConfigurationClass::read() } } + JsonObject logging = doc["logging"]; + config.Logging.Default = logging["default"] | ESP_LOG_ERROR; + JsonArray modules = logging["modules"]; + for (uint8_t i = 0; i < LOG_MODULE_COUNT; i++) { + JsonObject module = modules[i].as(); + strlcpy(config.Logging.Modules[i].Name, module["name"] | "", sizeof(config.Logging.Modules[i].Name)); + config.Logging.Modules[i].Level = module["level"] | ESP_LOG_VERBOSE; + } + f.close(); // Check for default DTU serial @@ -426,6 +445,12 @@ void ConfigurationClass::migrate() } } + if (config.Cfg.Version < 0x00011e00) { + config.Logging.Default = ESP_LOG_VERBOSE; + strlcpy(config.Logging.Modules[0].Name, "CORE", sizeof(config.Logging.Modules[0].Name)); + config.Logging.Modules[0].Level = ESP_LOG_ERROR; + } + f.close(); config.Cfg.Version = CONFIG_VERSION; @@ -487,6 +512,17 @@ void ConfigurationClass::deleteInverterById(const uint8_t id) } } +int8_t ConfigurationClass::getIndexForLogModule(const String& moduleName) const +{ + for (uint8_t i = 0; i < LOG_MODULE_COUNT; i++) { + if (strcmp(config.Logging.Modules[i].Name, moduleName.c_str()) == 0) { + return i; + } + } + + return -1; +} + void ConfigurationClass::loop() { std::unique_lock lock(sWriterMutex); diff --git a/src/Logging.cpp b/src/Logging.cpp new file mode 100644 index 00000000..8bdb0216 --- /dev/null +++ b/src/Logging.cpp @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2025 Thomas Basler and others + */ +#include "Logging.h" +#include "Configuration.h" + +LoggingClass Logging; + +LoggingClass::LoggingClass() +{ + _configurableModules.reserve(3); + _configurableModules.push_back("CORE"); + _configurableModules.push_back("hoymiles"); + _configurableModules.push_back("mqtt"); + _configurableModules.push_back("network"); + _configurableModules.push_back("webapi"); +} + +const std::vector& LoggingClass::getConfigurableModules() const +{ + return _configurableModules; +} + +void LoggingClass::applyLogLevels() +{ + const CONFIG_T& config = Configuration.get(); + + esp_log_level_set("*", static_cast(config.Logging.Default)); + for (int8_t i = 0; i < LOG_MODULE_COUNT; i++) { + if (strlen(config.Logging.Modules[i].Name) == 0 || config.Logging.Modules[i].Level < ESP_LOG_NONE || config.Logging.Modules[i].Level > ESP_LOG_VERBOSE) { + continue; + } + + esp_log_level_set(config.Logging.Modules[i].Name, static_cast(config.Logging.Modules[i].Level)); + } +} diff --git a/src/WebApi.cpp b/src/WebApi.cpp index 11c3e2ac..5f44a89f 100644 --- a/src/WebApi.cpp +++ b/src/WebApi.cpp @@ -27,6 +27,7 @@ void WebApiClass::init(Scheduler& scheduler) _webApiI18n.init(_server, scheduler); _webApiInverter.init(_server, scheduler); _webApiLimit.init(_server, scheduler); + _webApiLogging.init(_server, scheduler); _webApiMaintenance.init(_server, scheduler); _webApiMqtt.init(_server, scheduler); _webApiNetwork.init(_server, scheduler); diff --git a/src/WebApi_logging.cpp b/src/WebApi_logging.cpp new file mode 100644 index 00000000..2f0f00ca --- /dev/null +++ b/src/WebApi_logging.cpp @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2025 Thomas Basler and others + */ +#include "WebApi_logging.h" +#include "Configuration.h" +#include "Logging.h" +#include "WebApi.h" +#include "WebApi_errors.h" +#include "helper.h" +#include + +#include + +void WebApiLoggingClass::init(AsyncWebServer& server, Scheduler& scheduler) +{ + using std::placeholders::_1; + + server.on("/api/logging/config", HTTP_GET, std::bind(&WebApiLoggingClass::onLoggingAdminGet, this, _1)); + server.on("/api/logging/config", HTTP_POST, std::bind(&WebApiLoggingClass::onLoggingAdminPost, this, _1)); +} + +void WebApiLoggingClass::onLoggingAdminGet(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentials(request)) { + return; + } + + AsyncJsonResponse* response = new AsyncJsonResponse(); + auto& root = response->getRoot(); + const CONFIG_T& config = Configuration.get(); + + auto& configurableModules = Logging.getConfigurableModules(); + + JsonObject loglevel = root["loglevel"].to(); + loglevel["default"] = config.Logging.Default; + + JsonArray logModules = loglevel["modules"].to(); + for (const auto& availModule : configurableModules) { + JsonObject logModule = logModules.add(); + logModule["name"] = availModule; + + int8_t idx = Configuration.getIndexForLogModule(availModule); + // Set to inherit if unknown + logModule["level"] = idx < 0 || idx > ESP_LOG_VERBOSE ? -1 : config.Logging.Modules[idx].Level; + } + + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); +} + +void WebApiLoggingClass::onLoggingAdminPost(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentials(request)) { + return; + } + + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { + return; + } + + auto& retMsg = response->getRoot(); + auto& configurableModules = Logging.getConfigurableModules(); + + { + auto guard = Configuration.getWriteGuard(); + auto& config = guard.getConfig(); + + config.Logging.Default = std::max(ESP_LOG_NONE, std::min(ESP_LOG_VERBOSE, root["loglevel"]["default"].as())); + + for (uint8_t i = 0; i < LOG_MODULE_COUNT; i++) { + config.Logging.Modules[i].Level = ESP_LOG_NONE; + config.Logging.Modules[i].Name[0] = '\0'; + } + + JsonArray logmodules = root["loglevel"]["modules"].as(); + uint8_t i = 0; + for (const auto& logmodule : logmodules) { + bool isValidModule = std::find(configurableModules.begin(), configurableModules.end(), logmodule["name"] | "") != configurableModules.end(); + if (!isValidModule) { + continue; + } + + int8_t level = std::max(-1, std::min(ESP_LOG_VERBOSE, logmodule["level"].as())); + if (level < ESP_LOG_NONE) { + // Skip modules set to inherit + continue; + } + + config.Logging.Modules[i].Level = level; + strlcpy(config.Logging.Modules[i].Name, logmodule["name"] | "", sizeof(config.Logging.Modules[i].Name)); + + if (++i >= LOG_MODULE_COUNT) { + break; + } + } + } + + Logging.applyLogLevels(); + + WebApi.writeConfig(retMsg); + + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); +} diff --git a/src/main.cpp b/src/main.cpp index c8fa027d..78253060 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -8,6 +8,7 @@ #include "I18n.h" #include "InverterSettings.h" #include "Led_Single.h" +#include "Logging.h" #include "MessageOutput.h" #include "MqttHandleDtu.h" #include "MqttHandleHass.h" @@ -73,6 +74,10 @@ void setup() Configuration.migrate(); } + // Set configured log levels + Logging.applyLogLevels(); + esp_log_level_set(TAG, ESP_LOG_VERBOSE); + // Read languate pack ESP_LOGI(TAG, "Reading language pack..."); I18n.init(scheduler); diff --git a/webapp/src/components/NavBar.vue b/webapp/src/components/NavBar.vue index 2cae5d09..6ef8913e 100644 --- a/webapp/src/components/NavBar.vue +++ b/webapp/src/components/NavBar.vue @@ -63,6 +63,11 @@ >{{ $t('menu.SecuritySettings') }} +
  • + {{ $t('menu.LoggingSettings') }} + +
  • {{ $t('menu.DTUSettings') diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index ba607488..53a81d3c 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -7,6 +7,7 @@ "MQTTSettings": "MQTT", "InverterSettings": "Wechselrichter", "SecuritySettings": "Sicherheit", + "LoggingSettings": "Protokollierung", "DTUSettings": "DTU", "DeviceManager": "Hardware", "ConfigManagement": "Konfigurationsverwaltung", @@ -684,5 +685,18 @@ "format_herf_valid": "E-Star HERF Format (wird konvertiert gespeichert): {serial}", "format_herf_invalid": "E-Star HERF Format: Ungültige Prüfsumme", "format_unknown": "Unbekanntes Format" + }, + "loggingadmin": { + "LoggingSettings": "Protokollierungseinstellungen", + "LogLevel": "Protokollierungsstufe", + "DefaultLevel": "Standard-Protokollierungsstufe", + "Module": "Modul", + "log_inherit": "Von Standard erben", + "log_none": "Keine", + "log_error": "(E) Fehler", + "log_warn": "(W) Warnung", + "log_info": "(I) Info", + "log_debug": "(D) Debug", + "log_verbose": "(V) Ausführlich" } } diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 55ecc4c9..41de105f 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -7,6 +7,7 @@ "MQTTSettings": "MQTT Settings", "InverterSettings": "Inverter Settings", "SecuritySettings": "Security Settings", + "LoggingSettings": "Logging Settings", "DTUSettings": "DTU Settings", "DeviceManager": "Device-Manager", "ConfigManagement": "Config Management", @@ -685,5 +686,18 @@ "format_herf_valid": "E-Star HERF format (will be saved converted): {serial}", "format_herf_invalid": "E-Star HERF format: Invalid checksum", "format_unknown": "Unknown format" + }, + "loggingadmin": { + "LoggingSettings": "Logging Settings", + "LogLevel": "Log Level", + "DefaultLevel": "Default log level", + "Module": "Module", + "log_inherit": "Inherit from default", + "log_none": "None", + "log_error": "(E) Error", + "log_warn": "(W) Warning", + "log_info": "(I) Info", + "log_debug": "(D) Debug", + "log_verbose": "(V) Verbose" } } diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index 47024d00..73afb555 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -7,6 +7,7 @@ "MQTTSettings": "MQTT", "InverterSettings": "Onduleurs", "SecuritySettings": "Sécurité", + "LoggingSettings": "Logging Settings", "DTUSettings": "DTU", "DeviceManager": "Périphériques", "ConfigManagement": "Gestion de la configuration", @@ -666,5 +667,18 @@ "format_herf_valid": "E-Star HERF format (will be saved converted): {serial}", "format_herf_invalid": "E-Star HERF format: Invalid checksum", "format_unknown": "Unknown format" + }, + "loggingadmin": { + "LoggingSettings": "Logging Settings", + "LogLevel": "Log Level", + "DefaultLevel": "Default log level", + "Module": "Module", + "log_inherit": "Inherit from default", + "log_none": "None", + "log_error": "(E) Error", + "log_warn": "(W) Warning", + "log_info": "(I) Info", + "log_debug": "(D) Debug", + "log_verbose": "(V) Verbose" } } diff --git a/webapp/src/router/index.ts b/webapp/src/router/index.ts index cd3bdbec..9f5a68d5 100644 --- a/webapp/src/router/index.ts +++ b/webapp/src/router/index.ts @@ -9,6 +9,7 @@ import HomeView from '@/views/HomeView.vue'; import InverterAdminView from '@/views/InverterAdminView.vue'; import LoginView from '@/views/LoginView.vue'; import MaintenanceRebootView from '@/views/MaintenanceRebootView.vue'; +import LoggingAdminView from '@/views/LoggingAdminView.vue'; import MqttAdminView from '@/views/MqttAdminView.vue'; import MqttInfoView from '@/views/MqttInfoView.vue'; import NetworkAdminView from '@/views/NetworkAdminView.vue'; @@ -121,6 +122,11 @@ const router = createRouter({ name: 'Security', component: SecurityAdminView, }, + { + path: '/settings/logging', + name: 'Logging', + component: LoggingAdminView, + }, { path: '/maintenance/reboot', name: 'Device Reboot', diff --git a/webapp/src/types/LoggingConfig.ts b/webapp/src/types/LoggingConfig.ts new file mode 100644 index 00000000..9a4cac66 --- /dev/null +++ b/webapp/src/types/LoggingConfig.ts @@ -0,0 +1,13 @@ +export interface LogModule { + name: string; + level: number; +} + +export interface LogLevel { + default: number; + modules: Array; +} + +export interface LoggingConfig { + loglevel: LogLevel; +} diff --git a/webapp/src/views/LoggingAdminView.vue b/webapp/src/views/LoggingAdminView.vue new file mode 100644 index 00000000..ebc481b4 --- /dev/null +++ b/webapp/src/views/LoggingAdminView.vue @@ -0,0 +1,125 @@ + + +