diff --git a/docs/DeviceProfiles/wt32-eth01.json b/docs/DeviceProfiles/wt32-eth01.json index ebf874e7..971dec91 100644 --- a/docs/DeviceProfiles/wt32-eth01.json +++ b/docs/DeviceProfiles/wt32-eth01.json @@ -16,7 +16,7 @@ "mdc": 23, "mdio": 18, "type": 0, - "clk_mode": 3 + "clk_mode": 0 } } ] \ No newline at end of file diff --git a/include/WebApi.h b/include/WebApi.h index c7c18839..9f223402 100644 --- a/include/WebApi.h +++ b/include/WebApi.h @@ -35,6 +35,8 @@ public: static bool checkCredentials(AsyncWebServerRequest* request); static bool checkCredentialsReadonly(AsyncWebServerRequest* request); + static void sendTooManyRequests(AsyncWebServerRequest* request); + private: AsyncWebServer _server; AsyncEventSource _events; diff --git a/include/WebApi_prometheus.h b/include/WebApi_prometheus.h index ac2478a0..5eb894ee 100644 --- a/include/WebApi_prometheus.h +++ b/include/WebApi_prometheus.h @@ -3,6 +3,7 @@ #include #include +#include class WebApiPrometheusClass { public: @@ -15,4 +16,28 @@ private: void addField(AsyncResponseStream* stream, String& serial, uint8_t idx, std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, const char* channelName = NULL); AsyncWebServer* _server; + + enum { + METRIC_TYPE_NONE = 0, + METRIC_TYPE_GAUGE, + METRIC_TYPE_COUNTER, + }; + const char* _metricTypes[3] = { 0, "gauge", "counter" }; + + std::map _fieldMetricAssignment { + { FLD_UDC, METRIC_TYPE_GAUGE }, + { FLD_IDC, METRIC_TYPE_GAUGE }, + { FLD_PDC, METRIC_TYPE_GAUGE }, + { FLD_YD, METRIC_TYPE_COUNTER }, + { FLD_YT, METRIC_TYPE_COUNTER }, + { FLD_UAC, METRIC_TYPE_GAUGE }, + { FLD_IAC, METRIC_TYPE_GAUGE }, + { FLD_PAC, METRIC_TYPE_GAUGE }, + { FLD_F, METRIC_TYPE_GAUGE }, + { FLD_T, METRIC_TYPE_GAUGE }, + { FLD_PF, METRIC_TYPE_GAUGE }, + { FLD_EFF, METRIC_TYPE_GAUGE }, + { FLD_IRR, METRIC_TYPE_GAUGE }, + { FLD_PRA, METRIC_TYPE_GAUGE } + }; }; \ No newline at end of file diff --git a/lib/ResetReason/src/ResetReason.cpp b/lib/ResetReason/src/ResetReason.cpp index dbd62712..5e664145 100644 --- a/lib/ResetReason/src/ResetReason.cpp +++ b/lib/ResetReason/src/ResetReason.cpp @@ -34,7 +34,7 @@ String ResetReasonClass::get_reset_reason_verbose(uint8_t cpu_id) case 3: reason_str = F("Software reset digital core"); break; -#ifndef CONFIG_IDF_TARGET_ESP32C3 +#if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) case 4: reason_str = F("Legacy watch dog reset digital core"); break; @@ -42,7 +42,7 @@ String ResetReasonClass::get_reset_reason_verbose(uint8_t cpu_id) case 5: reason_str = F("Deep Sleep reset digital core"); break; -#ifndef CONFIG_IDF_TARGET_ESP32C3 +#if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) case 6: reason_str = F("Reset by SLC module, reset digital core"); break; @@ -68,9 +68,9 @@ String ResetReasonClass::get_reset_reason_verbose(uint8_t cpu_id) case 13: reason_str = F("RTC Watch dog Reset CPU"); break; -#ifndef CONFIG_IDF_TARGET_ESP32C3 +#if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) case 14: - reason_str = F("for APP CPU, reseted by PRO CPU"); + reason_str = F("for APP CPU, reset by PRO CPU"); break; #endif case 15: @@ -100,7 +100,7 @@ String ResetReasonClass::get_reset_reason_short(uint8_t cpu_id) case 3: reason_str = F("SW_RESET"); break; -#ifndef CONFIG_IDF_TARGET_ESP32C3 +#if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) case 4: reason_str = F("OWDT_RESET"); break; @@ -108,7 +108,7 @@ String ResetReasonClass::get_reset_reason_short(uint8_t cpu_id) case 5: reason_str = F("DEEPSLEEP_RESET"); break; -#ifndef CONFIG_IDF_TARGET_ESP32C3 +#if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) case 6: reason_str = F("SDIO_RESET"); break; @@ -134,7 +134,7 @@ String ResetReasonClass::get_reset_reason_short(uint8_t cpu_id) case 13: reason_str = F("RTCWDT_CPU_RESET"); break; -#ifndef CONFIG_IDF_TARGET_ESP32C3 +#if !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) case 14: reason_str = F("EXT_CPU_RESET"); break; @@ -152,4 +152,4 @@ String ResetReasonClass::get_reset_reason_short(uint8_t cpu_id) return reason_str; } -ResetReasonClass ResetReason; \ No newline at end of file +ResetReasonClass ResetReason; diff --git a/src/WebApi.cpp b/src/WebApi.cpp index 3ec2a044..dad4db24 100644 --- a/src/WebApi.cpp +++ b/src/WebApi.cpp @@ -98,4 +98,11 @@ bool WebApiClass::checkCredentialsReadonly(AsyncWebServerRequest* request) } } +void WebApiClass::sendTooManyRequests(AsyncWebServerRequest* request) +{ + auto response = request->beginResponse(429, "text/plain", "Too Many Requests"); + response->addHeader("Retry-After", "60"); + request->send(response); +} + WebApiClass WebApi; \ No newline at end of file diff --git a/src/WebApi_prometheus.cpp b/src/WebApi_prometheus.cpp index 77675ee7..1a92d31f 100644 --- a/src/WebApi_prometheus.cpp +++ b/src/WebApi_prometheus.cpp @@ -5,7 +5,9 @@ */ #include "WebApi_prometheus.h" #include "Configuration.h" +#include "MessageOutput.h" #include "NetworkSettings.h" +#include "WebApi.h" #include #include "MessageOutput.h" @@ -23,10 +25,10 @@ void WebApiPrometheusClass::loop() } void WebApiPrometheusClass::onPrometheusMetricsGet(AsyncWebServerRequest* request) -{ +{ try { auto stream = request->beginResponseStream("text/plain; charset=utf-8", 40960); - + stream->print(F("# HELP opendtu_build Build info\n")); stream->print(F("# TYPE opendtu_build gauge\n")); stream->printf("opendtu_build{name=\"%s\",id=\"%s\",version=\"%d.%d.%d\"} 1\n", @@ -64,39 +66,39 @@ void WebApiPrometheusClass::onPrometheusMetricsGet(AsyncWebServerRequest* reques stream->printf("opendtu_last_update{serial=\"%s\",unit=\"%d\",name=\"%s\"} %d\n", serial.c_str(), i, name, inv->Statistics()->getLastUpdate() / 1000); - // Loop all channels - for (auto& t : inv->Statistics()->getChannelTypes()) { - for (auto& c : inv->Statistics()->getChannelsByType(t)) { - addField(stream, serial, i, inv, t, c, FLD_PAC); - addField(stream, serial, i, inv, t, c, FLD_UAC); - addField(stream, serial, i, inv, t, c, FLD_IAC); - if (t == TYPE_AC) { - addField(stream, serial, i, inv, t, c, FLD_PDC, "PowerDC"); - } else { - addField(stream, serial, i, inv, t, c, FLD_PDC); + // Loop all channels if Statistics have been updated at least once since DTU boot + if (inv->Statistics()->getLastUpdate() > 0) { + for (auto& t : inv->Statistics()->getChannelTypes()) { + for (auto& c : inv->Statistics()->getChannelsByType(t)) { + addField(stream, serial, i, inv, t, c, FLD_PAC); + addField(stream, serial, i, inv, t, c, FLD_UAC); + addField(stream, serial, i, inv, t, c, FLD_IAC); + if (t == TYPE_AC) { + addField(stream, serial, i, inv, t, c, FLD_PDC, "PowerDC"); + } else { + addField(stream, serial, i, inv, t, c, FLD_PDC); + } + addField(stream, serial, i, inv, t, c, FLD_UDC); + addField(stream, serial, i, inv, t, c, FLD_IDC); + addField(stream, serial, i, inv, t, c, FLD_YD); + addField(stream, serial, i, inv, t, c, FLD_YT); + addField(stream, serial, i, inv, t, c, FLD_F); + addField(stream, serial, i, inv, t, c, FLD_T); + addField(stream, serial, i, inv, t, c, FLD_PF); + addField(stream, serial, i, inv, t, c, FLD_PRA); + addField(stream, serial, i, inv, t, c, FLD_EFF); + addField(stream, serial, i, inv, t, c, FLD_IRR); } - addField(stream, serial, i, inv, t, c, FLD_UDC); - addField(stream, serial, i, inv, t, c, FLD_IDC); - addField(stream, serial, i, inv, t, c, FLD_YD); - addField(stream, serial, i, inv, t, c, FLD_YT); - addField(stream, serial, i, inv, t, c, FLD_F); - addField(stream, serial, i, inv, t, c, FLD_T); - addField(stream, serial, i, inv, t, c, FLD_PF); - addField(stream, serial, i, inv, t, c, FLD_PRA); - addField(stream, serial, i, inv, t, c, FLD_EFF); - addField(stream, serial, i, inv, t, c, FLD_IRR); } } } stream->addHeader(F("Cache-Control"), F("no-cache")); request->send(stream); - } - catch (std::bad_alloc& bad_alloc) { + + } catch (std::bad_alloc& bad_alloc) { MessageOutput.printf("Call to /api/prometheus/metrics temporarely out of resources. Reason: \"%s\".\r\n", bad_alloc.what()); - - auto response = request->beginResponse(429, "text/plain", "Too Many Requests"); - response->addHeader("Retry-After", "60"); - request->send(response); + + WebApi.sendTooManyRequests(request); } } @@ -106,7 +108,7 @@ void WebApiPrometheusClass::addField(AsyncResponseStream* stream, String& serial const char* chanName = (channelName == NULL) ? inv->Statistics()->getChannelFieldName(type, channel, fieldId) : channelName; if (idx == 0 && type == TYPE_AC && channel == 0) { stream->printf("# HELP opendtu_%s in %s\n", chanName, inv->Statistics()->getChannelFieldUnit(type, channel, fieldId)); - stream->printf("# TYPE opendtu_%s gauge\n", chanName); + stream->printf("# TYPE opendtu_%s %s\n", chanName, _metricTypes[_fieldMetricAssignment[fieldId]]); } stream->printf("opendtu_%s{serial=\"%s\",unit=\"%d\",name=\"%s\",type=\"%s\",channel=\"%d\"} %f\n", chanName, diff --git a/src/WebApi_ws_live.cpp b/src/WebApi_ws_live.cpp index 3fb42ecb..1a69d46d 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -59,6 +59,7 @@ void WebApiWsLiveClass::loop() // Update on every inverter change or at least after 10 seconds if (millis() - _lastWsPublish > (10 * 1000) || (maxTimeStamp != _newestInverterTimestamp)) { + try { DynamicJsonDocument root(40960); JsonVariant var = root; @@ -77,11 +78,11 @@ void WebApiWsLiveClass::loop() _ws.textAll(buffer); } - _lastWsPublish = millis(); - } - catch (std::bad_alloc& bad_alloc) { + } catch (std::bad_alloc& bad_alloc) { MessageOutput.printf("Call to /api/livedata/status temporarely out of resources. Reason: \"%s\".\r\n", bad_alloc.what()); } + + _lastWsPublish = millis(); } } @@ -224,11 +225,18 @@ void WebApiWsLiveClass::onLivedataStatus(AsyncWebServerRequest* request) return; } - AsyncJsonResponse* response = new AsyncJsonResponse(false, 40960U); - JsonVariant root = response->getRoot(); + try { + AsyncJsonResponse* response = new AsyncJsonResponse(false, 40960U); + JsonVariant root = response->getRoot(); - generateJsonResponse(root); + generateJsonResponse(root); - response->setLength(); - request->send(response); + response->setLength(); + request->send(response); + + } catch (std::bad_alloc& bad_alloc) { + MessageOutput.printf("Call to /api/livedata/status temporarely out of resources. Reason: \"%s\".\r\n", bad_alloc.what()); + + WebApi.sendTooManyRequests(request); + } } \ No newline at end of file diff --git a/src/WebApi_ws_vedirect_live.cpp b/src/WebApi_ws_vedirect_live.cpp index 31654a5a..a8d16069 100644 --- a/src/WebApi_ws_vedirect_live.cpp +++ b/src/WebApi_ws_vedirect_live.cpp @@ -55,6 +55,7 @@ void WebApiWsVedirectLiveClass::loop() // Update on ve.direct change or at least after 10 seconds if (millis() - _lastWsPublish > (10 * 1000) || (maxTimeStamp != _newestVedirectTimestamp)) { + try { DynamicJsonDocument root(1024); JsonVariant var = root; @@ -73,11 +74,11 @@ void WebApiWsVedirectLiveClass::loop() _ws.textAll(buffer); } - _lastWsPublish = millis(); - } - catch (std::bad_alloc& bad_alloc) { + } catch (std::bad_alloc& bad_alloc) { MessageOutput.printf("Call to /api/vedirectlivedata/status temporarely out of resources. Reason: \"%s\".\r\n", bad_alloc.what()); } + + _lastWsPublish = millis(); } } diff --git a/webapp/src/components/DevInfo.vue b/webapp/src/components/DevInfo.vue index 7e4f5fb9..df049b7a 100644 --- a/webapp/src/components/DevInfo.vue +++ b/webapp/src/components/DevInfo.vue @@ -6,6 +6,18 @@ + + + + + + + + + + + + @@ -61,6 +73,16 @@ export default defineComponent({ const version_patch = Math.floor((value - version_major * 10000 - version_minor * 100)); return version_major + "." + version_minor + "." + version_patch; }; + }, + productionYear() { + return() => { + return ((parseInt(this.devInfoList.serial.toString(), 16) >> (7 * 4)) & 0xF) + 2014; + } + }, + productionWeek() { + return() => { + return ((parseInt(this.devInfoList.serial.toString(), 16) >> (5 * 4)) & 0xFF).toString(16); + } } } }); diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 2152c292..1dc76270 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -159,6 +159,9 @@ "NoInfo": "Keine Informationen verfügbar", "NoInfoLong": "Bisher wurden noch keine gültigen Daten vom Wechselrichter empfangen. Versuche es weiter...", "UnknownModel": "Unbekanntes Modell! Bitte melden Sie die \"Hardware Teilenummer\" und das Modell (z.B. HM-350) hier als Problem.", + "Serial": "Seriennummer", + "ProdYear": "Produktionsjahr", + "ProdWeek": "Produktionswoche", "Model": "Modell", "DetectedMaxPower": "Ermittelte max. Leistung", "BootloaderVersion": "Bootloader-Version", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index cdd219fb..9dcd1628 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -159,6 +159,9 @@ "NoInfo": "No Information available", "NoInfoLong": "Did not receive any valid data from the inverter till now. Still trying...", "UnknownModel": "Unknown model! Please report the \"Hardware Part Number\" and model (e.g. HM-350) as an issue here.", + "Serial": "Serial", + "ProdYear": "Production Year", + "ProdWeek": "Production Week", "Model": "Model", "DetectedMaxPower": "Detected max. Power", "BootloaderVersion": "Bootloader Version", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index 3560b739..11fbbcd4 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -2,14 +2,14 @@ "menu": { "LiveView": "Direct", "Settings": "Paramètres", - "NetworkSettings": "Paramètres réseau", - "NTPSettings": "Paramètres NTP", - "MQTTSettings": "Paramètres MQTT", - "InverterSettings": "Paramètres des onduleurs", - "SecuritySettings": "Paramètres de sécurité", - "DTUSettings": "Paramètres DTU", - "DeviceManager": "Gestionnaire de périphériques", - "VedirectSettings": "Paramètres Ve.direct", + "NetworkSettings": "Réseau", + "NTPSettings": "Heure locale", + "MQTTSettings": "MQTT", + "InverterSettings": "Onduleurs", + "SecuritySettings": "Sécurité", + "DTUSettings": "DTU", + "DeviceManager": "Périphériques", + "VedirectSettings": "Ve.direct", "ConfigManagement": "Gestion de la configuration", "FirmwareUpgrade": "Mise à jour du firmware", "DeviceReboot": "Redémarrage de l'appareil", @@ -158,6 +158,9 @@ "NoInfo": "Aucune information disponible", "NoInfoLong": "N'a pas reçu de données valides de l'onduleur jusqu'à présent. J'essaie toujours...", "UnknownModel": "Modèle inconnu ! Veuillez signaler le \"Numéro d'article matériel\" et le modèle (par exemple, HM-350) comme un problème ici.", + "Serial": "Serial", + "ProdYear": "Production Year", + "ProdWeek": "Production Week", "Model": "Modèle", "DetectedMaxPower": "Puissance maximale détectée", "BootloaderVersion": "Version du bootloader", @@ -372,7 +375,7 @@ "TimeServerHint": "La valeur par défaut convient tant que OpenDTU a un accès direct à Internet.", "Timezone": "Fuseau horaire", "TimezoneConfig": "Configuration du fuseau horaire", - "LocationConfiguration": "Location Configuration", + "LocationConfiguration": "Géolocalisation", "Longitude": "Longitude", "Latitude": "Latitude", "Save": "@:dtuadmin.Save", @@ -450,10 +453,10 @@ "Add": "Ajouter", "AddHint": " Astuce : Vous pouvez définir des paramètres supplémentaires après avoir créé l'onduleur. Utilisez l'icône du stylo dans la liste des onduleurs.", "InverterList": "Liste des onduleurs", - "Status": "Status", - "Send": "Send", - "Receive": "Receive", - "StatusHint": "Astuce : The inverter is power by it's DC input. If there is no sun, the inverter is off. Requests can still be sent.", + "Status": "État", + "Send": "Envoyer", + "Receive": "Recevoir", + "StatusHint": "Astuce : L'onduleur est alimenté par son entrée courant continu. S'il n'y a pas de soleil, l'onduleur est éteint, mais les requêtes peuvent toujours être envoyées.", "Type": "Type", "Action": "Action", "DeleteInverter": "Supprimer l'onduleur", @@ -462,16 +465,16 @@ "InverterName": "Nom de l'onduleur :", "InverterNameHint": "Ici, vous pouvez spécifier un nom personnalisé pour votre onduleur.", "InverterStatus": "Receive / Send", - "PollEnable": "Poll inverter data", - "PollEnableNight": "Poll inverter data at night", - "CommandEnable": "Send commands", - "CommandEnableNight": "Send commands at night", + "PollEnable": "Interroger les données de l'onduleur", + "PollEnableNight": "Interroger les données de l'onduleur la nuit", + "CommandEnable": "Envoyer des commandes", + "CommandEnableNight": "Envoyer des commandes la nuit", "StringName": "Nom de la ligne {num}:", "StringNameHint": "Ici, vous pouvez spécifier un nom personnalisé pour le port respectif de votre onduleur.", "StringMaxPower": "Puissance maximale de la ligne {num}:", "StringMaxPowerHint": "Entrez la puissance maximale des panneaux solaires connectés.", - "StringYtOffset": "Yield total offset string {num}:", - "StringYtOffsetHint": "This offset is applied the read yield total value from the inverter. This can be used to set the yield total of the inverter to zero if a used inverter is used.", + "StringYtOffset": "Décalage du rendement total de la ligne {num} :", + "StringYtOffsetHint": "Ce décalage est appliqué à la valeur de rendement total lue sur le variateur. Il peut être utilisé pour mettre le rendement total du variateur à zéro si un variateur usagé est utilisé.", "InverterHint": "*) Entrez le Wp du canal pour calculer l'irradiation.", "Cancel": "@:maintenancereboot.Cancel", "Save": "@:dtuadmin.Save", @@ -483,8 +486,8 @@ "BackupHeader": "Sauvegarder le fichier de configuration", "BackupConfig": "Fichier de configuration", "Backup": "Sauvegarder", - "Restore": "Restore", - "NoFileSelected": "No file selected", + "Restore": "Restaurer", + "NoFileSelected": "Aucun fichier sélectionné", "RestoreHeader": "Restaurer le fichier de configuration", "Back": "Retour", "UploadSuccess": "Succès du téléversement", diff --git a/webapp/src/types/DevInfoStatus.ts b/webapp/src/types/DevInfoStatus.ts index a60d5381..4c09e6b4 100644 --- a/webapp/src/types/DevInfoStatus.ts +++ b/webapp/src/types/DevInfoStatus.ts @@ -1,4 +1,5 @@ export interface DevInfoStatus { + serial: number; valid_data: boolean; fw_bootloader_version: number; fw_build_version: number; diff --git a/webapp/src/views/HomeView.vue b/webapp/src/views/HomeView.vue index 24a7491b..334c0725 100644 --- a/webapp/src/views/HomeView.vue +++ b/webapp/src/views/HomeView.vue @@ -543,6 +543,7 @@ export default defineComponent({ .then((response) => handleResponse(response, this.$emitter, this.$router)) .then((data) => { this.devInfoList = data[serial][0]; + this.devInfoList.serial = serial; this.devInfoLoading = false; }); diff --git a/webapp_dist/index.html.gz b/webapp_dist/index.html.gz index f7547917..0830730b 100644 Binary files a/webapp_dist/index.html.gz and b/webapp_dist/index.html.gz differ diff --git a/webapp_dist/js/app.js.gz b/webapp_dist/js/app.js.gz index 54402892..d23f9022 100644 Binary files a/webapp_dist/js/app.js.gz and b/webapp_dist/js/app.js.gz differ diff --git a/webapp_dist/zones.json.gz b/webapp_dist/zones.json.gz index 45374b96..f1ea50bf 100644 Binary files a/webapp_dist/zones.json.gz and b/webapp_dist/zones.json.gz differ
{{ $t('devinfo.Serial') }}{{ devInfoList.serial }}
{{ $t('devinfo.ProdYear') }}{{ productionYear() }}
{{ $t('devinfo.ProdWeek') }}{{ productionWeek() }}
{{ $t('devinfo.Model') }} {{ devInfoList.hw_model_name }}