Merge branch 'tbnobody:master' into master

This commit is contained in:
Ralf Bauer 2025-01-10 12:07:21 +01:00 committed by GitHub
commit 9262bbf1c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
128 changed files with 6588 additions and 3813 deletions

View File

@ -48,7 +48,7 @@ body:
description: How did you install OpenDTU? description: How did you install OpenDTU?
options: options:
- Pre-Compiled binary from GitHub releases - Pre-Compiled binary from GitHub releases
- Pre-Compiles binary from GitHub actions/pull-request - Pre-Compiled binary from GitHub actions/pull-request
- Self-Compiled - Self-Compiled
validations: validations:
required: true required: true

View File

@ -243,5 +243,83 @@
"data": 2, "data": 2,
"clk": 1 "clk": 1
} }
},
{
"name": "OpenDTU Fusion v2 PoE with SH1106 Display",
"links": [
{"name": "Information", "url": "https://github.com/markusdd/OpenDTUFusionDocs"}
],
"nrf24": {
"miso": 48,
"mosi": 35,
"clk": 36,
"irq": 47,
"en": 38,
"cs": 37
},
"cmt": {
"clk": 6,
"cs": 4,
"fcs": 21,
"sdio": 5,
"gpio2": 3,
"gpio3": 8
},
"w5500": {
"mosi": 40,
"miso": 41,
"sclk": 39,
"cs": 42,
"int": 44,
"rst": 43
},
"led": {
"led0": 17,
"led1": 18
},
"display": {
"type": 3,
"data": 2,
"clk": 1
}
},
{
"name": "OpenDTU Fusion v2 PoE with SSD1306 Display",
"links": [
{"name": "Information", "url": "https://github.com/markusdd/OpenDTUFusionDocs"}
],
"nrf24": {
"miso": 48,
"mosi": 35,
"clk": 36,
"irq": 47,
"en": 38,
"cs": 37
},
"cmt": {
"clk": 6,
"cs": 4,
"fcs": 21,
"sdio": 5,
"gpio2": 3,
"gpio3": 8
},
"w5500": {
"mosi": 40,
"miso": 41,
"sclk": 39,
"cs": 42,
"int": 44,
"rst": 43
},
"led": {
"led0": 17,
"led1": 18
},
"display": {
"type": 2,
"data": 2,
"clk": 1
}
} }
] ]

View File

@ -1,3 +1,3 @@
# Upgrade Partition # Upgrade Partition
This documentation will has been moved and can be found here: <https://tbnobody.github.io/OpenDTU-docs/firmware/howto/upgrade_partition/> This documentation has been moved and can be found here: <https://tbnobody.github.io/OpenDTU-docs/firmware/howto/upgrade_partition/>

View File

@ -3,9 +3,12 @@
#include "PinMapping.h" #include "PinMapping.h"
#include <cstdint> #include <cstdint>
#include <TaskSchedulerDeclarations.h>
#include <mutex>
#include <condition_variable>
#define CONFIG_FILENAME "/config.json" #define CONFIG_FILENAME "/config.json"
#define CONFIG_VERSION 0x00011c00 // 0.1.28 // make sure to clean all after change #define CONFIG_VERSION 0x00011d00 // 0.1.29 // make sure to clean all after change
#define WIFI_MAX_SSID_STRLEN 32 #define WIFI_MAX_SSID_STRLEN 32
#define WIFI_MAX_PASSWORD_STRLEN 64 #define WIFI_MAX_PASSWORD_STRLEN 64
@ -30,6 +33,7 @@
#define CHAN_MAX_NAME_STRLEN 31 #define CHAN_MAX_NAME_STRLEN 31
#define DEV_MAX_MAPPING_NAME_STRLEN 63 #define DEV_MAX_MAPPING_NAME_STRLEN 63
#define LOCALE_STRLEN 2
struct CHANNEL_CONFIG_T { struct CHANNEL_CONFIG_T {
uint16_t MaxChannelPower; uint16_t MaxChannelPower;
@ -144,7 +148,7 @@ struct CONFIG_T {
bool ScreenSaver; bool ScreenSaver;
uint8_t Rotation; uint8_t Rotation;
uint8_t Contrast; uint8_t Contrast;
uint8_t Language; char Locale[LOCALE_STRLEN + 1];
struct { struct {
uint32_t Duration; uint32_t Duration;
uint8_t Mode; uint8_t Mode;
@ -161,15 +165,32 @@ struct CONFIG_T {
class ConfigurationClass { class ConfigurationClass {
public: public:
void init(); void init(Scheduler& scheduler);
bool read(); bool read();
bool write(); bool write();
void migrate(); void migrate();
CONFIG_T& get(); CONFIG_T const& get();
class WriteGuard {
public:
WriteGuard();
CONFIG_T& getConfig();
~WriteGuard();
private:
std::unique_lock<std::mutex> _lock;
};
WriteGuard getWriteGuard();
INVERTER_CONFIG_T* getFreeInverterSlot(); INVERTER_CONFIG_T* getFreeInverterSlot();
INVERTER_CONFIG_T* getInverterConfig(const uint64_t serial); INVERTER_CONFIG_T* getInverterConfig(const uint64_t serial);
void deleteInverterById(const uint8_t id); void deleteInverterById(const uint8_t id);
private:
void loop();
Task _loopTask;
}; };
extern ConfigurationClass Configuration; extern ConfigurationClass Configuration;

View File

@ -40,7 +40,7 @@ public:
void setContrast(const uint8_t contrast); void setContrast(const uint8_t contrast);
void setStatus(const bool turnOn); void setStatus(const bool turnOn);
void setOrientation(const uint8_t rotation = DISPLAY_ROTATION); void setOrientation(const uint8_t rotation = DISPLAY_ROTATION);
void setLanguage(const uint8_t language); void setLocale(const String& locale);
void setDiagramMode(DiagramMode_t mode); void setDiagramMode(DiagramMode_t mode);
void setStartupDisplay(); void setStartupDisplay();
@ -65,7 +65,7 @@ private:
DisplayType_t _display_type = DisplayType_t::None; DisplayType_t _display_type = DisplayType_t::None;
DiagramMode_t _diagram_mode = DiagramMode_t::Off; DiagramMode_t _diagram_mode = DiagramMode_t::Off;
uint8_t _display_language = DISPLAY_LANGUAGE; String _display_language = DISPLAY_LOCALE;
uint8_t _mExtra; uint8_t _mExtra;
const uint16_t _period = 1000; const uint16_t _period = 1000;
const uint16_t _interval = 60000; // interval at which to power save (milliseconds) const uint16_t _interval = 60000; // interval at which to power save (milliseconds)
@ -73,6 +73,15 @@ private:
char _fmtText[32]; char _fmtText[32];
bool _isLarge = false; bool _isLarge = false;
uint8_t _lineOffsets[5]; uint8_t _lineOffsets[5];
String _i18n_offline;
String _i18n_yield_today_kwh;
String _i18n_yield_today_wh;
String _i18n_date_format;
String _i18n_current_power_kw;
String _i18n_current_power_w;
String _i18n_yield_total_mwh;
String _i18n_yield_total_kwh;
}; };
extern DisplayGraphicClass Display; extern DisplayGraphicClass Display;

35
include/I18n.h Normal file
View File

@ -0,0 +1,35 @@
// 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();
String getFilenameByLocale(const String& locale) const;
void readDisplayStrings(
const String& locale,
String& date_format,
String& offline,
String& power_w, String& power_kw,
String& yield_today_wh, String& yield_today_kwh,
String& yield_total_kwh, String& yield_total_mwh);
private:
void readLangPacks();
void readConfig(String file);
std::list<LanguageInfo_t> _availLanguages;
};
extern I18nClass I18n;

View File

@ -13,7 +13,6 @@ enum DeviceClassType {
DEVICE_CLS_PWR, DEVICE_CLS_PWR,
DEVICE_CLS_VOLTAGE, DEVICE_CLS_VOLTAGE,
DEVICE_CLS_FREQ, DEVICE_CLS_FREQ,
DEVICE_CLS_TEMP,
DEVICE_CLS_POWER_FACTOR, DEVICE_CLS_POWER_FACTOR,
DEVICE_CLS_REACTIVE_POWER, DEVICE_CLS_REACTIVE_POWER,
DEVICE_CLS_CONNECTIVITY, DEVICE_CLS_CONNECTIVITY,
@ -22,7 +21,7 @@ enum DeviceClassType {
DEVICE_CLS_TEMPERATURE, DEVICE_CLS_TEMPERATURE,
DEVICE_CLS_RESTART DEVICE_CLS_RESTART
}; };
const char* const deviceClass_name[] = { 0, "current", "energy", "power", "voltage", "frequency", "temperature", "power_factor", "reactive_power", "connectivity", "duration", "signal_strength", "temperature", "restart" }; const char* const deviceClass_name[] = { 0, "current", "energy", "power", "voltage", "frequency", "power_factor", "reactive_power", "connectivity", "duration", "signal_strength", "temperature", "restart" };
enum StateClassType { enum StateClassType {
STATE_CLS_NONE = 0, STATE_CLS_NONE = 0,
@ -55,7 +54,7 @@ const byteAssign_fieldDeviceClass_t deviceFieldAssignment[] = {
{ FLD_IAC, DEVICE_CLS_CURRENT, STATE_CLS_MEASUREMENT }, { FLD_IAC, DEVICE_CLS_CURRENT, STATE_CLS_MEASUREMENT },
{ FLD_PAC, DEVICE_CLS_PWR, STATE_CLS_MEASUREMENT }, { FLD_PAC, DEVICE_CLS_PWR, STATE_CLS_MEASUREMENT },
{ FLD_F, DEVICE_CLS_FREQ, STATE_CLS_MEASUREMENT }, { FLD_F, DEVICE_CLS_FREQ, STATE_CLS_MEASUREMENT },
{ FLD_T, DEVICE_CLS_TEMP, STATE_CLS_MEASUREMENT }, { FLD_T, DEVICE_CLS_TEMPERATURE, STATE_CLS_MEASUREMENT },
{ FLD_PF, DEVICE_CLS_POWER_FACTOR, STATE_CLS_MEASUREMENT }, { FLD_PF, DEVICE_CLS_POWER_FACTOR, STATE_CLS_MEASUREMENT },
{ FLD_EFF, DEVICE_CLS_NONE, STATE_CLS_NONE }, { FLD_EFF, DEVICE_CLS_NONE, STATE_CLS_NONE },
{ FLD_IRR, DEVICE_CLS_NONE, STATE_CLS_NONE }, { FLD_IRR, DEVICE_CLS_NONE, STATE_CLS_NONE },
@ -75,21 +74,21 @@ private:
static void publish(const String& subtopic, const String& payload); static void publish(const String& subtopic, const String& payload);
static void publish(const String& subtopic, const JsonDocument& doc); static void publish(const String& subtopic, const JsonDocument& doc);
static void addCommonMetadata(JsonDocument& doc, const String& unit_of_measure, const String& icon, const DeviceClassType device_class, const CategoryType category); static void addCommonMetadata(JsonDocument& doc, const String& unit_of_measure, const String& icon, const DeviceClassType device_class, const StateClassType state_class, const CategoryType category);
// Binary Sensor // Binary Sensor
static void publishBinarySensor(JsonDocument& doc, const String& root_device, const String& unique_id_prefix, const String& name, const String& state_topic, const String& payload_on, const String& payload_off, const DeviceClassType device_class, const CategoryType category); static void publishBinarySensor(JsonDocument& doc, const String& root_device, const String& unique_id_prefix, const String& name, const String& state_topic, const String& payload_on, const String& payload_off, const DeviceClassType device_class, const StateClassType state_class, const CategoryType category);
static void publishDtuBinarySensor(const String& name, const String& state_topic, const String& payload_on, const String& payload_off, const DeviceClassType device_class, const CategoryType category); static void publishDtuBinarySensor(const String& name, const String& state_topic, const String& payload_on, const String& payload_off, const DeviceClassType device_class, const StateClassType state_class, const CategoryType category);
static void publishInverterBinarySensor(std::shared_ptr<InverterAbstract> inv, const String& name, const String& state_topic, const String& payload_on, const String& payload_off, const DeviceClassType device_class, const CategoryType category); static void publishInverterBinarySensor(std::shared_ptr<InverterAbstract> inv, const String& name, const String& state_topic, const String& payload_on, const String& payload_off, const DeviceClassType device_class, const StateClassType state_class, const CategoryType category);
// Sensor // Sensor
static void publishSensor(JsonDocument& doc, const String& root_device, const String& unique_id_prefix, const String& name, const String& state_topic, const String& unit_of_measure, const String& icon, const DeviceClassType device_class, const CategoryType category); static void publishSensor(JsonDocument& doc, const String& root_device, const String& unique_id_prefix, const String& name, const String& state_topic, const String& unit_of_measure, const String& icon, const DeviceClassType device_class, const StateClassType state_class, const CategoryType category);
static void publishDtuSensor(const String& name, const String& state_topic, const String& unit_of_measure, const String& icon, const DeviceClassType device_class, const CategoryType category); static void publishDtuSensor(const String& name, const String& state_topic, const String& unit_of_measure, const String& icon, const DeviceClassType device_class, const StateClassType state_class, const CategoryType category);
static void publishInverterSensor(std::shared_ptr<InverterAbstract> inv, const String& name, const String& state_topic, const String& unit_of_measure, const String& icon, const DeviceClassType device_class, const CategoryType category); static void publishInverterSensor(std::shared_ptr<InverterAbstract> inv, const String& name, const String& state_topic, const String& unit_of_measure, const String& icon, const DeviceClassType device_class, const StateClassType state_class, const CategoryType category);
static void publishInverterField(std::shared_ptr<InverterAbstract> inv, const ChannelType_t type, const ChannelNum_t channel, const byteAssign_fieldDeviceClass_t fieldType, const bool clear = false); static void publishInverterField(std::shared_ptr<InverterAbstract> inv, const ChannelType_t type, const ChannelNum_t channel, const byteAssign_fieldDeviceClass_t fieldType, const bool clear = false);
static void publishInverterButton(std::shared_ptr<InverterAbstract> inv, const String& name, const String& state_topic, const String& payload, const String& icon, const DeviceClassType device_class, const CategoryType category); static void publishInverterButton(std::shared_ptr<InverterAbstract> inv, const String& name, const String& state_topic, const String& payload, const String& icon, const DeviceClassType device_class, const StateClassType state_class, const CategoryType category);
static void publishInverterNumber(std::shared_ptr<InverterAbstract> inv, const String& name, const String& state_topic, const String& command_topic, const int16_t min, const int16_t max, float step, const String& unit_of_measure, const String& icon, const CategoryType category); static void publishInverterNumber(std::shared_ptr<InverterAbstract> inv, const String& name, const String& state_topic, const String& command_topic, const int16_t min, const int16_t max, float step, const String& unit_of_measure, const String& icon, const StateClassType state_class, const CategoryType category);
static void createInverterInfo(JsonDocument& doc, std::shared_ptr<InverterAbstract> inv); static void createInverterInfo(JsonDocument& doc, std::shared_ptr<InverterAbstract> inv);
static void createDtuInfo(JsonDocument& doc); static void createDtuInfo(JsonDocument& doc);

View File

@ -26,16 +26,16 @@ enum class network_event {
typedef std::function<void(network_event event)> DtuNetworkEventCb; typedef std::function<void(network_event event)> DtuNetworkEventCb;
typedef struct NetworkEventCbList { typedef struct DtuNetworkEventCbList {
DtuNetworkEventCb cb; DtuNetworkEventCb cb;
network_event event; network_event event;
NetworkEventCbList() DtuNetworkEventCbList()
: cb(nullptr) : cb(nullptr)
, event(network_event::NETWORK_UNKNOWN) , event(network_event::NETWORK_UNKNOWN)
{ {
} }
} NetworkEventCbList_t; } DtuNetworkEventCbList_t;
class NetworkSettingsClass { class NetworkSettingsClass {
public: public:
@ -82,7 +82,7 @@ private:
bool _dnsServerStatus = false; bool _dnsServerStatus = false;
network_mode _networkMode = network_mode::Undefined; network_mode _networkMode = network_mode::Undefined;
bool _ethConnected = false; bool _ethConnected = false;
std::vector<NetworkEventCbList_t> _cbEventList; std::vector<DtuNetworkEventCbList_t> _cbEventList;
bool _lastMdnsEnabled = false; bool _lastMdnsEnabled = false;
std::unique_ptr<W5500> _w5500; std::unique_ptr<W5500> _w5500;
}; };

View File

@ -12,6 +12,7 @@
struct PinMapping_t { struct PinMapping_t {
char name[MAPPING_NAME_STRLEN + 1]; char name[MAPPING_NAME_STRLEN + 1];
int8_t nrf24_miso; int8_t nrf24_miso;
int8_t nrf24_mosi; int8_t nrf24_mosi;
int8_t nrf24_clk; int8_t nrf24_clk;
@ -33,6 +34,7 @@ struct PinMapping_t {
int8_t w5500_int; int8_t w5500_int;
int8_t w5500_rst; int8_t w5500_rst;
#if CONFIG_ETH_USE_ESP32_EMAC
int8_t eth_phy_addr; int8_t eth_phy_addr;
bool eth_enabled; bool eth_enabled;
int eth_power; int eth_power;
@ -40,11 +42,14 @@ struct PinMapping_t {
int eth_mdio; int eth_mdio;
eth_phy_type_t eth_type; eth_phy_type_t eth_type;
eth_clock_mode_t eth_clk_mode; eth_clock_mode_t eth_clk_mode;
#endif
uint8_t display_type; uint8_t display_type;
uint8_t display_data; uint8_t display_data;
uint8_t display_clk; uint8_t display_clk;
uint8_t display_cs; uint8_t display_cs;
uint8_t display_reset; uint8_t display_reset;
int8_t led[PINMAPPING_LED_COUNT]; int8_t led[PINMAPPING_LED_COUNT];
}; };
@ -57,7 +62,9 @@ public:
bool isValidNrf24Config() const; bool isValidNrf24Config() const;
bool isValidCmt2300Config() const; bool isValidCmt2300Config() const;
bool isValidW5500Config() const; bool isValidW5500Config() const;
#if CONFIG_ETH_USE_ESP32_EMAC
bool isValidEthConfig() const; bool isValidEthConfig() const;
#endif
private: private:
PinMapping_t _pinMapping; PinMapping_t _pinMapping;

View File

@ -2,6 +2,7 @@
#pragma once #pragma once
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <LittleFS.h>
#include <cstdint> #include <cstdint>
class Utils { class Utils {
@ -11,4 +12,6 @@ public:
static int getTimezoneOffset(); static int getTimezoneOffset();
static bool checkJsonAlloc(const JsonDocument& doc, const char* function, const uint16_t line); static bool checkJsonAlloc(const JsonDocument& doc, const char* function, const uint16_t line);
static void removeAllFiles(); static void removeAllFiles();
static String generateMd5FromFile(String file);
static void skipBom(File& f);
}; };

View File

@ -2,19 +2,28 @@
#pragma once #pragma once
#include <Arduino.h> #include <Arduino.h>
#include <driver/spi_master.h>
#include <esp_eth.h> // required for esp_eth_handle_t #include <esp_eth.h> // required for esp_eth_handle_t
#include <esp_netif.h> #include <esp_netif.h>
#include <memory>
class W5500 { class W5500 {
private:
explicit W5500(spi_device_handle_t spi, gpio_num_t pin_int);
public: public:
explicit W5500(int8_t pin_mosi, int8_t pin_miso, int8_t pin_sclk, int8_t pin_cs, int8_t pin_int, int8_t pin_rst);
W5500(const W5500&) = delete; W5500(const W5500&) = delete;
W5500& operator=(const W5500&) = delete; W5500& operator=(const W5500&) = delete;
~W5500(); ~W5500();
static std::unique_ptr<W5500> setup(int8_t pin_mosi, int8_t pin_miso, int8_t pin_sclk, int8_t pin_cs, int8_t pin_int, int8_t pin_rst);
String macAddress(); String macAddress();
private: private:
static bool connection_check_spi(spi_device_handle_t spi);
static bool connection_check_interrupt(gpio_num_t pin_int);
esp_eth_handle_t eth_handle; esp_eth_handle_t eth_handle;
esp_netif_t* eth_netif; esp_netif_t* eth_netif;
}; };

View File

@ -1,14 +1,15 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#pragma once #pragma once
#include "WebApi_config.h"
#include "WebApi_device.h" #include "WebApi_device.h"
#include "WebApi_devinfo.h" #include "WebApi_devinfo.h"
#include "WebApi_dtu.h" #include "WebApi_dtu.h"
#include "WebApi_errors.h" #include "WebApi_errors.h"
#include "WebApi_eventlog.h" #include "WebApi_eventlog.h"
#include "WebApi_file.h"
#include "WebApi_firmware.h" #include "WebApi_firmware.h"
#include "WebApi_gridprofile.h" #include "WebApi_gridprofile.h"
#include "WebApi_i18n.h"
#include "WebApi_inverter.h" #include "WebApi_inverter.h"
#include "WebApi_limit.h" #include "WebApi_limit.h"
#include "WebApi_maintenance.h" #include "WebApi_maintenance.h"
@ -30,6 +31,7 @@ class WebApiClass {
public: public:
WebApiClass(); WebApiClass();
void init(Scheduler& scheduler); void init(Scheduler& scheduler);
void reload();
static bool checkCredentials(AsyncWebServerRequest* request); static bool checkCredentials(AsyncWebServerRequest* request);
static bool checkCredentialsReadonly(AsyncWebServerRequest* request); static bool checkCredentialsReadonly(AsyncWebServerRequest* request);
@ -45,13 +47,14 @@ public:
private: private:
AsyncWebServer _server; AsyncWebServer _server;
WebApiConfigClass _webApiConfig;
WebApiDeviceClass _webApiDevice; WebApiDeviceClass _webApiDevice;
WebApiDevInfoClass _webApiDevInfo; WebApiDevInfoClass _webApiDevInfo;
WebApiDtuClass _webApiDtu; WebApiDtuClass _webApiDtu;
WebApiEventlogClass _webApiEventlog; WebApiEventlogClass _webApiEventlog;
WebApiFileClass _webApiFile;
WebApiFirmwareClass _webApiFirmware; WebApiFirmwareClass _webApiFirmware;
WebApiGridProfileClass _webApiGridprofile; WebApiGridProfileClass _webApiGridprofile;
WebApiI18nClass _webApiI18n;
WebApiInverterClass _webApiInverter; WebApiInverterClass _webApiInverter;
WebApiLimitClass _webApiLimit; WebApiLimitClass _webApiLimit;
WebApiMaintenanceClass _webApiMaintenance; WebApiMaintenanceClass _webApiMaintenance;

View File

@ -1,17 +0,0 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <ESPAsyncWebServer.h>
#include <TaskSchedulerDeclarations.h>
class WebApiConfigClass {
public:
void init(AsyncWebServer& server, Scheduler& scheduler);
private:
void onConfigGet(AsyncWebServerRequest* request);
void onConfigDelete(AsyncWebServerRequest* request);
void onConfigListGet(AsyncWebServerRequest* request);
void onConfigUploadFinish(AsyncWebServerRequest* request);
void onConfigUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final);
};

View File

@ -18,9 +18,10 @@ enum WebApiError {
DtuInvalidCmtFrequency, DtuInvalidCmtFrequency,
DtuInvalidCmtCountry, DtuInvalidCmtCountry,
ConfigBase = 3000, FileBase = 3000,
ConfigNotDeleted, FileNotDeleted,
ConfigSuccess, FileSuccess,
FileDeleteSuccess,
InverterBase = 4000, InverterBase = 4000,
InverterSerialZero, InverterSerialZero,

18
include/WebApi_file.h Normal file
View File

@ -0,0 +1,18 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <ESPAsyncWebServer.h>
#include <TaskSchedulerDeclarations.h>
class WebApiFileClass {
public:
void init(AsyncWebServer& server, Scheduler& scheduler);
private:
void onFileGet(AsyncWebServerRequest* request);
void onFileDelete(AsyncWebServerRequest* request);
void onFileDeleteAll(AsyncWebServerRequest* request);
void onFileListGet(AsyncWebServerRequest* request);
void onFileUploadFinish(AsyncWebServerRequest* request);
void onFileUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final);
};

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

@ -8,9 +8,11 @@ class WebApiWsConsoleClass {
public: public:
WebApiWsConsoleClass(); WebApiWsConsoleClass();
void init(AsyncWebServer& server, Scheduler& scheduler); void init(AsyncWebServer& server, Scheduler& scheduler);
void reload();
private: private:
AsyncWebSocket _ws; AsyncWebSocket _ws;
AuthenticationMiddleware _simpleDigestAuth;
Task _wsCleanupTask; Task _wsCleanupTask;
void wsCleanupTaskCb(); void wsCleanupTaskCb();

View File

@ -11,6 +11,7 @@ class WebApiWsLiveClass {
public: public:
WebApiWsLiveClass(); WebApiWsLiveClass();
void init(AsyncWebServer& server, Scheduler& scheduler); void init(AsyncWebServer& server, Scheduler& scheduler);
void reload();
private: private:
static void generateInverterCommonJsonResponse(JsonObject& root, std::shared_ptr<InverterAbstract> inv); static void generateInverterCommonJsonResponse(JsonObject& root, std::shared_ptr<InverterAbstract> inv);
@ -24,6 +25,7 @@ private:
void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len); void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len);
AsyncWebSocket _ws; AsyncWebSocket _ws;
AuthenticationMiddleware _simpleDigestAuth;
uint32_t _lastPublishStats[INV_MAX_COUNT] = { 0 }; uint32_t _lastPublishStats[INV_MAX_COUNT] = { 0 };

View File

@ -99,7 +99,7 @@
#define DISPLAY_SCREENSAVER true #define DISPLAY_SCREENSAVER true
#define DISPLAY_ROTATION 2U #define DISPLAY_ROTATION 2U
#define DISPLAY_CONTRAST 60U #define DISPLAY_CONTRAST 60U
#define DISPLAY_LANGUAGE 0U #define DISPLAY_LOCALE "en"
#define DISPLAY_DIAGRAM_DURATION (10UL * 60UL * 60UL) #define DISPLAY_DIAGRAM_DURATION (10UL * 60UL * 60UL)
#define DISPLAY_DIAGRAM_MODE 1U #define DISPLAY_DIAGRAM_MODE 1U
@ -108,3 +108,5 @@
#define LED_BRIGHTNESS 100U #define LED_BRIGHTNESS 100U
#define MAX_INVERTER_LIMIT 2250 #define MAX_INVERTER_LIMIT 2250
#define LANG_PACK_SUFFIX ".lang.json"

9
lang/README.md Normal file
View File

@ -0,0 +1,9 @@
# Language Packs
This folder contains language packs for OpenDTU which can be uploaded to the
device using the "Config Management" function.
Select "Language Pack" in the restore section, select a `.json` file containing
your language and press "Restore". Afterwards all language selection drop down
menues contain the new language.
Create a pull to request to share your own language pack (or corrections) with the community.

690
lang/es.lang.json Normal file
View File

@ -0,0 +1,690 @@
{
"meta": {
"name": "Español",
"code": "es"
},
"display": {
"date_format": "%d/%m/%Y %H:%M",
"offline": "Apagado",
"power_w": "%.0f W",
"power_kw": "%.1f kW",
"yield_today_wh": "Hoy: %4.0f Wh",
"yield_today_kwh": "Hoy: %.1f kWh",
"yield_total_kwh": "Total: %.1f kWh",
"yield_total_mwh": "Total: %.0f kWh"
},
"webapp": {
"menu": {
"LiveView": "Vista en directo",
"Settings": "Ajustes",
"NetworkSettings": "Ajustes de Red",
"NTPSettings": "Ajustes NTP",
"MQTTSettings": "Ajustes MQTT",
"InverterSettings": "Ajustes Inversor",
"SecuritySettings": "Ajustes Seguridad",
"DTUSettings": "Ajustes DTU",
"DeviceManager": "Administrador Dispositivos",
"ConfigManagement": "Gestión configuración",
"FirmwareUpgrade": "Actualización Firmware",
"DeviceReboot": "Reinicio Dispositivo",
"Info": "Info",
"System": "Sistema",
"Network": "Red",
"NTP": "NTP",
"MQTT": "MQTT",
"Console": "Consola",
"About": "Acerca",
"Logout": "Logout",
"Login": "Login"
},
"base": {
"Loading": "Cargando...",
"Reload": "Recargar",
"Cancel": "Cancelar",
"Save": "Guardar",
"Refreshing": "Refrescando",
"Pull": "Tira hacia abajo para refrescar",
"Release": "Soltar para refrescar",
"Close": "Cerrar"
},
"wait": {
"NotReady": "OpenDTU is not yet ready",
"PleaseWait": "Please wait. You will be automatically redirected to the home page."
},
"Error": {
"Oops": "Oops!"
},
"localeswitcher": {
"Dark": "Oscuro",
"Light": "Claro",
"Auto": "Automático"
},
"apiresponse": {
"1001": "¡Opciones guardadas!",
"1002": "No se encontraron valores",
"1003": "Datos demasiado grandes",
"1004": "Fallo al procesar los datos",
"1005": "Faltan valores",
"1006": "Fallo en la escritura",
"2001": "¡El número de serie no puede ser cero!",
"2002": "Intervalo de Poll interval debe ser mayor que cero!",
"2003": "Configuración de potencia incorrecta!",
"2004": "La frecuencia debe estar entre {min} y {max} kHz y debe ser un múltiplo de 250 kHz!",
"2005": "Modelo desconocido! Por favor, informe el \"Modelo de pieza de hardware\" y el modelo (por ejemplo, HM-350) como un problema en <a href=\"https://github.com/tbnobody/OpenDTU/issues\" target=\"_blank\">aquí</a>.",
"3001": "No se eliminó nada",
"3002": "Configuración borrada. Reinicio en curso...",
"4001": "@:apiresponse.2001",
"4002": "El nombre debe tener entre 1 y {max} caracteres de longitud!",
"4003": "Solo se admiten {max} inversores!",
"4004": "Inversor creado!",
"4005": "ID no válido especificado",
"4006": "Cantidad de canales máxima incorrecta dada!",
"4007": "Inversor modificado!",
"4008": "Inversor eliminado!",
"4009": "Orden de inversores guardado!",
"5001": "@:apiresponse.2001",
"5002": "Límite debe estar entre 1 y {max}!",
"5003": "Tipo incorrecto especificado!",
"5004": "Inversor incorrecto especificado!",
"6001": "Reinicio desencadenado!",
"6002": "Reinicio cancelado!",
"7001": "¡El servidor MQTT debe tener entre 1 y {max} caracteres de longitud!",
"7002": "¡El nombre de usuario debe no tener más de {max} caracteres!",
"7003": "¡La contraseña debe no tener más de {max} caracteres!",
"7004": "¡El tema debe tener entre 1 y {max} caracteres de longitud!",
"7005": "¡El tema no debe contener caracteres de espacio!",
"7006": "¡El tema debe terminar con barra inclinada (/)!",
"7007": "¡El puerto debe ser un número entre 1 y 65535!",
"7008": "¡El certificado debe tener entre 1 y {max} caracteres de longitud!",
"7009": "¡El tema LWT debe tener entre 1 y {max} caracteres de longitud!",
"7010": "¡El tema LWT no debe contener caracteres de espacio!",
"7011": "¡El valor LWT en línea debe tener entre 1 y {max} caracteres de longitud!",
"7012": "¡El valor LWT fuera de línea debe tener entre 1 y {max} caracteres de longitud!",
"7013": "¡El intervalo de publicación debe ser un número entre {min} y {max}!",
"7014": "¡El tema Hass debe tener entre 1 y {max} caracteres de longitud!",
"7015": "¡El tema Hass no debe contener caracteres de espacio!",
"7016": "¡La QoS LWT no debe ser mayor que {max}!",
"7017": "Client ID must not longer then {max} characters!",
"8001": "¡La dirección IP no es válida!",
"8002": "¡La máscara de red no es válida!",
"8003": "¡El gateway no es válido!",
"8004": "¡La dirección IP del servidor DNS 1 no es válida!",
"8005": "¡La dirección IP del servidor DNS 2 no es válida!",
"8006": "¡El valor de tiempo de espera del punto de acceso administrativo es inválido!",
"9001": "¡El servidor NTP debe tener entre 1 y {max} caracteres de longitud!",
"9002": "¡La zona horaria debe tener entre 1 y {max} caracteres de longitud!",
"9003": "¡La descripción de la zona horaria debe tener entre 1 y {max} caracteres de longitud!",
"9004": "¡El año debe ser un número entre {min} y {max}!",
"9005": "¡El mes debe ser un número entre {min} y {max}!",
"9006": "¡El día debe ser un número entre {min} y {max}!",
"9007": "¡La hora debe ser un número entre {min} y {max}!",
"9008": "¡Los minutos deben ser un número entre {min} y {max}!",
"9009": "¡Los segundos deben ser un número entre {min} y {max}!",
"9010": "¡Hora actualizada!",
"10001": "¡La contraseña debe tener entre 8 y {max} caracteres de longitud!",
"10002": "¡Autenticación exitosa!",
"11001": "¡@:apiresponse.2001",
"11002": "¡@:apiresponse:5004",
"12001": "¡El perfil debe tener entre 1 y {max} caracteres de longitud!"
},
"home": {
"LiveData": "Datos en Vivo",
"SerialNumber": "Número de Serie: ",
"CurrentLimit": "Límite de Corriente: ",
"DataAge": "Edad de los Datos: ",
"Seconds": "{val} segundos",
"ShowSetInverterLimit": "Ver / Establecer Límite del Inversor",
"TurnOnOff": "Encender/Apagar el Inversor",
"ShowInverterInfo": "Ver Información del Inversor",
"ShowEventlog": "Ver Registro de Eventos",
"UnreadMessages": "mensajes sin leer",
"Loading": "@:base.Cargando",
"EventLog": "Registro de Eventos",
"InverterInfo": "Información del Inversor",
"LimitSettings": "Configuración de Límites",
"LastLimitSetStatus": "Último Estado de Configuración del Límite:",
"SetLimit": "Establecer Límite:",
"Relative": "Relativo (%)",
"Absolute": "Absoluto (W)",
"LimitHint": "<b>Consejo:</b> Si establece el límite como un valor absoluto, la visualización del valor actual solo se actualizará después de ~4 minutos.",
"SetPersistent": "Establecer Límite Permanente",
"SetNonPersistent": "Establecer Límite No Permanente",
"PowerSettings": "Configuración de Energía",
"LastPowerSetStatus": "Último Estado de Configuración de Energía:",
"TurnOn": "Encender",
"TurnOff": "Apagar",
"Restart": "Reiniciar",
"Failure": "Fallo",
"Pending": "Pendiente",
"Ok": "Aceptar",
"Unknown": "Desconocido",
"ShowGridProfile": "Ver Perfil de la Red",
"GridProfile": "Perfil de la Red",
"LoadingInverter": "Waiting for data... (can take up to 10 seconds)",
"RadioStats": "Radio Statistics",
"TxRequest": "TX Request Count",
"RxSuccess": "RX Success",
"RxFailNothing": "RX Fail: Receive Nothing",
"RxFailPartial": "RX Fail: Receive Partial",
"RxFailCorrupt": "RX Fail: Receive Corrupt",
"TxReRequest": "TX Re-Request Fragment",
"StatsReset": "Reset Statistics",
"StatsResetting": "Resetting...",
"Rssi": "RSSI of last received packet",
"RssiHint": "HM inverters only support RSSI values < -64 dBm and > -64 dBm. In this case, -80 dbm and -30 dbm is shown.",
"dBm": "{dbm} dBm"
},
"eventlog": {
"Start": "Iniciar",
"Stop": "Parar",
"Id": "ID",
"Message": "Mensaje"
},
"devinfo": {
"NoInfo": "Sin información disponible",
"NoInfoLong": "No se ha recibido ningún dato válido del inversor hasta ahora. Todavía estamos intentando...",
"UnknownModel": "¡Modelo desconocido! Por favor, informe el \"Número de parte de hardware\" y el modelo (por ejemplo, HM-350) como un problema <a href=\"https://github.com/tbnobody/OpenDTU/issues\" target=\"_blank\">aquí</a>.",
"Serial": "Número de serie",
"ProdYear": "Año de producción",
"ProdWeek": "Semana de producción",
"Model": "Modelo",
"DetectedMaxPower": "Potencia máxima detectada",
"BootloaderVersion": "Versión del cargador de arranque",
"FirmwareVersion": "Versión del firmware",
"FirmwareBuildDate": "Fecha de construcción del firmware",
"HardwarePartNumber": "Número de parte de hardware",
"HardwareVersion": "Versión de hardware"
},
"gridprofile": {
"NoInfo": "@:devinfo.NoInfo",
"NoInfoLong": "@:devinfo.NoInfoLong",
"Name": "Nombre",
"Version": "Versión",
"Enabled": "@:wifistationinfo.Enabled",
"Disabled": "@:wifistationinfo.Disabled",
"GridprofileSupport": "Apoyar el desarrollo",
"GridprofileSupportLong": "Por favor, consulte <a href=\"https://github.com/tbnobody/OpenDTU/wiki/Grid-Profile-Parser\" target=\"_blank\">aquí</a> para obtener más información."
},
"systeminfo": {
"SystemInfo": "Información del sistema",
"VersionError": "Error al obtener información de la versión",
"VersionNew": "¡Nueva versión disponible! ¡Mostrar cambios!",
"VersionOk": "¡Actualizado!"
},
"firmwareinfo": {
"FirmwareInformation": "Información del firmware",
"Hostname": "Hostname",
"SdkVersion": "Versión del SDK",
"ConfigVersion": "Versión de la configuración",
"FirmwareVersion": "Versión del firmware / Hash de Git",
"PioEnv": "Entorno PIO",
"FirmwareVersionHint": "Haga clic aquí para mostrar información sobre su versión actual",
"FirmwareUpdate": "Actualización de firmware",
"FirmwareUpdateHint": "Haga clic aquí para ver las diferencias entre su versión y la última versión",
"FrmwareUpdateAllow": "Al activar la comprobación de actualización, se envía una solicitud a GitHub.com cada vez que se llama a la página para recuperar la versión actualmente disponible. Si no está de acuerdo con esto, deje esta función desactivada.",
"ResetReason0": "Razón de reinicio CPU 0",
"ResetReason1": "Razón de reinicio CPU 1",
"ConfigSaveCount": "Contador de guardado de configuración",
"Uptime": "Tiempo de actividad",
"UptimeValue": "0 días {time} | 1 día {time} | {count} días {time}"
},
"hardwareinfo": {
"HardwareInformation": "Información del hardware",
"ChipModel": "Modelo de chip",
"ChipRevision": "Revisión de chip",
"ChipCores": "Núcleos del chip",
"CpuFrequency": "Frecuencia de la CPU",
"Mhz": "MHz",
"CpuTemperature": "CPU Temperature",
"FlashSize": "Flash Memory Size"
},
"memoryinfo": {
"MemoryInformation": "Información de la memoria",
"Type": "Tipo",
"Usage": "Uso",
"Free": "Libre",
"Used": "Usado",
"Size": "Tamaño",
"Heap": "Montón",
"PsRam": "PSRAM",
"LittleFs": "LittleFs",
"Sketch": "Boceto"
},
"heapdetails": {
"HeapDetails": "Detalles del montón",
"TotalFree": "Total libre",
"LargestFreeBlock": "Bloque libre contiguo más grande",
"MaxUsage": "Uso máximo desde el inicio",
"Fragmentation": "Nivel de fragmentación"
},
"taskdetails": {
"TaskDetails": "Task Details",
"Name": "Name",
"StackFree": "Stack Free",
"Priority": "Priority",
"Task_idle0": "Idle (CPU Core 0)",
"Task_idle1": "Idle (CPU Core 1)",
"Task_wifi": "Wi-Fi",
"Task_tit": "TCP/IP",
"Task_looptask": "Arduino Main Loop",
"Task_asynctcp": "Async TCP",
"Task_mqttclient": "MQTT Client",
"Task_huaweican0": "AC Charger CAN",
"Task_pmsdm": "PowerMeter (SDM)",
"Task_pmhttpjson": "PowerMeter (HTTP+JSON)",
"Task_pmsml": "PowerMeter (Serial SML)",
"Task_pmhttpsml": "PowerMeter (HTTP+SML)"
},
"radioinfo": {
"RadioInformation": "Información de la radio",
"Status": "Estado de {module}",
"ChipStatus": "Estado del chip de {module}",
"ChipType": "Tipo de chip de {module}",
"Connected": "conectado",
"NotConnected": "no conectado",
"Configured": "configurado",
"NotConfigured": "no configurado",
"Unknown": "Desconocido"
},
"networkinfo": {
"NetworkInformation": "Información de la red"
},
"wifistationinfo": {
"WifiStationInfo": "Información de WiFi (Estación)",
"Status": "Estado",
"Enabled": "habilitado",
"Disabled": "deshabilitado",
"Ssid": "SSID",
"Bssid": "BSSID",
"Quality": "Calidad",
"Rssi": "RSSI"
},
"wifiapinfo": {
"WifiApInfo": "Información de WiFi (Punto de acceso)",
"Status": "@:wifistationinfo.Status",
"Enabled": "@:wifistationinfo.Enabled",
"Disabled": "@:wifistationinfo.Disabled",
"Ssid": "@:wifistationinfo.Ssid",
"Stations": "# Estaciones"
},
"interfacenetworkinfo": {
"NetworkInterface": "Interfaz de red ({iface})",
"Hostname": "@:firmwareinfo.Hostname",
"IpAddress": "Dirección IP",
"Netmask": "Máscara de red",
"DefaultGateway": "Puerta de enlace predeterminada",
"Dns": "DNS {num}",
"MacAddress": "Dirección MAC"
},
"interfaceapinfo": {
"NetworkInterface": "Interfaz de red (Punto de acceso)",
"IpAddress": "@:interfacenetworkinfo.IpAddress",
"MacAddress": "@:interfacenetworkinfo.MacAddress"
},
"ntpinfo": {
"NtpInformation": "Información de NTP",
"ConfigurationSummary": "Resumen de configuración",
"Server": "Servidor",
"Timezone": "Zona horaria",
"TimezoneDescription": "Descripción de la zona horaria",
"CurrentTime": "Hora actual",
"Status": "Estado",
"Synced": "sincronizado",
"NotSynced": "no sincronizado",
"LocalTime": "Hora local",
"Sunrise": "Amanecer",
"Sunset": "Atardecer",
"NotAvailable": "No disponible",
"Mode": "Modo",
"Day": "Día",
"Night": "Noche"
},
"mqttinfo": {
"MqttInformation": "Información de MQTT",
"ConfigurationSummary": "@:ntpinfo.ConfigurationSummary",
"Status": "@:ntpinfo.Status",
"Enabled": "Habilitado",
"Disabled": "Deshabilitado",
"Server": "@:ntpinfo.Server",
"Port": "Puerto",
"ClientId": "Client ID",
"Username": "Nombre de usuario",
"BaseTopic": "Tema base",
"PublishInterval": "Intervalo de publicación",
"Seconds": "{sec} segundos",
"CleanSession": "Bandera CleanSession",
"Retain": "Retener",
"Tls": "TLS",
"RootCertifcateInfo": "Información del certificado raíz de CA",
"TlsCertLogin": "Iniciar sesión con certificado TLS",
"ClientCertifcateInfo": "Información del Certificado del Cliente",
"HassSummary": "Resumen de la Configuración de Descubrimiento Automático MQTT de Home Assistant",
"Expire": "Expirar",
"IndividualPanels": "Paneles Individuales",
"RuntimeSummary": "Resumen de Tiempo de Ejecución",
"ConnectionStatus": "Estado de Conexión",
"Connected": "conectado",
"Disconnected": "desconectado"
},
"console": {
"Console": "Consola",
"VirtualDebugConsole": "Consola de Depuración Virtual",
"EnableAutoScroll": "Habilitar Desplazamiento Automático",
"ClearConsole": "Limpiar Consola",
"CopyToClipboard": "Copiar al Portapapeles"
},
"inverterchannelinfo": {
"String": "Cadena {num}",
"Phase": "Fase {num}",
"General": "General"
},
"invertertotalinfo": {
"TotalYieldTotal": "Total de Rendimiento Acumulado",
"TotalYieldDay": "Total de Rendimiento del Día",
"TotalPower": "Potencia Total"
},
"inverterchannelproperty": {
"Power": "Potencia",
"Voltage": "Voltaje",
"Current": "Corriente",
"Power DC": "Potencia DC",
"YieldDay": "Rendimiento del Día",
"YieldTotal": "Rendimiento Total",
"Frequency": "Frecuencia",
"Temperature": "Temperatura",
"PowerFactor": "Factor de Potencia",
"ReactivePower": "Potencia Reactiva",
"Efficiency": "Eficiencia",
"Irradiation": "Irradiación"
},
"maintenancereboot": {
"DeviceReboot": "Reinicio del Dispositivo",
"PerformReboot": "Realizar Reinicio",
"Reboot": "¡Reiniciar!",
"Cancel": "@:base.Cancel",
"RebootOpenDTU": "Reiniciar OpenDTU",
"RebootQuestion": "¿Realmente desea reiniciar el dispositivo?",
"RebootHint": "<b>Nota:</b> Normalmente no es necesario realizar un reinicio manual. OpenDTU realiza cualquier reinicio necesario (por ejemplo, después de una actualización de firmware) automáticamente. También se adoptan configuraciones sin reiniciar. Si necesita reiniciar debido a un error, considere informarlo en <a href=\"https://github.com/tbnobody/OpenDTU/issues\" class=\"alert-link\" target=\"_blank\">https://github.com/tbnobody/OpenDTU/issues</a>."
},
"dtuadmin": {
"DtuSettings": "Configuración de DTU",
"DtuConfiguration": "Configuración de DTU",
"Serial": "Serial",
"SerialHint": "Tanto el inversor como el DTU tienen un número de serie. El número de serie del DTU se genera aleatoriamente en el primer inicio y generalmente no es necesario cambiarlo.",
"PollInterval": "Intervalo de Sondeo",
"Seconds": "Segundos",
"NrfPaLevel": "Potencia de Transmisión NRF24",
"CmtPaLevel": "Potencia de Transmisión CMT2300A",
"NrfPaLevelHint": "Utilizado para inversores HM. Asegúrese de que su fuente de alimentación sea lo suficientemente estable antes de aumentar la potencia de transmisión.",
"CmtPaLevelHint": "Utilizado para inversores HMS/HMT. Asegúrese de que su fuente de alimentación sea lo suficientemente estable antes de aumentar la potencia de transmisión.",
"CmtCountry": "Región/País CMT2300A",
"CmtCountryHint": "Cada país tiene asignaciones de frecuencia diferentes.",
"country_0": "Europa ({min}MHz - {max}MHz)",
"country_1": "América del Norte ({min}MHz - {max}MHz)",
"country_2": "Brasil ({min}MHz - {max}MHz)",
"CmtFrequency": "Frecuencia CMT2300A",
"CmtFrequencyHint": "¡Asegúrese de utilizar solo frecuencias permitidas en el país respectivo! Después de un cambio de frecuencia, puede tardar hasta 15 minutos en establecer una conexión.",
"CmtFrequencyWarning": "La frecuencia seleccionada está fuera del rango permitido en su región/país seleccionado. Asegúrese de que esta selección no infrinja ninguna regulación local.",
"MHz": "{mhz} MHz",
"dBm": "{dbm} dBm",
"Min": "Mínimo ({db} dBm)",
"Low": "Bajo ({db} dBm)",
"High": "Alto ({db} dBm)",
"Max": "Máximo ({db} dBm)"
},
"securityadmin": {
"SecuritySettings": "Configuración de Seguridad",
"AdminPassword": "Contraseña de Administrador",
"Password": "Contraseña",
"RepeatPassword": "Repetir Contraseña",
"PasswordHint": "<b>Consejo:</b> La contraseña de administrador se utiliza para acceder a esta interfaz web (usuario 'admin'), pero también para conectarse al dispositivo cuando está en modo AP. Debe tener 8 a 64 caracteres.",
"Permissions": "Permisos",
"ReadOnly": "Permitir acceso de solo lectura a la interfaz web sin contraseña"
},
"ntpadmin": {
"NtpSettings": "Configuración de NTP",
"NtpConfiguration": "Configuración de NTP",
"TimeServer": "Servidor de Tiempo",
"TimeServerHint": "El valor predeterminado es adecuado siempre que OpenDTU tenga acceso directo a Internet.",
"Timezone": "Zona Horaria",
"TimezoneConfig": "Configuración de Zona Horaria",
"LocationConfiguration": "Configuración de Ubicación",
"Longitude": "Longitud",
"Latitude": "Latitud",
"SunSetType": "Tipo de Atardecer",
"SunSetTypeHint": "Afecta al cálculo día/noche. Puede tardar hasta un minuto en aplicarse el nuevo tipo.",
"OFFICIAL": "Amanecer estándar (90.8°)",
"NAUTICAL": "Amanecer náutico (102°)",
"CIVIL": "Amanecer civil (96°)",
"ASTONOMICAL": "Amanecer astronómico (108°)",
"ManualTimeSynchronization": "Sincronización Manual del Tiempo",
"CurrentOpenDtuTime": "Hora Actual de OpenDTU",
"CurrentLocalTime": "Hora Local Actual",
"SynchronizeTime": "Sincronizar Tiempo",
"SynchronizeTimeHint": "<b>Consejo:</b> Puede utilizar la sincronización manual del tiempo para establecer la hora actual de OpenDTU si no hay un servidor NTP disponible. Pero tenga en cuenta que en caso de un ciclo de energía, se perderá la hora. Además, tenga en cuenta que la precisión del tiempo se verá gravemente afectada, ya que no se puede resincronizar regularmente y el microcontrolador ESP32 no tiene un reloj en tiempo real."
},
"networkadmin": {
"NetworkSettings": "Configuración de Red",
"WifiConfiguration": "Configuración de WiFi",
"WifiSsid": "SSID de WiFi",
"WifiPassword": "Contraseña de WiFi",
"Hostname": "Nombre de Host",
"HostnameHint": "<b>Consejo:</b> El texto <span class=\"font-monospace\">%06X</span> se remplazará con los últimos 6 dígitos del ChipID de ESP en formato hexadecimal.",
"EnableDhcp": "Habilitar DHCP",
"StaticIpConfiguration": "Configuración de IP Estática",
"IpAddress": "Dirección IP",
"Netmask": "Máscara de Red",
"DefaultGateway": "Puerta de Enlace Predeterminada",
"Dns": "Servidor DNS {num}",
"AdminAp": "Configuración de WiFi (Punto de Acceso de Administrador)",
"ApTimeout": "Tiempo de espera del Punto de Acceso",
"ApTimeoutHint": "Tiempo que se mantiene abierto el Punto de Acceso. Un valor de 0 significa infinito.",
"Minutes": "minutos",
"EnableMdns": "Habilitar mDNS",
"MdnsSettings": "Configuración de mDNS"
},
"mqttadmin": {
"MqttSettings": "Configuración de MQTT",
"MqttConfiguration": "Configuración de MQTT",
"EnableMqtt": "Habilitar MQTT",
"EnableHass": "Habilitar Descubrimiento Automático MQTT de Home Assistant",
"MqttBrokerParameter": "Parámetros del Broker MQTT",
"Hostname": "Nombre de Host",
"HostnameHint": "Nombre de host o dirección IP",
"Port": "Puerto",
"ClientId": "Client ID",
"Username": "Nombre de Usuario",
"UsernameHint": "Nombre de usuario, dejar vacío para conexión anónima",
"Password": "Contraseña",
"PasswordHint": "Contraseña, dejar vacío para conexión anónima",
"BaseTopic": "Tema Base",
"BaseTopicHint": "Tema base, se antepondrá a todos los temas publicados (por ejemplo, inverter/)",
"PublishInterval": "Intervalo de Publicación",
"Seconds": "segundos",
"CleanSession": "Habilitar Bandera CleanSession",
"EnableRetain": "Habilitar Bandera Retain",
"EnableTls": "Habilitar TLS",
"RootCa": "Certificado Raíz CA (predeterminado Letsencrypt)",
"TlsCertLoginEnable": "Habilitar Inicio de Sesión con Certificado TLS",
"ClientCert": "Certificado del Cliente TLS",
"ClientKey": "Clave del Cliente TLS",
"LwtParameters": "Parámetros de LWT",
"LwtTopic": "Tema de LWT",
"LwtTopicHint": "Tema de LWT, se añadirá al tema base",
"LwtOnline": "Mensaje de LWT en línea",
"LwtOnlineHint": "Mensaje que se publicará en el tema de LWT cuando esté en línea",
"LwtOffline": "Mensaje de LWT fuera de línea",
"LwtOfflineHint": "Mensaje que se publicará en el tema de LWT cuando esté fuera de línea",
"LwtQos": "QoS (Calidad de Servicio)",
"QOS0": "0 (Como máximo una vez)",
"QOS1": "1 (Al menos una vez)",
"QOS2": "2 (Exactamente una vez)",
"HassParameters": "Parámetros de Descubrimiento Automático MQTT de Home Assistant",
"HassPrefixTopic": "Tema de Prefijo",
"HassPrefixTopicHint": "El prefijo para el tema de descubrimiento",
"HassRetain": "Habilitar Bandera Retain",
"HassExpire": "Habilitar Expiración",
"HassIndividual": "Paneles Individuales"
},
"inverteradmin": {
"InverterSettings": "Configuración del Inversor",
"AddInverter": "Agregar un nuevo Inversor",
"Serial": "Serial",
"Name": "Nombre",
"Add": "Agregar",
"AddHint": "<b>Consejo:</b> Puede configurar parámetros adicionales después de haber creado el inversor. Use el ícono de lápiz en la lista de inversores.",
"InverterList": "Lista de Inversores",
"Status": "Estado",
"Send": "Enviar",
"Receive": "Recibir",
"StatusHint": "<b>Consejo:</b> El inversor se alimenta con su entrada de CC. Si no hay sol, el inversor está apagado. Aún se pueden enviar solicitudes.",
"Type": "Tipo",
"Action": "Acción",
"SaveOrder": "Guardar orden",
"DeleteInverter": "Eliminar inversor",
"EditInverter": "Editar inversor",
"General": "General",
"String": "Cadena",
"Advanced": "Avanzado",
"InverterSerial": "Serial del Inversor:",
"InverterName": "Nombre del Inversor:",
"InverterNameHint": "Aquí puede especificar un nombre personalizado para su inversor.",
"InverterStatus": "Recibir / Enviar",
"PollEnable": "Sondear datos del inversor",
"PollEnableNight": "Sondear datos del inversor por la noche",
"CommandEnable": "Enviar comandos",
"CommandEnableNight": "Enviar comandos por la noche",
"StringName": "Nombre de cadena {num}:",
"StringNameHint": "Aquí puede especificar un nombre personalizado para el puerto respectivo de su inversor.",
"StringMaxPower": "Potencia máxima de cadena {num}:",
"StringMaxPowerHint": "Ingrese la potencia máxima de los paneles solares conectados.",
"StringYtOffset": "Compensación total de rendimiento de cadena {num}:",
"StringYtOffsetHint": "Esta compensación se aplica al valor total de rendimiento leído del inversor. Esto se puede usar para ajustar el rendimiento total del inversor a cero si se utiliza un inversor usado. Pero aún puede intentar sondear datos.",
"InverterHint": "*) Ingrese W<sub>p</sub> del canal para calcular la irradiación.",
"ReachableThreshold": "Umbral de Alcanzabilidad",
"ReachableThresholdHint": "Define cuántas solicitudes se permiten fallar hasta que el inversor se considere no alcanzable.",
"ZeroRuntime": "Datos de tiempo cero",
"ZeroRuntimeHint": "Datos de tiempo cero (sin datos de rendimiento) si el inversor se vuelve inalcanzable.",
"ZeroDay": "Rendimiento diario cero a medianoche",
"ZeroDayHint": "Esto solo funciona si el inversor es inalcanzable. Si se leen datos del inversor, se usarán sus valores. (El reinicio solo ocurre en el ciclo de energía)",
"ClearEventlog": "Clear Eventlog at midnight",
"Cancel": "@:base.Cancel",
"Save": "@:base.Save",
"DeleteMsg": "¿Está seguro de que desea eliminar el inversor \"{name}\" con número de serie {serial}?",
"Delete": "Eliminar",
"YieldDayCorrection": "Corrección de Rendimiento Diario",
"YieldDayCorrectionHint": "Sumar el rendimiento diario incluso si el inversor se reinicia. El valor se restablecerá a medianoche"
},
"fileadmin": {
"ConfigManagement": "Gestión de Configuración",
"BackupHeader": "Copia de seguridad: Copia de Seguridad del Archivo de Configuración",
"BackupConfig": "Copia de seguridad del archivo de configuración",
"Backup": "Copia de seguridad",
"Restore": "Restaurar",
"NoFileSelected": "Ningún archivo seleccionado",
"RestoreHeader": "Restaurar: Restaurar el Archivo de Configuración",
"Back": "Atrás",
"UploadSuccess": "Carga Exitosa",
"RestoreHint": "<b>Nota:</b> Esta operación reemplaza el archivo de configuración con la configuración restaurada y reinicia OpenDTU para aplicar todas las configuraciones.",
"ResetHeader": "Inicializar: Realizar Restablecimiento de Fábrica",
"FactoryResetButton": "Restaurar Configuraciones Predeterminadas de Fábrica",
"ResetHint": "<b>Nota:</b> Haga clic en Restaurar Configuraciones Predeterminadas de Fábrica para restaurar e inicializar las configuraciones predeterminadas de fábrica y reiniciar.",
"FactoryReset": "Restablecimiento de Fábrica",
"ResetMsg": "¿Está seguro de que desea eliminar la configuración actual y restablecer todas las configuraciones a sus valores predeterminados de fábrica?",
"ResetConfirm": "Restablecimiento de Fábrica",
"Cancel": "@:base.Cancel",
"InvalidJson": "JSON file is formatted incorrectly.",
"InvalidJsonContent": "JSON file has the wrong content."
},
"login": {
"Login": "Iniciar Sesión",
"SystemLogin": "Inicio de Sesión en el Sistema",
"Username": "Nombre de Usuario",
"UsernameRequired": "Se requiere el nombre de usuario",
"Password": "Contraseña",
"PasswordRequired": "Se requiere la contraseña",
"LoginButton": "Iniciar Sesión"
},
"firmwareupgrade": {
"FirmwareUpgrade": "Actualización de Firmware",
"Loading": "@:base.Loading",
"OtaError": "Error OTA",
"Back": "Atrás",
"Retry": "Reintentar",
"OtaStatus": "Estado OTA",
"OtaSuccess": "La carga de firmware fue exitosa. El dispositivo se reinició automáticamente. Cuando el dispositivo vuelva a ser accesible, la interfaz se recargará automáticamente.",
"FirmwareUpload": "Carga de Firmware",
"UploadProgress": "Progreso de Carga"
},
"about": {
"AboutOpendtu": "Acerca de OpenDTU",
"Documentation": "Documentation",
"DocumentationBody": "The firmware and hardware documentation can be found here: <a href=\"https://www.opendtu.solar\" target=\"_blank\">https://www.opendtu.solar</a>",
"ProjectOrigin": "Origen del Proyecto",
"ProjectOriginBody1": "Este proyecto se inició a partir de <a href=\"https://www.mikrocontroller.net/topic/525778\" target=\"_blank\">esta discusión. (Mikrocontroller.net)</a>",
"ProjectOriginBody2": "El protocolo de Hoymiles fue descifrado mediante los esfuerzos voluntarios de muchos participantes. OpenDTU, entre otros, se desarrolló basado en este trabajo. El proyecto está bajo una Licencia de Código Abierto (<a href=\"https://www.gnu.de/documents/gpl-2.0.de.html\" target=\"_blank\">Licencia Pública General de GNU versión 2</a>).",
"ProjectOriginBody3": "El software se desarrolló según nuestro mejor conocimiento y creencia. Sin embargo, no se acepta ninguna responsabilidad por un mal funcionamiento o pérdida de garantía del inversor.",
"ProjectOriginBody4": "OpenDTU está disponible de forma gratuita. Si pagaste dinero por el software, probablemente te estafaron.",
"NewsUpdates": "Noticias y Actualizaciones",
"NewsUpdatesBody": "Las nuevas actualizaciones se pueden encontrar en Github: <a href=\"https://github.com/tbnobody/OpenDTU\" target=\"_blank\">https://github.com/tbnobody/OpenDTU</a>",
"ErrorReporting": "Reporte de Errores",
"ErrorReportingBody": "Por favor, informa problemas utilizando la función proporcionada por <a href=\"https://github.com/tbnobody/OpenDTU/issues\" target=\"_blank\">Github</a>",
"Discussion": "Discusión",
"DiscussionBody": "Discute con nosotros en <a href=\"https://discord.gg/WzhxEY62mB\" target=\"_blank\">Discord</a> o <a href=\"https://github.com/tbnobody/OpenDTU/discussions\" target=\"_blank\">Github</a>"
},
"hints": {
"RadioProblem": "No se pudo conectar a un módulo de radio configurado. Por favor, verifica la conexión.",
"TimeSync": "El reloj aún no ha sido sincronizado. Sin un reloj correctamente ajustado, no se realizan solicitudes al inversor. Esto es normal poco después del inicio. Sin embargo, después de un tiempo de ejecución más largo (>1 minuto), indica que el servidor NTP no es accesible.",
"TimeSyncLink": "Por favor, verifica la configuración de tu hora.",
"DefaultPassword": "Estás utilizando la contraseña predeterminada para la interfaz web y el punto de acceso de emergencia. Esto potencialmente es inseguro.",
"DefaultPasswordLink": "Por favor, cambia la contraseña."
},
"deviceadmin": {
"DeviceManager": "Administrador de Dispositivos",
"ParseError": "Error de análisis en 'pin_mapping.json': {error}",
"PinAssignment": "Configuración de Conexión",
"SelectedProfile": "Perfil Seleccionado",
"DefaultProfile": "(Configuraciones predeterminadas)",
"ProfileHint": "Tu dispositivo puede dejar de responder si seleccionas un perfil incompatible. En este caso, debes realizar una eliminación a través de la interfaz serial.",
"Display": "Pantalla",
"PowerSafe": "Habilitar Ahorro de Energía",
"PowerSafeHint": "Apaga la pantalla si no hay un inversor produciendo.",
"Screensaver": "Habilitar Protector de Pantalla",
"ScreensaverHint": "Mueve la pantalla un poco en cada actualización para evitar el quemado. (Útil especialmente para pantallas OLED)",
"DiagramMode": "Modo de Diagrama",
"off": "Apagar",
"small": "Pequeño",
"fullscreen": "Pantalla Completa",
"DiagramDuration": "Duración del Diagrama",
"DiagramDurationHint": "El período de tiempo que se muestra en el diagrama.",
"Seconds": "Segundos",
"Contrast": "Contraste ({contrast})",
"Rotation": "Rotación",
"rot0": "Sin rotación",
"rot90": "Rotación de 90 grados",
"rot180": "Rotación de 180 grados",
"rot270": "Rotación de 270 grados",
"DisplayLanguage": "Idioma de la Pantalla",
"en": "Inglés",
"de": "Alemán",
"fr": "Francés",
"Leds": "LEDs",
"EqualBrightness": "Brillo Equitativo",
"LedBrightness": "Brillo del LED {led} ({brightness})"
},
"pininfo": {
"Category": "Categoría",
"Name": "Nombre",
"Number": "Número",
"ValueSelected": "Seleccionado",
"ValueActive": "Activo"
},
"inputserial": {
"format_hoymiles": "Hoymiles serial number format",
"format_converted": "Already converted serial number",
"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"
}
}
}

690
lang/it.lang.json Normal file
View File

@ -0,0 +1,690 @@
{
"meta": {
"name": "Italiano",
"code": "it"
},
"display": {
"date_format": "%d/%m/%Y %H:%M",
"offline": "Offline",
"power_w": "%.0f W",
"power_kw": "%.1f kW",
"yield_today_wh": "oggi: %4.0f Wh",
"yield_today_kwh": "oggi: %.1f kWh",
"yield_total_kwh": "totale: %.1f kWh",
"yield_total_mwh": "totale: %.0f kWh"
},
"webapp": {
"menu": {
"LiveView": "Dati in tempo reale",
"Settings": "Impostazioni",
"NetworkSettings": "Impostazioni di rete",
"NTPSettings": "Impostazioni NTP",
"MQTTSettings": "Impostazioni MQTT",
"InverterSettings": "Impostazioni Inverter",
"SecuritySettings": "Impostazioni di Sicurezza",
"DTUSettings": "Impostazioni DTU",
"DeviceManager": "Gestione Dispositivi",
"ConfigManagement": "Gestione Configurazione",
"FirmwareUpgrade": "Aggiornamento Firmware",
"DeviceReboot": "Riavvio DTU",
"Info": "Info",
"System": "Sistema",
"Network": "Rete",
"NTP": "NTP",
"MQTT": "MQTT",
"Console": "Console",
"About": "Informazioni DTU",
"Logout": "Esci",
"Login": "Login"
},
"base": {
"Loading": "Caricamento...",
"Reload": "Ricarica",
"Cancel": "Cancella",
"Save": "Salva",
"Refreshing": "Aggiorna",
"Pull": "Trascina in basso per aggiornare",
"Release": "Rilascia per aggiornare",
"Close": "Chiudi"
},
"wait": {
"NotReady": "OpenDTU is not yet ready",
"PleaseWait": "Please wait. You will be automatically redirected to the home page."
},
"Error": {
"Oops": "Oops!"
},
"localeswitcher": {
"Dark": "Scuro",
"Light": "Chiaro",
"Auto": "Automatico"
},
"apiresponse": {
"1001": "Settings saved!",
"1002": "No values found!",
"1003": "Data too large!",
"1004": "Failed to parse data!",
"1005": "Values are missing!",
"1006": "Write failed!",
"2001": "Serial cannot be zero!",
"2002": "Poll interval must be greater zero!",
"2003": "Invalid power level setting!",
"2004": "The frequency must be set between {min} and {max} kHz and must be a multiple of 250kHz!",
"2005": "Invalid country selection!",
"3001": "Not deleted anything!",
"3002": "Configuration resettet. Rebooting now...",
"4001": "@:apiresponse.2001",
"4002": "Name must between 1 and {max} characters long!",
"4003": "Only {max} inverters are supported!",
"4004": "Inverter created!",
"4005": "Invalid ID specified!",
"4006": "Invalid amount of max channel setting given!",
"4007": "Inverter changed!",
"4008": "Inverter deleted!",
"4009": "Inverter order saved!",
"5001": "@:apiresponse.2001",
"5002": "Limit must between 1 and {max}!",
"5003": "Invalid type specified!",
"5004": "Invalid inverter specified!",
"6001": "Reboot triggered!",
"6002": "Reboot cancled!",
"7001": "MQTT Server must between 1 and {max} characters long!",
"7002": "Username must not longer then {max} characters!",
"7003": "Password must not longer then {max} characters!",
"7004": "Topic must not longer then {max} characters!",
"7005": "Topic must not contain space characters!",
"7006": "Topic must end with slash (/)!",
"7007": "Port must be a number between 1 and 65535!",
"7008": "Certificate must not longer then {max} characters!",
"7009": "LWT topic must not longer then {max} characters!",
"7010": "LWT topic must not contain space characters!",
"7011": "LWT online value must not longer then {max} characters!",
"7012": "LWT offline value must not longer then {max} characters!",
"7013": "Publish interval must be a number between {min} and {max}!",
"7014": "Hass topic must not longer then {max} characters!",
"7015": "Hass topic must not contain space characters!",
"7016": "LWT QOS must not greater then {max}!",
"7017": "Client ID must not longer then {max} characters!",
"8001": "IP address is invalid!",
"8002": "Netmask is invalid!",
"8003": "Gateway is invalid!",
"8004": "DNS Server IP 1 is invalid!",
"8005": "DNS Server IP 2 is invalid!",
"8006": "Administrative AccessPoint Timeout value is invalid",
"9001": "NTP Server must between 1 and {max} characters long!",
"9002": "Timezone must between 1 and {max} characters long!",
"9003": "Timezone description must between 1 and {max} characters long!",
"9004": "Year must be a number between {min} and {max}!",
"9005": "Month must be a number between {min} and {max}!",
"9006": "Day must be a number between {min} and {max}!",
"9007": "Hour must be a number between {min} and {max}!",
"9008": "Minute must be a number between {min} and {max}!",
"9009": "Second must be a number between {min} and {max}!",
"9010": "Time updated!",
"10001": "Password must between 8 and {max} characters long!",
"10002": "Authentication successful!",
"11001": "@:apiresponse.2001",
"11002": "@:apiresponse:5004",
"12001": "Profil must between 1 and {max} characters long!"
},
"home": {
"LiveData": "Dati in tempo reale",
"SerialNumber": "Numero seriale: ",
"CurrentLimit": "Limite attuale: ",
"DataAge": "Aggiornamento Dati: ",
"Seconds": "{val} secondi",
"ShowSetInverterLimit": "Mostra / Imposta Limite di Potenza",
"TurnOnOff": "Accendi/Spegni Inverter",
"ShowInverterInfo": "Mostra info Inverter",
"ShowEventlog": "Mostra Log Eventi",
"UnreadMessages": "msg non letti",
"Loading": "@:base.Loading",
"EventLog": "Log Eventi",
"InverterInfo": "Info Inverter",
"LimitSettings": "Impostazioni Limite Potenza",
"LastLimitSetStatus": "Stato ultimo limite impostato:",
"SetLimit": "Imposta Limite a:",
"Relative": "Percentuale (%)",
"Absolute": "Assoluto (W)",
"LimitHint": "<b>Nota:</b> Se imposti il limite assoluto, il valore sul display sarà aggiornato dopo circa 4 minuti.",
"SetPersistent": "Imposta Limite in Modo Persistente",
"SetNonPersistent": "Imposta Limite Temporaneamente",
"PowerSettings": "Impostazioni Potenza",
"LastPowerSetStatus": "Ultimo Stato dell'Inverter:",
"TurnOn": "Accendi Inverter",
"TurnOff": "Spegni Inverter",
"Restart": "Riavvia Inverter",
"Failure": "Fallito",
"Pending": "In Attesa",
"Ok": "Ok",
"Unknown": "Sconosciuto",
"ShowGridProfile": "Mostra Settaggi Inverter",
"GridProfile": "Settaggi Inverter",
"LoadingInverter": "In attesa dei dati... (puo' richiedere fino a 10 secondi)",
"RadioStats": "Radio Statistics",
"TxRequest": "TX Request Count",
"RxSuccess": "RX Success",
"RxFailNothing": "RX Fail: Receive Nothing",
"RxFailPartial": "RX Fail: Receive Partial",
"RxFailCorrupt": "RX Fail: Receive Corrupt",
"TxReRequest": "TX Re-Request Fragment",
"StatsReset": "Reset Statistics",
"StatsResetting": "Resetting...",
"Rssi": "RSSI of last received packet",
"RssiHint": "HM inverters only support RSSI values < -64 dBm and > -64 dBm. In this case, -80 dbm and -30 dbm is shown.",
"dBm": "{dbm} dBm"
},
"eventlog": {
"Start": "Inizio",
"Stop": "Fine",
"Id": "ID",
"Message": "Messaggio"
},
"devinfo": {
"NoInfo": "Informazioni non disponibili",
"NoInfoLong": "Ancora nessuna informazione dall'inverter. Sto riprovando...",
"UnknownModel": "Modello sconosciuto! Per favore fornisci \"Hardware Part Number\" ed il modello (esempio HM-350) in una Issue su <a href=\"https://github.com/tbnobody/OpenDTU/issues\" target=\"_blank\">GitHub</a>.",
"Serial": "Seriale",
"ProdYear": "Produzione Annua",
"ProdWeek": "Produzione Settimanale",
"Model": "Modello",
"DetectedMaxPower": "Rilevata potenza massima",
"BootloaderVersion": "Versione Bootloader",
"FirmwareVersion": "Versione Firmware",
"FirmwareBuildDate": "Data Firmware",
"HardwarePartNumber": "Hardware Part Number",
"HardwareVersion": "Hardware Version"
},
"gridprofile": {
"NoInfo": "@:devinfo.NoInfo",
"NoInfoLong": "@:devinfo.NoInfoLong",
"Name": "Nome",
"Version": "Versione",
"Enabled": "@:wifistationinfo.Enabled",
"Disabled": "@:wifistationinfo.Disabled",
"GridprofileSupport": "Supporto sviluppatori",
"GridprofileSupportLong": "Clicca <a href=\"https://github.com/tbnobody/OpenDTU/wiki/Grid-Profile-Parser\" target=\"_blank\">qui</a> per ulteriori informazioni."
},
"systeminfo": {
"SystemInfo": "Info Sistema",
"VersionError": "Errore ricezione della versione",
"VersionNew": "Nuova versione disponibile! Mostra aggiornamenti!",
"VersionOk": "Già aggiornato!"
},
"firmwareinfo": {
"FirmwareInformation": "Info Firmware",
"Hostname": "Hostname",
"SdkVersion": "SDK Version",
"ConfigVersion": "Config Version",
"FirmwareVersion": "Firmware Version / Git Hash",
"PioEnv": "PIO Environment",
"FirmwareVersionHint": "Click here to show information about your current version",
"FirmwareUpdate": "Firmware Update",
"FirmwareUpdateHint": "Click here to view the changes between your version and the latest version",
"FrmwareUpdateAllow": "By activating the update check, a request is sent to GitHub.com each time the page is called up to retrieve the currently available version. If you do not agree with this, leave this function deactivated.",
"ResetReason0": "Reset Reason CPU 0",
"ResetReason1": "Reset Reason CPU 1",
"ConfigSaveCount": "Config save count",
"Uptime": "Uptime",
"UptimeValue": "0 days {time} | 1 day {time} | {count} days {time}"
},
"hardwareinfo": {
"HardwareInformation": "Info Hardware",
"ChipModel": "Chip Model",
"ChipRevision": "Chip Revision",
"ChipCores": "Chip Cores",
"CpuFrequency": "CPU Frequency",
"Mhz": "MHz",
"CpuTemperature": "CPU Temperature",
"FlashSize": "Flash Memory Size"
},
"memoryinfo": {
"MemoryInformation": "Info Memoria",
"Type": "Tipo",
"Usage": "Uso",
"Free": "Libera",
"Used": "Usata",
"Size": "Dimensione",
"Heap": "Heap",
"PsRam": "PSRAM",
"LittleFs": "LittleFs",
"Sketch": "Sketch"
},
"heapdetails": {
"HeapDetails": "Dettagli memoria Heap",
"TotalFree": "Libera totale",
"LargestFreeBlock": "Blocco contiguo libero più grande",
"MaxUsage": "Massima utilizzata dall'avvio",
"Fragmentation": "Livello frammentazione"
},
"taskdetails": {
"TaskDetails": "Task Details",
"Name": "Name",
"StackFree": "Stack Free",
"Priority": "Priority",
"Task_idle0": "Idle (CPU Core 0)",
"Task_idle1": "Idle (CPU Core 1)",
"Task_wifi": "Wi-Fi",
"Task_tit": "TCP/IP",
"Task_looptask": "Arduino Main Loop",
"Task_asynctcp": "Async TCP",
"Task_mqttclient": "MQTT Client",
"Task_huaweican0": "AC Charger CAN",
"Task_pmsdm": "PowerMeter (SDM)",
"Task_pmhttpjson": "PowerMeter (HTTP+JSON)",
"Task_pmsml": "PowerMeter (Serial SML)",
"Task_pmhttpsml": "PowerMeter (HTTP+SML)"
},
"radioinfo": {
"RadioInformation": "Info Transceiver Radio",
"Status": "{module} Stato",
"ChipStatus": "{module} Chip Stato",
"ChipType": "{module} Chip Tipo",
"Connected": "connesso",
"NotConnected": "non connesso",
"Configured": "configurato",
"NotConfigured": "no configurato",
"Unknown": "Sconosciuto"
},
"networkinfo": {
"NetworkInformation": "Informazioni Rete"
},
"wifistationinfo": {
"WifiStationInfo": "Info WiFi (Station)",
"Status": "Stato",
"Enabled": "abilitato",
"Disabled": "disabilitato",
"Ssid": "SSID",
"Bssid": "BSSID",
"Quality": "Qualità",
"Rssi": "RSSI"
},
"wifiapinfo": {
"WifiApInfo": "Info WiFi (Access Point)",
"Status": "@:wifistationinfo.Status",
"Enabled": "@:wifistationinfo.Enabled",
"Disabled": "@:wifistationinfo.Disabled",
"Ssid": "@:wifistationinfo.Ssid",
"Stations": "Numero Stazioni"
},
"interfacenetworkinfo": {
"NetworkInterface": "Interfaccia di Rete ({iface})",
"Hostname": "@:firmwareinfo.Hostname",
"IpAddress": "Indirizzo IP",
"Netmask": "Netmask",
"DefaultGateway": "Gateway",
"Dns": "DNS {num}",
"MacAddress": "Indirizzo MAC"
},
"interfaceapinfo": {
"NetworkInterface": "Interfaccia di Rete (Access Point)",
"IpAddress": "@:interfacenetworkinfo.IpAddress",
"MacAddress": "@:interfacenetworkinfo.MacAddress"
},
"ntpinfo": {
"NtpInformation": "Informazioni NTP",
"ConfigurationSummary": "Riepilogo Configurazione",
"Server": "Server",
"Timezone": "Timezone",
"TimezoneDescription": "Descrizione Timezone",
"CurrentTime": "Data/Ora attuale",
"Status": "Stato",
"Synced": "sincronizzata",
"NotSynced": "non sincronizzata",
"LocalTime": "Ora Locale",
"Sunrise": "Alba",
"Sunset": "Tramonto",
"NotAvailable": "Non Disponibile",
"Mode": "Modalità",
"Day": "Giorno",
"Night": "Notte"
},
"mqttinfo": {
"MqttInformation": "Informazioni MQTT",
"ConfigurationSummary": "@:ntpinfo.ConfigurationSummary",
"Status": "@:ntpinfo.Status",
"Enabled": "Abilitato",
"Disabled": "Disabilitato",
"Server": "@:ntpinfo.Server",
"Port": "Porta",
"ClientId": "Client ID",
"Username": "Username",
"BaseTopic": "Topic Base",
"PublishInterval": "Intervallo Publish",
"Seconds": "{sec} secondi",
"CleanSession": "CleanSession",
"Retain": "Retain",
"Tls": "TLS",
"RootCertifcateInfo": "Info Certificato Root CA",
"TlsCertLogin": "Entra con Certificato TLS",
"ClientCertifcateInfo": "Info Certificato Client",
"HassSummary": "Riepilogo Configurazione Home Assistant MQTT Auto Discovery",
"Expire": "Scade",
"IndividualPanels": "Pannello Individuale",
"RuntimeSummary": "Riepilogo Runtime",
"ConnectionStatus": "Stato Connessione",
"Connected": "connesso",
"Disconnected": "disconnesso"
},
"console": {
"Console": "Console",
"VirtualDebugConsole": "Virtual Debug Console",
"EnableAutoScroll": "Abilita AutoScroll",
"ClearConsole": "Pulisci Console",
"CopyToClipboard": "Copia nella clipboard"
},
"inverterchannelinfo": {
"String": "Stringa {num}",
"Phase": "Fase {num}",
"General": "Generale"
},
"invertertotalinfo": {
"TotalYieldTotal": "Totale Energia",
"TotalYieldDay": "Energia Giornaliera",
"TotalPower": "Potenza Totale"
},
"inverterchannelproperty": {
"Power": "Potenza",
"Voltage": "Tensione",
"Current": "Corrente",
"Power DC": "PotenzaDC",
"YieldDay": "EnergiaOggi",
"YieldTotal": "EnergiaTotale",
"Frequency": "Frequenza",
"Temperature": "Temperatura",
"PowerFactor": "FattorePotenza",
"ReactivePower": "PotenzaReattiva",
"Efficiency": "Efficienza",
"Irradiation": "Irragiamento"
},
"maintenancereboot": {
"DeviceReboot": "Riavvio DTU",
"PerformReboot": "Fai il riavvio",
"Reboot": "Riavvio!",
"Cancel": "@:base.Cancel",
"RebootOpenDTU": "Riavvio OpenDTU",
"RebootQuestion": "Vuoi veramente riavvia il DTU?",
"RebootHint": "<b>Nota:</b> Normalmente non serve riavviare OpenDTU, in quanto esegue automaticamente il ravvio quando necessario (ad esempio dopo aggiornamento firmware). Modifiche alla configurazione vengono apprese subito, senza richiedere riavvio. Se devi riavviare a causa di un errore, ti preghiamo di segnalarcelo cliccando su <a href=\"https://github.com/tbnobody/OpenDTU/issues\" class=\"alert-link\" target=\"_blank\">https://github.com/tbnobody/OpenDTU/issues</a>."
},
"dtuadmin": {
"DtuSettings": "Impostazioni DTU",
"DtuConfiguration": "Configurazione DTU",
"Serial": "Seriale",
"SerialHint": "Sia il DTU che l'inverter hanno un numero seriale. Il numero seriale del DTU è generato casualmente al primo avvio e normalmente non serve modificarlo.",
"PollInterval": "Intervallo Interrogazione",
"Seconds": "Secondi",
"NrfPaLevel": "Potenza Trasmettitore NRF24",
"CmtPaLevel": "Potenza Trasmettitore CMT2300A",
"NrfPaLevelHint": "Usato per inverter HM. Considera che aumentando la potenza aumentano il consumo di corrente.",
"CmtPaLevelHint": "Usato per inverter HMS/HMT. Considera che aumentando la potenza aumentano il consumo di corrente.",
"CmtCountry": "CMT2300A Zona/Paese",
"CmtCountryHint": "Ogni zona ha una differente allocazione di frequenze utilizzabili.",
"country_0": "Europa ({min}MHz - {max}MHz)",
"country_1": "Nord America ({min}MHz - {max}MHz)",
"country_2": "Brasile ({min}MHz - {max}MHz)",
"CmtFrequency": "Frequenza CMT2300A",
"CmtFrequencyHint": "Fai attenzione ad usare solo frequenze ammesse nel tuo Paese! Dopo la modifica frequenza, servono fino a 15 minuti affinché la connessione si ristabilisca.",
"CmtFrequencyWarning": "La frequenza selezionata è fuori dal range selezionato dal tuo Paese. Verifica che la frequenza selezionata non violi le normative del tuo Paese.",
"MHz": "{mhz} MHz",
"dBm": "{dbm} dBm",
"Min": "Minima ({db} dBm)",
"Low": "Bassa ({db} dBm)",
"High": "Alta ({db} dBm)",
"Max": "Massima ({db} dBm)"
},
"securityadmin": {
"SecuritySettings": "Impostazioni di Sicurezza",
"AdminPassword": "Password Admin",
"Password": "Password",
"RepeatPassword": "Ripeti Password",
"PasswordHint": "<b>Nota:</b> La password di amministrazione viene utilizzata non solo per accedere a questa interfaccia web (con user 'admin'), ma anche per connettersi al dispositivo in modalità AP. Deve avere da 8 a 64 caratteri.",
"Permissions": "Permessi",
"ReadOnly": "Permetti accessi web in sola lettura senza richiedere la password"
},
"ntpadmin": {
"NtpSettings": "Impostazioni NTP (Data / Ora)",
"NtpConfiguration": "Configurazione NTP",
"TimeServer": "Server NTP",
"TimeServerHint": "Puoi lasciare il valore di default, nel caso in cui OpenDTU abbia accesso ad internet.",
"Timezone": "Timezone",
"TimezoneConfig": "Timezone Config",
"LocationConfiguration": "Configurazione Posizione",
"Longitude": "Longitudine",
"Latitude": "Latitudine",
"SunSetType": "Tipo di Alba",
"SunSetTypeHint": "Influenza il calcolo dell'ora di Alba/Tramonto. Dopo la conferma, è richiesto fino ad un minuto perché la modifica venga applicata.",
"OFFICIAL": "Standard dawn (90.8°)",
"NAUTICAL": "Nautical dawn (102°)",
"CIVIL": "Civil dawn (96°)",
"ASTONOMICAL": "Astronomical dawn (108°)",
"ManualTimeSynchronization": "Sincronizzazione Manuale Data/Ora",
"CurrentOpenDtuTime": "Ora OpenDTU attuale",
"CurrentLocalTime": "Ora Locale attuale",
"SynchronizeTime": "Sincronizza Data/Ora",
"SynchronizeTimeHint": "<b>Nota:</b> Puoi usare la sincronizzazione manuale per impostare Data/Ora nel caso che non sia disponibile un server NTP. In questo caso la data/ora viene persa in caso di mancata alimentazione. Inoltre, con la sincronizzazione manuale ci sarà una progressiva deriva della Data/Ora in quanto l'ESP32 non ha un Real Time Clock interno."
},
"networkadmin": {
"NetworkSettings": "Impostazioni di Rete",
"WifiConfiguration": "Configurazione WiFi",
"WifiSsid": "WiFi SSID",
"WifiPassword": "WiFi Password",
"Hostname": "Hostname",
"HostnameHint": "<b>Nota:</b> Il testo <span class=\"font-monospace\">%06X</span> sarà rimpiazzato con le ultime 6 cifre del ChipID dell'ESP32 in formato esadecimale.",
"EnableDhcp": "Abilita DHCP",
"StaticIpConfiguration": "Configurazione IP Statico",
"IpAddress": "Indirizzo IP",
"Netmask": "Netmask",
"DefaultGateway": "Default Gateway",
"Dns": "DNS Server {num}",
"AdminAp": "Configurazione WiFi (Admin AccessPoint)",
"ApTimeout": "Timeout AccessPoint",
"ApTimeoutHint": "Tempo in cui la modalità AccessPoint rimarrà attiva. 0=per sempre.",
"Minutes": "minuti",
"EnableMdns": "Abilita mDNS",
"MdnsSettings": "Configurazione mDNS"
},
"mqttadmin": {
"MqttSettings": "Impostazioni MQTT",
"MqttConfiguration": "Configurazione MQTT",
"EnableMqtt": "Abilita MQTT",
"EnableHass": "Abilita Home Assistant MQTT Auto Discovery",
"MqttBrokerParameter": "Parametri Broker MQTT",
"Hostname": "Hostname",
"HostnameHint": "Hostname o Indirizzo IP",
"Port": "Porta",
"ClientId": "Client ID",
"Username": "Username",
"UsernameHint": "Username, lascia vuoto per connessione anonima",
"Password": "Password",
"PasswordHint": "Password, lascia vuota per connessione anonima",
"BaseTopic": "Topic Base",
"BaseTopicHint": "Topic Base, prefisso da aggiungere (ad esempio inverter/)",
"PublishInterval": "Intervallo pubblicazione",
"Seconds": "secondi",
"CleanSession": "Abilita CleanSession",
"EnableRetain": "Abilita Retain",
"EnableTls": "Abilita TLS",
"RootCa": "CA-Root-Certificate (default Letsencrypt)",
"TlsCertLoginEnable": "Abilita Login con certificato TLS",
"ClientCert": "TLS Client-Certificate",
"ClientKey": "TLS Client-Key",
"LwtParameters": "Parametri LWT",
"LwtTopic": "Topic LWT",
"LwtTopicHint": "Topic LWT, da aggiungere al Topic Base",
"LwtOnline": "Messaggio 'Online0 LWT",
"LwtOnlineHint": "Messaggio pubblicato quando online",
"LwtOffline": "Messaggio 'Offline' LWT",
"LwtOfflineHint": "Messaggio che sarà pubblicato quando offline",
"LwtQos": "QoS (Quality of Service)",
"QOS0": "0 (Al massimo una volta)",
"QOS1": "1 (Almeno una volta)",
"QOS2": "2 (Esattamente una volta)",
"HassParameters": "Parametri Home Assistant MQTT Auto Discovery",
"HassPrefixTopic": "Prefisso Topic",
"HassPrefixTopicHint": "Prefisso per Topic autodiscovery",
"HassRetain": "Abilita Retain",
"HassExpire": "Abilita Scadenza",
"HassIndividual": "Pannelli Individuale"
},
"inverteradmin": {
"InverterSettings": "Impostazioni Inverter",
"AddInverter": "Aggiungi nuovo Inverter",
"Serial": "Seriale",
"Name": "Nome",
"Add": "Aggiungi",
"AddHint": "<b>Nota:</b> Potrai aggiungere ulteriori parametri dopo aver creato l'inverter, cliccando sull'icona 'Matita' nella lista inverter.",
"InverterList": "Lista Inverter",
"Status": "Stato",
"Send": "Invia",
"Receive": "Riceve",
"StatusHint": "<b>Nota:</b> L'inverter viene alimentato dal fotovoltaico. Durante la notte, l'inverter risulterà spento. Le richieste potranno comunque essere trasmesse.",
"Type": "Tipo",
"Action": "Azione",
"SaveOrder": "Salva ordine",
"DeleteInverter": "Rimuovi inverter",
"EditInverter": "Modifica inverter",
"General": "Generale",
"String": "Stringa",
"Advanced": "Avanzate",
"InverterSerial": "Seriale Inverter:",
"InverterName": "Nome Inverter:",
"InverterNameHint": "Puoi specificare un nome qualsiasi da assegnare all'inverter.",
"InverterStatus": "Riceve / Invia",
"PollEnable": "Interroga inverter",
"PollEnableNight": "Interroga inverter di notte",
"CommandEnable": "Invia comandi",
"CommandEnableNight": "Invia comandi di notte",
"StringName": "Nome stringa {num}:",
"StringNameHint": "Qui puoi specificare un nome qualsiasi per la porta dell'inverter o per il pannello fotovoltaico collegato.",
"StringMaxPower": "Massima potenza stringa {num}:",
"StringMaxPowerHint": "Inserisci la potenza massima associata ai panelli fotovoltaici collegati a questa stringa.",
"StringYtOffset": "Offset Energia totale per la stringa {num}:",
"StringYtOffsetHint": "Questo offset viene utilizzato per azzerare il contatore qualora venga usato un inverter usato.",
"InverterHint": "*) Inserisci la potenza W<sub>p</sub> dei pannelli fotovoltaici collegati alla stringa: servirà per calcolare l'irragiamento.",
"ReachableThreshold": "Reachable Threshold",
"ReachableThresholdHint": "Definisce il numero di richieste fallite prima che l'inverter sia considerato irraggiungibile.",
"ZeroRuntime": "Azzera dati in tempo reale",
"ZeroRuntimeHint": "Azzera i dati in tempo reale (tranne l'Energia) se l'inverter diventa irraggiunbile.",
"ZeroDay": "Azzera dati energia alla mezzanotte",
"ZeroDayHint": "Questo vale se l'inverter risulta irraggiungibile. Se l'inverter risponde anche di notte, verranno mostrati i suoi valori. (Il Reset si verifica al riavvio)",
"ClearEventlog": "Clear Eventlog at midnight",
"Cancel": "@:base.Cancel",
"Save": "@:base.Save",
"DeleteMsg": "Sicuro di voler rimuovere l'inverter \"{name}\" con numero seriale {serial}?",
"Delete": "Rimuovi",
"YieldDayCorrection": "Correzione energia giornaliera",
"YieldDayCorrectionHint": "Aggiungi questo valore all'energia giornaliera se l'inverter è stato riavviato. Questo valore sarò resettato a mezzanotte"
},
"fileadmin": {
"ConfigManagement": "Configurazione Gestione",
"BackupHeader": "Backup: Configurazione File Backup",
"BackupConfig": "Esegui il backup del file",
"Backup": "Backup",
"Restore": "Ripristina",
"NoFileSelected": "Nessun file selezionato",
"RestoreHeader": "Ripristina: Ripristina File Configurazione",
"Back": "Indietro",
"UploadSuccess": "Invio File con successo",
"RestoreHint": "<b>Nota:</b> questa operazione rimpiazza la configurazione con quella contenuta nel file, e poi riavvia automaticamente OpenDTU per applicare la nuova configurazione.",
"ResetHeader": "Inizializza: Esegui il Factory Reset",
"FactoryResetButton": "Ripristina Configurazione Factory-Default",
"ResetHint": "<b>Nota:</b> Clicca 'Ripristina Configurazione Factory-Default' per stabilire le impostazioni di fabbrica e riavviare automaticamente OpenDTU.",
"FactoryReset": "Factory Reset",
"ResetMsg": "Sei sicuro di voler cancellare la configurazione attuale e applicare la configurazione di fabbrica?",
"ResetConfirm": "Factory Reset!",
"Cancel": "@:base.Cancel",
"InvalidJson": "JSON file is formatted incorrectly.",
"InvalidJsonContent": "JSON file has the wrong content."
},
"login": {
"Login": "Login",
"SystemLogin": "System Login",
"Username": "Username",
"UsernameRequired": "Inserisci Username",
"Password": "Password",
"PasswordRequired": "Inserisci Password",
"LoginButton": "Login"
},
"firmwareupgrade": {
"FirmwareUpgrade": "Aggiornamento Firmware",
"Loading": "@:base.Loading",
"OtaError": "Errore aggiornamento OTA",
"Back": "Indietro",
"Retry": "Riprova",
"OtaStatus": "Stato OTA",
"OtaSuccess": "Aggiornamento firmware eseguito con successo. Il dispositivo si riavvierà automaticamente. Quando sarà nuovamente disponibile, l'interfacca sarà ricaricata automaticamente.",
"FirmwareUpload": "Invia Firmware",
"UploadProgress": "Upload in corso"
},
"about": {
"AboutOpendtu": "About OpenDTU",
"Documentation": "Documentazione",
"DocumentationBody": "La documentazione firmware e hardware sono disponibili qui: <a href=\"https://www.opendtu.solar\" target=\"_blank\">https://www.opendtu.solar</a>",
"ProjectOrigin": "Origine Progetto",
"ProjectOriginBody1": "Questo progetto è partito da <a href=\"https://www.mikrocontroller.net/topic/525778\" target=\"_blank\">questa discussione. (Mikrocontroller.net)</a>",
"ProjectOriginBody2": "Il protocollo Hoymiles è stato decriptato grazie al contributo volontario di molti programmatori. OpenDTU, fra gli altri, è stato sviluppato grazie a questo lavoro. Il progetto è distribuito con Licenza Open Source (<a href=\"https://www.gnu.de/documents/gpl-2.0.de.html\" target=\"_blank\">GNU General Public License version 2</a>).",
"ProjectOriginBody3": "Il software è stato sviluppato con le nostre migliori conoscenze e convinzioni. Tuttavia, non si assume alcuna responsabilità per malfunzionamenti o perdita di garanzia dell'inverter.",
"ProjectOriginBody4": "OpenDTU è disponibile gratuitamente. Se hai pagato per questo software, probabilmente sei stato truffato.",
"NewsUpdates": "Novità e Aggiornamenti",
"NewsUpdatesBody": "Nuovi aggiornamenti sono disponibili su Github: <a href=\"https://github.com/tbnobody/OpenDTU\" target=\"_blank\">https://github.com/tbnobody/OpenDTU</a>",
"ErrorReporting": "Segnalazione Errori",
"ErrorReportingBody": "Per favore segnala eventuali problemi utilizzando le funzionalità della piattaforma <a href=\"https://github.com/tbnobody/OpenDTU/issues\" target=\"_blank\">Github</a>",
"Discussion": "Discussioni",
"DiscussionBody": "Puoi avviare una discussione con noi su <a href=\"https://discord.gg/WzhxEY62mB\" target=\"_blank\">Discord</a> o <a href=\"https://github.com/tbnobody/OpenDTU/discussions\" target=\"_blank\">Github</a>"
},
"hints": {
"RadioProblem": "Non è possibile dialogare con il modulo radio selezionato. Controlla i collegamenti alla radio.",
"TimeSync": "La Data/Ora non sono state sincronizzate, ed in tal caso non è possibile eseguire richieste all'inverter. Questa condizione è normale appena avviato, tuttavia dopo un po' (>1 minuto), questa situazione potrebbe indicare un problema di accesso al server NTP.",
"TimeSyncLink": "Controlla le impostazioni Data/Ora.",
"DefaultPassword": "Stai usando la password di default per accedere all'interfaccia web e per la modalità Access Point di emergenza. Questo può portare ad un rischio di sicurezza.",
"DefaultPasswordLink": "Per favore cambia la password."
},
"deviceadmin": {
"DeviceManager": "Device-Manager",
"ParseError": "Parse error in 'pin_mapping.json': {error}",
"PinAssignment": "Impostazioni Connessione",
"SelectedProfile": "Profilo selezionato",
"DefaultProfile": "(Impostazioni di Default)",
"ProfileHint": "Il tuo dispositivo potrebbe smettere di rispondere selezionando un profilo incompatibile. In questo caso, dovrai eseguire una cancellazione collegandoti all'interfaccia seriale.",
"Display": "Display",
"PowerSafe": "Abilita Risparmio Energetico",
"PowerSafeHint": "Spegni il display se l'inverter non produce.",
"Screensaver": "Abilita Screensaver",
"ScreensaverHint": "Muove il testo nel display per prevenire danneggiamento pixel. (Utile in caso di display OLED)",
"DiagramMode": "Modalità grafica",
"off": "Off",
"small": "Small",
"fullscreen": "Fullscreen",
"DiagramDuration": "Durata grafico",
"DiagramDurationHint": "Periodo che viene mostrato nel grafico.",
"Seconds": "Secondi",
"Contrast": "Contrasto ({contrast})",
"Rotation": "Rotazione",
"rot0": "Nessuna rotazione",
"rot90": "Rotazione 90 gradi",
"rot180": "Rotazione 180 gradi",
"rot270": "Rotazione 270 gradi",
"DisplayLanguage": "Linuga Display",
"en": "English",
"de": "German",
"fr": "French",
"Leds": "LEDs",
"EqualBrightness": "Equalizza luminosità",
"LedBrightness": "LED {led}, Luminosità ({brightness})"
},
"pininfo": {
"Category": "Categoria",
"Name": "Nome",
"Number": "Numero",
"ValueSelected": "Selezionato",
"ValueActive": "Attivo"
},
"inputserial": {
"format_hoymiles": "Hoymiles serial number format",
"format_converted": "Already converted serial number",
"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"
}
}
}

View File

@ -34,7 +34,7 @@ uint32_t HoymilesRadio_CMT::getFrequencyFromChannel(const uint8_t channel) const
uint8_t HoymilesRadio_CMT::getChannelFromFrequency(const uint32_t frequency) const uint8_t HoymilesRadio_CMT::getChannelFromFrequency(const uint32_t frequency) const
{ {
if ((frequency % getChannelWidth()) != 0) { if ((frequency % getChannelWidth()) != 0) {
Hoymiles.getMessageOutput()->printf("%.3f MHz is not divisible by %d kHz!\r\n", frequency / 1000000.0, getChannelWidth()); Hoymiles.getMessageOutput()->printf("%.3f MHz is not divisible by %" PRId32 " kHz!\r\n", frequency / 1000000.0, getChannelWidth());
return 0xFF; // ERROR return 0xFF; // ERROR
} }
if (frequency < getMinFrequency() || frequency > getMaxFrequency()) { if (frequency < getMinFrequency() || frequency > getMaxFrequency()) {
@ -43,7 +43,7 @@ uint8_t HoymilesRadio_CMT::getChannelFromFrequency(const uint32_t frequency) con
return 0xFF; // ERROR return 0xFF; // ERROR
} }
if (frequency < countryDefinition.at(_countryMode).Freq_Legal_Min || frequency > countryDefinition.at(_countryMode).Freq_Legal_Max) { if (frequency < countryDefinition.at(_countryMode).Freq_Legal_Min || frequency > countryDefinition.at(_countryMode).Freq_Legal_Max) {
Hoymiles.getMessageOutput()->printf("!!! caution: %.2f MHz is out of region legal range! (%d - %d MHz)\r\n", Hoymiles.getMessageOutput()->printf("!!! caution: %.2f MHz is out of region legal range! (%" PRId32 " - %" PRId32 " MHz)\r\n",
frequency / 1000000.0, frequency / 1000000.0,
static_cast<uint32_t>(countryDefinition.at(_countryMode).Freq_Legal_Min / 1e6), static_cast<uint32_t>(countryDefinition.at(_countryMode).Freq_Legal_Min / 1e6),
static_cast<uint32_t>(countryDefinition.at(_countryMode).Freq_Legal_Max / 1e6)); static_cast<uint32_t>(countryDefinition.at(_countryMode).Freq_Legal_Max / 1e6));
@ -167,9 +167,9 @@ void HoymilesRadio_CMT::loop()
// Save packet in inverter rx buffer // Save packet in inverter rx buffer
Hoymiles.getMessageOutput()->printf("RX %.2f MHz --> ", getFrequencyFromChannel(f.channel) / 1000000.0); Hoymiles.getMessageOutput()->printf("RX %.2f MHz --> ", getFrequencyFromChannel(f.channel) / 1000000.0);
dumpBuf(f.fragment, f.len, false); dumpBuf(f.fragment, f.len, false);
Hoymiles.getMessageOutput()->printf("| %d dBm\r\n", f.rssi); Hoymiles.getMessageOutput()->printf("| %" PRId8 " dBm\r\n", f.rssi);
inv->addRxFragment(f.fragment, f.len); inv->addRxFragment(f.fragment, f.len, f.rssi);
} else { } else {
Hoymiles.getMessageOutput()->println("Inverter Not found!"); Hoymiles.getMessageOutput()->println("Inverter Not found!");
} }
@ -194,9 +194,9 @@ void HoymilesRadio_CMT::setPALevel(const int8_t paLevel)
} }
if (_radio->setPALevel(paLevel)) { if (_radio->setPALevel(paLevel)) {
Hoymiles.getMessageOutput()->printf("CMT TX power set to %d dBm\r\n", paLevel); Hoymiles.getMessageOutput()->printf("CMT TX power set to %" PRId8 " dBm\r\n", paLevel);
} else { } else {
Hoymiles.getMessageOutput()->printf("CMT TX power %d dBm is not defined! (min: -10 dBm, max: 20 dBm)\r\n", paLevel); Hoymiles.getMessageOutput()->printf("CMT TX power %" PRId8 " dBm is not defined! (min: -10 dBm, max: 20 dBm)\r\n", paLevel);
} }
} }

View File

@ -76,11 +76,11 @@ void HoymilesRadio_NRF::loop()
if (nullptr != inv) { if (nullptr != inv) {
// Save packet in inverter rx buffer // Save packet in inverter rx buffer
Hoymiles.getMessageOutput()->printf("RX Channel: %d --> ", f.channel); Hoymiles.getMessageOutput()->printf("RX Channel: %" PRId8 " --> ", f.channel);
dumpBuf(f.fragment, f.len, false); dumpBuf(f.fragment, f.len, false);
Hoymiles.getMessageOutput()->printf("| %d dBm\r\n", f.rssi); Hoymiles.getMessageOutput()->printf("| %" PRId8 " dBm\r\n", f.rssi);
inv->addRxFragment(f.fragment, f.len); inv->addRxFragment(f.fragment, f.len, f.rssi);
} else { } else {
Hoymiles.getMessageOutput()->println("Inverter Not found!"); Hoymiles.getMessageOutput()->println("Inverter Not found!");
} }
@ -183,7 +183,7 @@ void HoymilesRadio_NRF::sendEsbPacket(CommandAbstract& cmd)
openWritingPipe(s); openWritingPipe(s);
_radio->setRetries(3, 15); _radio->setRetries(3, 15);
Hoymiles.getMessageOutput()->printf("TX %s Channel: %d --> ", Hoymiles.getMessageOutput()->printf("TX %s Channel: %" PRId8 " --> ",
cmd.getCommandName().c_str(), _radio->getChannel()); cmd.getCommandName().c_str(), _radio->getChannel());
cmd.dumpDataPayload(Hoymiles.getMessageOutput()); cmd.dumpDataPayload(Hoymiles.getMessageOutput());
_radio->write(cmd.getDataPayload(), cmd.getDataSize()); _radio->write(cmd.getDataPayload(), cmd.getDataSize());

View File

@ -85,13 +85,13 @@ bool ActivePowerControlCommand::handleResponse(const fragment_t fragment[], cons
float ActivePowerControlCommand::getLimit() const float ActivePowerControlCommand::getLimit() const
{ {
const float l = (((uint16_t)_payload[12] << 8) | _payload[13]); const float l = (static_cast<uint16_t>(_payload[12]) << 8) | _payload[13];
return l / 10; return l / 10;
} }
PowerLimitControlType ActivePowerControlCommand::getType() PowerLimitControlType ActivePowerControlCommand::getType()
{ {
return (PowerLimitControlType)(((uint16_t)_payload[14] << 8) | _payload[15]); return (PowerLimitControlType)((static_cast<uint16_t>(_payload[14]) << 8) | _payload[15]);
} }
void ActivePowerControlCommand::gotTimeout() void ActivePowerControlCommand::gotTimeout()

View File

@ -35,8 +35,8 @@ DevControlCommand::DevControlCommand(InverterAbstract* inv, const uint64_t route
void DevControlCommand::udpateCRC(const uint8_t len) void DevControlCommand::udpateCRC(const uint8_t len)
{ {
const uint16_t crc = crc16(&_payload[10], len); const uint16_t crc = crc16(&_payload[10], len);
_payload[10 + len] = (uint8_t)(crc >> 8); _payload[10 + len] = static_cast<uint8_t>(crc >> 8);
_payload[10 + len + 1] = (uint8_t)(crc); _payload[10 + len + 1] = static_cast<uint8_t>(crc);
} }
bool DevControlCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id) bool DevControlCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id)

View File

@ -63,10 +63,10 @@ uint8_t MultiDataCommand::getDataType() const
void MultiDataCommand::setTime(const time_t time) void MultiDataCommand::setTime(const time_t time)
{ {
_payload[12] = (uint8_t)(time >> 24); _payload[12] = static_cast<uint8_t>(time >> 24);
_payload[13] = (uint8_t)(time >> 16); _payload[13] = static_cast<uint8_t>(time >> 16);
_payload[14] = (uint8_t)(time >> 8); _payload[14] = static_cast<uint8_t>(time >> 8);
_payload[15] = (uint8_t)(time); _payload[15] = static_cast<uint8_t>(time);
udpateCRC(); udpateCRC();
} }
@ -112,8 +112,8 @@ bool MultiDataCommand::handleResponse(const fragment_t fragment[], const uint8_t
void MultiDataCommand::udpateCRC() void MultiDataCommand::udpateCRC()
{ {
const uint16_t crc = crc16(&_payload[10], 14); // From data_type till password const uint16_t crc = crc16(&_payload[10], 14); // From data_type till password
_payload[24] = (uint8_t)(crc >> 8); _payload[24] = static_cast<uint8_t>(crc >> 8);
_payload[25] = (uint8_t)(crc); _payload[25] = static_cast<uint8_t>(crc);
} }
uint8_t MultiDataCommand::getTotalFragmentSize(const fragment_t fragment[], const uint8_t max_fragment_id) uint8_t MultiDataCommand::getTotalFragmentSize(const fragment_t fragment[], const uint8_t max_fragment_id)

View File

@ -48,7 +48,7 @@ bool RealTimeRunDataCommand::handleResponse(const fragment_t fragment[], const u
const uint8_t fragmentsSize = getTotalFragmentSize(fragment, max_fragment_id); const uint8_t fragmentsSize = getTotalFragmentSize(fragment, max_fragment_id);
const uint8_t expectedSize = _inv->Statistics()->getExpectedByteCount(); const uint8_t expectedSize = _inv->Statistics()->getExpectedByteCount();
if (fragmentsSize < expectedSize) { if (fragmentsSize < expectedSize) {
Hoymiles.getMessageOutput()->printf("ERROR in %s: Received fragment size: %d, min expected size: %d\r\n", Hoymiles.getMessageOutput()->printf("ERROR in %s: Received fragment size: %" PRId8 ", min expected size: %" PRId8 "\r\n",
getCommandName().c_str(), fragmentsSize, expectedSize); getCommandName().c_str(), fragmentsSize, expectedSize);
return false; return false;

View File

@ -48,7 +48,7 @@ bool SystemConfigParaCommand::handleResponse(const fragment_t fragment[], const
const uint8_t fragmentsSize = getTotalFragmentSize(fragment, max_fragment_id); const uint8_t fragmentsSize = getTotalFragmentSize(fragment, max_fragment_id);
const uint8_t expectedSize = _inv->SystemConfigPara()->getExpectedByteCount(); const uint8_t expectedSize = _inv->SystemConfigPara()->getExpectedByteCount();
if (fragmentsSize < expectedSize) { if (fragmentsSize < expectedSize) {
Hoymiles.getMessageOutput()->printf("ERROR in %s: Received fragment size: %d, min expected size: %d\r\n", Hoymiles.getMessageOutput()->printf("ERROR in %s: Received fragment size: %" PRId8 ", min expected size: %" PRId8 "\r\n",
getCommandName().c_str(), fragmentsSize, expectedSize); getCommandName().c_str(), fragmentsSize, expectedSize);
return false; return false;

View File

@ -36,10 +36,10 @@ bool HM_1CH::isValidSerial(const uint64_t serial)
// serial >= 0x112100000000 && serial <= 0x1121ffffffff // serial >= 0x112100000000 && serial <= 0x1121ffffffff
uint8_t preId[2]; uint8_t preId[2];
preId[0] = (uint8_t)(serial >> 40); preId[0] = static_cast<uint8_t>(serial >> 40);
preId[1] = (uint8_t)(serial >> 32); preId[1] = static_cast<uint8_t>(serial >> 32);
if ((uint8_t)(((((uint16_t)preId[0] << 8) | preId[1]) >> 4) & 0xff) == 0x12) { if (static_cast<uint8_t>((((static_cast<uint16_t>(preId[0]) << 8) | preId[1]) >> 4) & 0xff) == 0x12) {
return true; return true;
} }

View File

@ -44,10 +44,10 @@ bool HM_2CH::isValidSerial(const uint64_t serial)
// serial >= 0x114100000000 && serial <= 0x1141ffffffff // serial >= 0x114100000000 && serial <= 0x1141ffffffff
uint8_t preId[2]; uint8_t preId[2];
preId[0] = (uint8_t)(serial >> 40); preId[0] = static_cast<uint8_t>(serial >> 40);
preId[1] = (uint8_t)(serial >> 32); preId[1] = static_cast<uint8_t>(serial >> 32);
if ((uint8_t)(((((uint16_t)preId[0] << 8) | preId[1]) >> 4) & 0xff) == 0x14) { if (static_cast<uint8_t>((((static_cast<uint16_t>(preId[0]) << 8) | preId[1]) >> 4) & 0xff) == 0x14) {
return true; return true;
} }

View File

@ -57,10 +57,10 @@ bool HM_4CH::isValidSerial(const uint64_t serial)
// serial >= 0x116100000000 && serial <= 0x1161ffffffff // serial >= 0x116100000000 && serial <= 0x1161ffffffff
uint8_t preId[2]; uint8_t preId[2];
preId[0] = (uint8_t)(serial >> 40); preId[0] = static_cast<uint8_t>(serial >> 40);
preId[1] = (uint8_t)(serial >> 32); preId[1] = static_cast<uint8_t>(serial >> 32);
if ((uint8_t)(((((uint16_t)preId[0] << 8) | preId[1]) >> 4) & 0xff) == 0x16) { if (static_cast<uint8_t>((((static_cast<uint16_t>(preId[0]) << 8) | preId[1]) >> 4) & 0xff) == 0x16) {
return true; return true;
} }

View File

@ -50,13 +50,13 @@ bool HM_Abstract::sendAlarmLogRequest(const bool force)
if (!force) { if (!force) {
if (Statistics()->hasChannelFieldValue(TYPE_INV, CH0, FLD_EVT_LOG)) { if (Statistics()->hasChannelFieldValue(TYPE_INV, CH0, FLD_EVT_LOG)) {
if ((uint8_t)Statistics()->getChannelFieldValue(TYPE_INV, CH0, FLD_EVT_LOG) == _lastAlarmLogCnt) { if (static_cast<uint8_t>(Statistics()->getChannelFieldValue(TYPE_INV, CH0, FLD_EVT_LOG) == _lastAlarmLogCnt)) {
return false; return false;
} }
} }
} }
_lastAlarmLogCnt = (uint8_t)Statistics()->getChannelFieldValue(TYPE_INV, CH0, FLD_EVT_LOG); _lastAlarmLogCnt = static_cast<uint8_t>(Statistics()->getChannelFieldValue(TYPE_INV, CH0, FLD_EVT_LOG));
time_t now; time_t now;
time(&now); time(&now);

View File

@ -14,8 +14,8 @@ InverterAbstract::InverterAbstract(HoymilesRadio* radio, const uint64_t serial)
char serial_buff[sizeof(uint64_t) * 8 + 1]; char serial_buff[sizeof(uint64_t) * 8 + 1];
snprintf(serial_buff, sizeof(serial_buff), "%0x%08x", snprintf(serial_buff, sizeof(serial_buff), "%0x%08x",
((uint32_t)((serial >> 32) & 0xFFFFFFFF)), static_cast<uint32_t>((serial >> 32) & 0xFFFFFFFF),
((uint32_t)(serial & 0xFFFFFFFF))); static_cast<uint32_t>(serial & 0xFFFFFFFF));
_serialString = serial_buff; _serialString = serial_buff;
_alarmLogParser.reset(new AlarmLogParser()); _alarmLogParser.reset(new AlarmLogParser());
@ -137,6 +137,11 @@ bool InverterAbstract::getClearEventlogOnMidnight() const
return _clearEventlogOnMidnight; return _clearEventlogOnMidnight;
} }
int8_t InverterAbstract::getLastRssi() const
{
return _lastRssi;
}
bool InverterAbstract::sendChangeChannelRequest() bool InverterAbstract::sendChangeChannelRequest()
{ {
return false; return false;
@ -185,8 +190,10 @@ void InverterAbstract::clearRxFragmentBuffer()
_rxFragmentRetransmitCnt = 0; _rxFragmentRetransmitCnt = 0;
} }
void InverterAbstract::addRxFragment(const uint8_t fragment[], const uint8_t len) void InverterAbstract::addRxFragment(const uint8_t fragment[], const uint8_t len, const int8_t rssi)
{ {
_lastRssi = rssi;
if (len < 11) { if (len < 11) {
Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) fragment too short\r\n", __FILE__, __LINE__); Hoymiles.getMessageOutput()->printf("FATAL: (%s, %d) fragment too short\r\n", __FILE__, __LINE__);
return; return;
@ -208,7 +215,7 @@ void InverterAbstract::addRxFragment(const uint8_t fragment[], const uint8_t len
} }
if (fragmentId >= MAX_RF_FRAGMENT_COUNT) { if (fragmentId >= MAX_RF_FRAGMENT_COUNT) {
Hoymiles.getMessageOutput()->printf("ERROR: fragment id %d is too large for buffer and ignored\r\n", fragmentId); Hoymiles.getMessageOutput()->printf("ERROR: fragment id %" PRId8 " is too large for buffer and ignored\r\n", fragmentId);
return; return;
} }

View File

@ -61,8 +61,10 @@ public:
void setClearEventlogOnMidnight(const bool enabled); void setClearEventlogOnMidnight(const bool enabled);
bool getClearEventlogOnMidnight() const; bool getClearEventlogOnMidnight() const;
int8_t getLastRssi() const;
void clearRxFragmentBuffer(); void clearRxFragmentBuffer();
void addRxFragment(const uint8_t fragment[], const uint8_t len); void addRxFragment(const uint8_t fragment[], const uint8_t len, const int8_t rssi);
uint8_t verifyAllFragments(CommandAbstract& cmd); uint8_t verifyAllFragments(CommandAbstract& cmd);
void performDailyTask(); void performDailyTask();
@ -131,6 +133,8 @@ private:
bool _zeroYieldDayOnMidnight = false; bool _zeroYieldDayOnMidnight = false;
bool _clearEventlogOnMidnight = false; bool _clearEventlogOnMidnight = false;
int8_t _lastRssi = -127;
std::unique_ptr<AlarmLogParser> _alarmLogParser; std::unique_ptr<AlarmLogParser> _alarmLogParser;
std::unique_ptr<DevInfoParser> _devInfoParser; std::unique_ptr<DevInfoParser> _devInfoParser;
std::unique_ptr<GridProfileParser> _gridProfileParser; std::unique_ptr<GridProfileParser> _gridProfileParser;

View File

@ -243,7 +243,7 @@ void AlarmLogParser::getLogEntry(const uint8_t entryId, AlarmLogEntry_t& entry,
HOY_SEMAPHORE_TAKE(); HOY_SEMAPHORE_TAKE();
const uint32_t wcode = (uint16_t)_payloadAlarmLog[entryStartOffset] << 8 | _payloadAlarmLog[entryStartOffset + 1]; const uint32_t wcode = static_cast<uint16_t>(_payloadAlarmLog[entryStartOffset]) << 8 | _payloadAlarmLog[entryStartOffset + 1];
uint32_t startTimeOffset = 0; uint32_t startTimeOffset = 0;
if (((wcode >> 13) & 0x01) == 1) { if (((wcode >> 13) & 0x01) == 1) {
startTimeOffset = 12 * 60 * 60; startTimeOffset = 12 * 60 * 60;
@ -255,8 +255,8 @@ void AlarmLogParser::getLogEntry(const uint8_t entryId, AlarmLogEntry_t& entry,
} }
entry.MessageId = _payloadAlarmLog[entryStartOffset + 1]; entry.MessageId = _payloadAlarmLog[entryStartOffset + 1];
entry.StartTime = (((uint16_t)_payloadAlarmLog[entryStartOffset + 4] << 8) | ((uint16_t)_payloadAlarmLog[entryStartOffset + 5])) + startTimeOffset + timezoneOffset; entry.StartTime = ((static_cast<uint16_t>(_payloadAlarmLog[entryStartOffset + 4]) << 8) | static_cast<uint16_t>(_payloadAlarmLog[entryStartOffset + 5])) + startTimeOffset + timezoneOffset;
entry.EndTime = ((uint16_t)_payloadAlarmLog[entryStartOffset + 6] << 8) | ((uint16_t)_payloadAlarmLog[entryStartOffset + 7]); entry.EndTime = (static_cast<uint16_t>(_payloadAlarmLog[entryStartOffset + 6]) << 8) | static_cast<uint16_t>(_payloadAlarmLog[entryStartOffset + 7]);
HOY_SEMAPHORE_GIVE(); HOY_SEMAPHORE_GIVE();

View File

@ -61,6 +61,7 @@ const devInfo_t devInfo[] = {
{ { 0x10, 0x10, 0x71, ALL }, 500, "HMS-500-1T" }, // 02 { { 0x10, 0x10, 0x71, ALL }, 500, "HMS-500-1T" }, // 02
{ { 0x10, 0x20, 0x71, ALL }, 500, "HMS-500-1T v2" }, // 02 { { 0x10, 0x20, 0x71, ALL }, 500, "HMS-500-1T v2" }, // 02
{ { 0x10, 0x21, 0x11, ALL }, 600, "HMS-600-2T" }, // 01 { { 0x10, 0x21, 0x11, ALL }, 600, "HMS-600-2T" }, // 01
{ { 0x10, 0x21, 0x21, ALL }, 700, "HMS-700-2T" }, // 00
{ { 0x10, 0x21, 0x41, ALL }, 800, "HMS-800-2T" }, // 00 { { 0x10, 0x21, 0x41, ALL }, 800, "HMS-800-2T" }, // 00
{ { 0x10, 0x11, 0x41, ALL }, 800, "HMS-800-2T-LV" }, // 00 { { 0x10, 0x11, 0x41, ALL }, 800, "HMS-800-2T-LV" }, // 00
{ { 0x10, 0x11, 0x51, ALL }, 900, "HMS-900-2T" }, // 01 { { 0x10, 0x11, 0x51, ALL }, 900, "HMS-900-2T" }, // 01
@ -149,7 +150,7 @@ void DevInfoParser::setLastUpdateSimple(const uint32_t lastUpdate)
uint16_t DevInfoParser::getFwBuildVersion() const uint16_t DevInfoParser::getFwBuildVersion() const
{ {
HOY_SEMAPHORE_TAKE(); HOY_SEMAPHORE_TAKE();
const uint16_t ret = (((uint16_t)_payloadDevInfoAll[0]) << 8) | _payloadDevInfoAll[1]; const uint16_t ret = (static_cast<uint16_t>(_payloadDevInfoAll[0]) << 8) | _payloadDevInfoAll[1];
HOY_SEMAPHORE_GIVE(); HOY_SEMAPHORE_GIVE();
return ret; return ret;
} }
@ -158,13 +159,13 @@ time_t DevInfoParser::getFwBuildDateTime() const
{ {
struct tm timeinfo = {}; struct tm timeinfo = {};
HOY_SEMAPHORE_TAKE(); HOY_SEMAPHORE_TAKE();
timeinfo.tm_year = ((((uint16_t)_payloadDevInfoAll[2]) << 8) | _payloadDevInfoAll[3]) - 1900; timeinfo.tm_year = ((static_cast<uint16_t>(_payloadDevInfoAll[2]) << 8) | _payloadDevInfoAll[3]) - 1900;
timeinfo.tm_mon = ((((uint16_t)_payloadDevInfoAll[4]) << 8) | _payloadDevInfoAll[5]) / 100 - 1; timeinfo.tm_mon = ((static_cast<uint16_t>(_payloadDevInfoAll[4]) << 8) | _payloadDevInfoAll[5]) / 100 - 1;
timeinfo.tm_mday = ((((uint16_t)_payloadDevInfoAll[4]) << 8) | _payloadDevInfoAll[5]) % 100; timeinfo.tm_mday = ((static_cast<uint16_t>(_payloadDevInfoAll[4]) << 8) | _payloadDevInfoAll[5]) % 100;
timeinfo.tm_hour = ((((uint16_t)_payloadDevInfoAll[6]) << 8) | _payloadDevInfoAll[7]) / 100; timeinfo.tm_hour = ((static_cast<uint16_t>(_payloadDevInfoAll[6]) << 8) | _payloadDevInfoAll[7]) / 100;
timeinfo.tm_min = ((((uint16_t)_payloadDevInfoAll[6]) << 8) | _payloadDevInfoAll[7]) % 100; timeinfo.tm_min = ((static_cast<uint16_t>(_payloadDevInfoAll[6]) << 8) | _payloadDevInfoAll[7]) % 100;
HOY_SEMAPHORE_GIVE(); HOY_SEMAPHORE_GIVE();
return timegm(&timeinfo); return timegm(&timeinfo);
@ -181,7 +182,7 @@ String DevInfoParser::getFwBuildDateTimeStr() const
uint16_t DevInfoParser::getFwBootloaderVersion() const uint16_t DevInfoParser::getFwBootloaderVersion() const
{ {
HOY_SEMAPHORE_TAKE(); HOY_SEMAPHORE_TAKE();
const uint16_t ret = (((uint16_t)_payloadDevInfoAll[8]) << 8) | _payloadDevInfoAll[9]; const uint16_t ret = (static_cast<uint16_t>(_payloadDevInfoAll[8]) << 8) | _payloadDevInfoAll[9];
HOY_SEMAPHORE_GIVE(); HOY_SEMAPHORE_GIVE();
return ret; return ret;
} }
@ -189,11 +190,11 @@ uint16_t DevInfoParser::getFwBootloaderVersion() const
uint32_t DevInfoParser::getHwPartNumber() const uint32_t DevInfoParser::getHwPartNumber() const
{ {
HOY_SEMAPHORE_TAKE(); HOY_SEMAPHORE_TAKE();
const uint16_t hwpn_h = (((uint16_t)_payloadDevInfoSimple[2]) << 8) | _payloadDevInfoSimple[3]; const uint16_t hwpn_h = (static_cast<uint16_t>(_payloadDevInfoSimple[2]) << 8) | _payloadDevInfoSimple[3];
const uint16_t hwpn_l = (((uint16_t)_payloadDevInfoSimple[4]) << 8) | _payloadDevInfoSimple[5]; const uint16_t hwpn_l = (static_cast<uint16_t>(_payloadDevInfoSimple[4]) << 8) | _payloadDevInfoSimple[5];
HOY_SEMAPHORE_GIVE(); HOY_SEMAPHORE_GIVE();
return ((uint32_t)hwpn_h << 16) | ((uint32_t)hwpn_l); return (static_cast<uint32_t>(hwpn_h) << 16) | static_cast<uint32_t>(hwpn_l);
} }
String DevInfoParser::getHwVersion() const String DevInfoParser::getHwVersion() const

View File

@ -443,7 +443,7 @@ std::list<GridProfileSection_t> GridProfileParser::getProfile() const
for (uint8_t val_id = 0; val_id < section_size; val_id++) { for (uint8_t val_id = 0; val_id < section_size; val_id++) {
auto itemDefinition = itemDefinitions.at(_profileValues[section_start + val_id].ItemDefinition); auto itemDefinition = itemDefinitions.at(_profileValues[section_start + val_id].ItemDefinition);
float value = (int16_t)((_payloadGridProfile[pos] << 8) | _payloadGridProfile[pos + 1]); float value = static_cast<int16_t>((_payloadGridProfile[pos] << 8) | _payloadGridProfile[pos + 1]);
value /= itemDefinition.Divider; value /= itemDefinition.Divider;
GridProfileItem_t v; GridProfileItem_t v;

View File

@ -45,7 +45,7 @@ void SystemConfigParaParser::appendFragment(const uint8_t offset, const uint8_t*
float SystemConfigParaParser::getLimitPercent() const float SystemConfigParaParser::getLimitPercent() const
{ {
HOY_SEMAPHORE_TAKE(); HOY_SEMAPHORE_TAKE();
const float ret = ((((uint16_t)_payload[2]) << 8) | _payload[3]) / 10.0; const float ret = ((static_cast<uint16_t>(_payload[2]) << 8) | _payload[3]) / 10.0;
HOY_SEMAPHORE_GIVE(); HOY_SEMAPHORE_GIVE();
return ret; return ret;
} }
@ -53,8 +53,8 @@ float SystemConfigParaParser::getLimitPercent() const
void SystemConfigParaParser::setLimitPercent(const float value) void SystemConfigParaParser::setLimitPercent(const float value)
{ {
HOY_SEMAPHORE_TAKE(); HOY_SEMAPHORE_TAKE();
_payload[2] = ((uint16_t)(value * 10)) >> 8; _payload[2] = static_cast<uint16_t>(value * 10) >> 8;
_payload[3] = ((uint16_t)(value * 10)); _payload[3] = static_cast<uint16_t>(value * 10);
HOY_SEMAPHORE_GIVE(); HOY_SEMAPHORE_GIVE();
} }

View File

@ -18,11 +18,19 @@ SpiBus::SpiBus(const std::string& _id, spi_host_device_t _host_device)
.data5_io_num = -1, .data5_io_num = -1,
.data6_io_num = -1, .data6_io_num = -1,
.data7_io_num = -1, .data7_io_num = -1,
.max_transfer_sz = SPI_MAX_DMA_LEN, .max_transfer_sz = 0, // defaults to SPI_MAX_DMA_LEN (=4092) or SOC_SPI_MAXIMUM_BUFFER_SIZE (=64)
.flags = 0, .flags = 0,
.intr_flags = 0 .intr_flags = 0
}; };
ESP_ERROR_CHECK(spi_bus_initialize(host_device, &bus_config, SPI_DMA_CH_AUTO));
#if !CONFIG_IDF_TARGET_ESP32S2
spi_dma_chan_t dma_channel = SPI_DMA_CH_AUTO;
#else
// DMA for SPI3 on ESP32-S2 is shared with ADC/DAC, so we cannot use it here
spi_dma_chan_t dma_channel = (host_device != SPI3_HOST ? SPI_DMA_CH_AUTO : SPI_DMA_DISABLED);
#endif
ESP_ERROR_CHECK(spi_bus_initialize(host_device, &bus_config, dma_channel));
} }
SpiBus::~SpiBus() SpiBus::~SpiBus()

View File

@ -130,4 +130,4 @@ def esp32_create_combined_bin(source, target, env):
esptool.main(cmd) esptool.main(cmd)
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin) env.AddPostAction("buildprog", esp32_create_combined_bin)

View File

@ -23,20 +23,20 @@ def is_tool(name):
return which(name) is not None return which(name) is not None
def replaceInFile(in_file, out_file, text, subs, flags=0): def replaceInFile(in_file, out_file, text, subs, flags=0):
""" """Function for replacing content for the given file."""
Function for replacing content for the given file
Taken from https://www.studytonight.com/python-howtos/search-and-replace-a-text-in-a-file-in-python
"""
if os.path.exists(in_file): if os.path.exists(in_file):
with open(in_file, "rb") as infile:
with open(out_file, "wb") as outfile:
# read the file contents # read the file contents
with open(in_file, "r", encoding="utf-8") as infile:
file_contents = infile.read() file_contents = infile.read()
# do replacement
text_pattern = re.compile(re.escape(text), flags) text_pattern = re.compile(re.escape(text), flags)
file_contents = text_pattern.sub(subs, file_contents.decode('utf-8')) file_contents = text_pattern.sub(subs, file_contents)
outfile.seek(0)
outfile.truncate() # write the result
outfile.write(file_contents.encode()) with open(out_file, "w", encoding="utf-8") as outfile:
outfile.write(file_contents)
def main(): def main():
if (env.GetProjectOption('custom_patches', '') == ''): if (env.GetProjectOption('custom_patches', '') == ''):

View File

@ -41,11 +41,11 @@ build_unflags =
-std=gnu++11 -std=gnu++11
lib_deps = lib_deps =
mathieucarbou/ESPAsyncWebServer @ 3.3.1 mathieucarbou/ESPAsyncWebServer @ 3.3.22
bblanchon/ArduinoJson @ 7.2.0 bblanchon/ArduinoJson @ 7.2.0
https://github.com/bertmelis/espMqttClient.git#v1.7.0 https://github.com/bertmelis/espMqttClient.git#v1.7.0
nrf24/RF24 @ 1.4.9 nrf24/RF24 @ 1.4.10
olikraus/U8g2 @ 2.35.30 olikraus/U8g2 @ 2.36.2
buelowp/sunset @ 1.1.7 buelowp/sunset @ 1.1.7
arkhipenko/TaskScheduler @ 3.8.5 arkhipenko/TaskScheduler @ 3.8.5
@ -254,7 +254,7 @@ build_flags = ${env.build_flags}
-DARDUINO_USB_MODE=1 -DARDUINO_USB_MODE=1
-DARDUINO_USB_CDC_ON_BOOT=1 -DARDUINO_USB_CDC_ON_BOOT=1
[env:opendtufusionv2_shield] [env:opendtufusionv2_poe]
board = esp32-s3-devkitc-1 board = esp32-s3-devkitc-1
upload_protocol = esp-builtin upload_protocol = esp-builtin
debug_tool = esp-builtin debug_tool = esp-builtin

View File

@ -13,8 +13,17 @@
CONFIG_T config; CONFIG_T config;
void ConfigurationClass::init() static std::condition_variable sWriterCv;
static std::mutex sWriterMutex;
static unsigned sWriterCount = 0;
void ConfigurationClass::init(Scheduler& scheduler)
{ {
scheduler.addTask(_loopTask);
_loopTask.setCallback(std::bind(&ConfigurationClass::loop, this));
_loopTask.setIterations(TASK_FOREVER);
_loopTask.enable();
memset(&config, 0x0, sizeof(config)); memset(&config, 0x0, sizeof(config));
} }
@ -107,7 +116,7 @@ bool ConfigurationClass::write()
display["screensaver"] = config.Display.ScreenSaver; display["screensaver"] = config.Display.ScreenSaver;
display["rotation"] = config.Display.Rotation; display["rotation"] = config.Display.Rotation;
display["contrast"] = config.Display.Contrast; display["contrast"] = config.Display.Contrast;
display["language"] = config.Display.Language; display["locale"] = config.Display.Locale;
display["diagram_duration"] = config.Display.Diagram.Duration; display["diagram_duration"] = config.Display.Diagram.Duration;
display["diagram_mode"] = config.Display.Diagram.Mode; display["diagram_mode"] = config.Display.Diagram.Mode;
@ -159,6 +168,7 @@ bool ConfigurationClass::write()
bool ConfigurationClass::read() bool ConfigurationClass::read()
{ {
File f = LittleFS.open(CONFIG_FILENAME, "r", false); File f = LittleFS.open(CONFIG_FILENAME, "r", false);
Utils::skipBom(f);
JsonDocument doc; JsonDocument doc;
@ -282,7 +292,7 @@ bool ConfigurationClass::read()
config.Display.ScreenSaver = display["screensaver"] | DISPLAY_SCREENSAVER; config.Display.ScreenSaver = display["screensaver"] | DISPLAY_SCREENSAVER;
config.Display.Rotation = display["rotation"] | DISPLAY_ROTATION; config.Display.Rotation = display["rotation"] | DISPLAY_ROTATION;
config.Display.Contrast = display["contrast"] | DISPLAY_CONTRAST; config.Display.Contrast = display["contrast"] | DISPLAY_CONTRAST;
config.Display.Language = display["language"] | DISPLAY_LANGUAGE; strlcpy(config.Display.Locale, display["locale"] | DISPLAY_LOCALE, sizeof(config.Display.Locale));
config.Display.Diagram.Duration = display["diagram_duration"] | DISPLAY_DIAGRAM_DURATION; config.Display.Diagram.Duration = display["diagram_duration"] | DISPLAY_DIAGRAM_DURATION;
config.Display.Diagram.Mode = display["diagram_mode"] | DISPLAY_DIAGRAM_MODE; config.Display.Diagram.Mode = display["diagram_mode"] | DISPLAY_DIAGRAM_MODE;
@ -318,6 +328,20 @@ bool ConfigurationClass::read()
} }
f.close(); f.close();
// Check for default DTU serial
MessageOutput.print("Check for default DTU serial... ");
if (config.Dtu.Serial == DTU_SERIAL) {
MessageOutput.print("generate serial based on ESP chip id: ");
const uint64_t dtuId = Utils::generateDtuSerial();
MessageOutput.printf("%0" PRIx32 "%08" PRIx32 "... ",
static_cast<uint32_t>((dtuId >> 32) & 0xFFFFFFFF),
static_cast<uint32_t>(dtuId & 0xFFFFFFFF));
config.Dtu.Serial = dtuId;
write();
}
MessageOutput.println("done");
return true; return true;
} }
@ -383,6 +407,22 @@ void ConfigurationClass::migrate()
} }
} }
if (config.Cfg.Version < 0x00011d00) {
JsonObject device = doc["device"];
JsonObject display = device["display"];
switch (display["language"] | 0U) {
case 0U:
strlcpy(config.Display.Locale, "en", sizeof(config.Display.Locale));
break;
case 1U:
strlcpy(config.Display.Locale, "de", sizeof(config.Display.Locale));
break;
case 2U:
strlcpy(config.Display.Locale, "fr", sizeof(config.Display.Locale));
break;
}
}
f.close(); f.close();
config.Cfg.Version = CONFIG_VERSION; config.Cfg.Version = CONFIG_VERSION;
@ -390,11 +430,16 @@ void ConfigurationClass::migrate()
read(); read();
} }
CONFIG_T& ConfigurationClass::get() CONFIG_T const& ConfigurationClass::get()
{ {
return config; return config;
} }
ConfigurationClass::WriteGuard ConfigurationClass::getWriteGuard()
{
return WriteGuard();
}
INVERTER_CONFIG_T* ConfigurationClass::getFreeInverterSlot() INVERTER_CONFIG_T* ConfigurationClass::getFreeInverterSlot()
{ {
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
@ -439,4 +484,30 @@ void ConfigurationClass::deleteInverterById(const uint8_t id)
} }
} }
void ConfigurationClass::loop()
{
std::unique_lock<std::mutex> lock(sWriterMutex);
if (sWriterCount == 0) { return; }
sWriterCv.notify_all();
sWriterCv.wait(lock, [] { return sWriterCount == 0; });
}
CONFIG_T& ConfigurationClass::WriteGuard::getConfig()
{
return config;
}
ConfigurationClass::WriteGuard::WriteGuard()
: _lock(sWriterMutex)
{
sWriterCount++;
sWriterCv.wait(_lock);
}
ConfigurationClass::WriteGuard::~WriteGuard() {
sWriterCount--;
if (sWriterCount == 0) { sWriterCv.notify_all(); }
}
ConfigurationClass Configuration; ConfigurationClass Configuration;

View File

@ -4,6 +4,7 @@
*/ */
#include "Display_Graphic.h" #include "Display_Graphic.h"
#include "Datastore.h" #include "Datastore.h"
#include "I18n.h"
#include <NetworkSettings.h> #include <NetworkSettings.h>
#include <map> #include <map>
#include <time.h> #include <time.h>
@ -16,18 +17,11 @@ std::map<DisplayType_t, std::function<U8G2*(uint8_t, uint8_t, uint8_t, uint8_t)>
{ DisplayType_t::ST7567_GM12864I_59N, [](uint8_t reset, uint8_t clock, uint8_t data, uint8_t cs) { return new U8G2_ST7567_ENH_DG128064I_F_HW_I2C(U8G2_R0, reset, clock, data); } }, { DisplayType_t::ST7567_GM12864I_59N, [](uint8_t reset, uint8_t clock, uint8_t data, uint8_t cs) { return new U8G2_ST7567_ENH_DG128064I_F_HW_I2C(U8G2_R0, reset, clock, data); } },
}; };
// Language defintion, respect order in languages[] and translation lists // Language defintion, respect order in translation lists
#define I18N_LOCALE_EN 0 #define I18N_LOCALE_EN 0
#define I18N_LOCALE_DE 1 #define I18N_LOCALE_DE 1
#define I18N_LOCALE_FR 2 #define I18N_LOCALE_FR 2
// Languages supported. Note: the order is important and must match locale_translations.h
const uint8_t languages[] = {
I18N_LOCALE_EN,
I18N_LOCALE_DE,
I18N_LOCALE_FR
};
static const char* const i18n_offline[] = { "Offline", "Offline", "Offline" }; static const char* const i18n_offline[] = { "Offline", "Offline", "Offline" };
static const char* const i18n_current_power_w[] = { "%.0f W", "%.0f W", "%.0f W" }; static const char* const i18n_current_power_w[] = { "%.0f W", "%.0f W", "%.0f W" };
@ -166,9 +160,34 @@ void DisplayGraphicClass::setOrientation(const uint8_t rotation)
calcLineHeights(); calcLineHeights();
} }
void DisplayGraphicClass::setLanguage(const uint8_t language) void DisplayGraphicClass::setLocale(const String& locale)
{ {
_display_language = language < sizeof(languages) / sizeof(languages[0]) ? language : DISPLAY_LANGUAGE; _display_language = locale;
uint8_t idx = I18N_LOCALE_EN;
if (locale == "de") {
idx = I18N_LOCALE_DE;
} else if (locale == "fr") {
idx = I18N_LOCALE_FR;
}
_i18n_date_format = i18n_date_format[idx];
_i18n_offline = i18n_offline[idx];
_i18n_current_power_w = i18n_current_power_w[idx];
_i18n_current_power_kw = i18n_current_power_kw[idx];
_i18n_yield_today_wh = i18n_yield_today_wh[idx];
_i18n_yield_today_kwh = i18n_yield_today_kwh[idx];
_i18n_yield_total_kwh = i18n_yield_total_kwh[idx];
_i18n_yield_total_mwh = i18n_yield_total_mwh[idx];
I18n.readDisplayStrings(locale,
_i18n_date_format,
_i18n_offline,
_i18n_current_power_w,
_i18n_current_power_kw,
_i18n_yield_today_wh,
_i18n_yield_today_kwh,
_i18n_yield_total_kwh,
_i18n_yield_total_mwh);
} }
void DisplayGraphicClass::setDiagramMode(DiagramMode_t mode) void DisplayGraphicClass::setDiagramMode(DiagramMode_t mode)
@ -225,9 +244,9 @@ void DisplayGraphicClass::loop()
if (showText) { if (showText) {
const float watts = Datastore.getTotalAcPowerEnabled(); const float watts = Datastore.getTotalAcPowerEnabled();
if (watts > 999) { if (watts > 999) {
snprintf(_fmtText, sizeof(_fmtText), i18n_current_power_kw[_display_language], watts / 1000); snprintf(_fmtText, sizeof(_fmtText), _i18n_current_power_kw.c_str(), watts / 1000);
} else { } else {
snprintf(_fmtText, sizeof(_fmtText), i18n_current_power_w[_display_language], watts); snprintf(_fmtText, sizeof(_fmtText), _i18n_current_power_w.c_str(), watts);
} }
printText(_fmtText, 0); printText(_fmtText, 0);
} }
@ -237,7 +256,7 @@ void DisplayGraphicClass::loop()
//=====> Offline =========== //=====> Offline ===========
else { else {
printText(i18n_offline[_display_language], 0); printText(_i18n_offline.c_str(), 0);
// check if it's time to enter power saving mode // check if it's time to enter power saving mode
if (millis() - _previousMillis >= (_interval * 2)) { if (millis() - _previousMillis >= (_interval * 2)) {
displayPowerSave = enablePowerSafe; displayPowerSave = enablePowerSafe;
@ -249,16 +268,16 @@ void DisplayGraphicClass::loop()
// Daily production // Daily production
float wattsToday = Datastore.getTotalAcYieldDayEnabled(); float wattsToday = Datastore.getTotalAcYieldDayEnabled();
if (wattsToday >= 10000) { if (wattsToday >= 10000) {
snprintf(_fmtText, sizeof(_fmtText), i18n_yield_today_kwh[_display_language], wattsToday / 1000); snprintf(_fmtText, sizeof(_fmtText), _i18n_yield_today_kwh.c_str(), wattsToday / 1000);
} else { } else {
snprintf(_fmtText, sizeof(_fmtText), i18n_yield_today_wh[_display_language], wattsToday); snprintf(_fmtText, sizeof(_fmtText), _i18n_yield_today_wh.c_str(), wattsToday);
} }
printText(_fmtText, 1); printText(_fmtText, 1);
// Total production // Total production
const float wattsTotal = Datastore.getTotalAcYieldTotalEnabled(); const float wattsTotal = Datastore.getTotalAcYieldTotalEnabled();
auto const format = (wattsTotal >= 1000) ? i18n_yield_total_mwh : i18n_yield_total_kwh; auto const format = (wattsTotal >= 1000) ? _i18n_yield_total_mwh : _i18n_yield_total_kwh;
snprintf(_fmtText, sizeof(_fmtText), format[_display_language], wattsTotal); snprintf(_fmtText, sizeof(_fmtText), format.c_str(), wattsTotal);
printText(_fmtText, 2); printText(_fmtText, 2);
//=====> IP or Date-Time ======== //=====> IP or Date-Time ========
@ -268,7 +287,7 @@ void DisplayGraphicClass::loop()
} else { } else {
// Get current time // Get current time
time_t now = time(nullptr); time_t now = time(nullptr);
strftime(_fmtText, sizeof(_fmtText), i18n_date_format[_display_language], localtime(&now)); strftime(_fmtText, sizeof(_fmtText), _i18n_date_format.c_str(), localtime(&now));
printText(_fmtText, 3); printText(_fmtText, 3);
} }
} }

View File

@ -87,7 +87,7 @@ void DisplayGraphicDiagramClass::redraw(uint8_t screenSaverOffsetX, uint8_t xPos
if (maxWatts > 999) { if (maxWatts > 999) {
snprintf(fmtText, sizeof(fmtText), "%2.1fkW", maxWatts / 1000); snprintf(fmtText, sizeof(fmtText), "%2.1fkW", maxWatts / 1000);
} else { } else {
snprintf(fmtText, sizeof(fmtText), "%dW", static_cast<uint16_t>(maxWatts)); snprintf(fmtText, sizeof(fmtText), "%" PRId16 "W", static_cast<uint16_t>(maxWatts));
} }
if (isFullscreen) { if (isFullscreen) {

158
src/I18n.cpp Normal file
View File

@ -0,0 +1,158 @@
// 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;
}
String I18nClass::getFilenameByLocale(const String& locale) const
{
auto it = std::find_if(_availLanguages.begin(), _availLanguages.end(), [locale](const LanguageInfo_t& elem) {
return elem.code == locale;
});
if (it != _availLanguages.end()) {
return it->filename;
} else {
return String();
}
}
void I18nClass::readDisplayStrings(
const String& locale,
String& date_format,
String& offline,
String& power_w, String& power_kw,
String& yield_today_wh, String& yield_today_kwh,
String& yield_total_kwh, String& yield_total_mwh)
{
auto filename = getFilenameByLocale(locale);
if (filename == "") {
return;
}
JsonDocument filter;
filter["display"] = true;
File f = LittleFS.open(filename, "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", filename.c_str());
f.close();
return;
}
if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) {
return;
}
auto displayData = doc["display"];
if (displayData["date_format"].as<String>() != "null") {
date_format = displayData["date_format"].as<String>();
}
if (displayData["offline"].as<String>() != "null") {
offline = displayData["offline"].as<String>();
}
if (displayData["power_w"].as<String>() != "null") {
power_w = displayData["power_w"].as<String>();
}
if (displayData["power_kw"].as<String>() != "null") {
power_kw = displayData["power_kw"].as<String>();
}
if (displayData["yield_today_wh"].as<String>() != "null") {
yield_today_wh = displayData["yield_today_wh"].as<String>();
}
if (displayData["yield_today_kwh"].as<String>() != "null") {
yield_today_kwh = displayData["yield_today_kwh"].as<String>();
}
if (displayData["yield_total_kwh"].as<String>() != "null") {
yield_total_kwh = displayData["yield_total_kwh"].as<String>();
}
if (displayData["yield_total_mwh"].as<String>() != "null") {
yield_total_mwh = displayData["yield_total_mwh"].as<String>();
}
f.close();
}
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;
if (lang.code != "" && lang.name != "") {
_availLanguages.push_back(lang);
} else {
MessageOutput.printf("Invalid meta data\r\n");
}
f.close();
}

View File

@ -60,10 +60,10 @@ void InverterSettingsClass::init(Scheduler& scheduler)
for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { for (uint8_t i = 0; i < INV_MAX_COUNT; i++) {
if (config.Inverter[i].Serial > 0) { if (config.Inverter[i].Serial > 0) {
MessageOutput.print(" Adding inverter: "); MessageOutput.printf(" Adding inverter: %0" PRIx32 "%08" PRIx32 " - %s",
MessageOutput.print(config.Inverter[i].Serial, HEX); static_cast<uint32_t>((config.Inverter[i].Serial >> 32) & 0xFFFFFFFF),
MessageOutput.print(" - "); static_cast<uint32_t>(config.Inverter[i].Serial & 0xFFFFFFFF),
MessageOutput.print(config.Inverter[i].Name); config.Inverter[i].Name);
auto inv = Hoymiles.addInverter( auto inv = Hoymiles.addInverter(
config.Inverter[i].Name, config.Inverter[i].Name,
config.Inverter[i].Serial); config.Inverter[i].Serial);

View File

@ -58,45 +58,46 @@ void MqttHandleHassClass::publishConfig()
const CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
// publish DTU sensors // publish DTU sensors
publishDtuSensor("IP", "dtu/ip", "", "mdi:network-outline", DEVICE_CLS_NONE, CATEGORY_DIAGNOSTIC); publishDtuSensor("IP", "dtu/ip", "", "mdi:network-outline", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishDtuSensor("WiFi Signal", "dtu/rssi", "dBm", "", DEVICE_CLS_SIGNAL_STRENGTH, CATEGORY_DIAGNOSTIC); publishDtuSensor("WiFi Signal", "dtu/rssi", "dBm", "", DEVICE_CLS_SIGNAL_STRENGTH, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishDtuSensor("Uptime", "dtu/uptime", "s", "", DEVICE_CLS_DURATION, CATEGORY_DIAGNOSTIC); publishDtuSensor("Uptime", "dtu/uptime", "s", "", DEVICE_CLS_DURATION, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishDtuSensor("Temperature", "dtu/temperature", "°C", "mdi:thermometer", DEVICE_CLS_TEMPERATURE, CATEGORY_DIAGNOSTIC); publishDtuSensor("Temperature", "dtu/temperature", "°C", "", DEVICE_CLS_TEMPERATURE, STATE_CLS_MEASUREMENT, CATEGORY_DIAGNOSTIC);
publishDtuSensor("Heap Size", "dtu/heap/size", "Bytes", "mdi:memory", DEVICE_CLS_NONE, CATEGORY_DIAGNOSTIC); publishDtuSensor("Heap Size", "dtu/heap/size", "Bytes", "mdi:memory", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishDtuSensor("Heap Free", "dtu/heap/free", "Bytes", "mdi:memory", DEVICE_CLS_NONE, CATEGORY_DIAGNOSTIC); publishDtuSensor("Heap Free", "dtu/heap/free", "Bytes", "mdi:memory", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishDtuSensor("Largest Free Heap Block", "dtu/heap/maxalloc", "Bytes", "mdi:memory", DEVICE_CLS_NONE, CATEGORY_DIAGNOSTIC); publishDtuSensor("Largest Free Heap Block", "dtu/heap/maxalloc", "Bytes", "mdi:memory", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishDtuSensor("Lifetime Minimum Free Heap", "dtu/heap/minfree", "Bytes", "mdi:memory", DEVICE_CLS_NONE, CATEGORY_DIAGNOSTIC); publishDtuSensor("Lifetime Minimum Free Heap", "dtu/heap/minfree", "Bytes", "mdi:memory", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishDtuSensor("Yield Total", "ac/yieldtotal", "kWh", "", DEVICE_CLS_ENERGY, CATEGORY_NONE); publishDtuSensor("Yield Total", "ac/yieldtotal", "kWh", "", DEVICE_CLS_ENERGY, STATE_CLS_TOTAL_INCREASING, CATEGORY_NONE);
publishDtuSensor("Yield Day", "ac/yieldday", "Wh", "", DEVICE_CLS_ENERGY, CATEGORY_NONE); publishDtuSensor("Yield Day", "ac/yieldday", "Wh", "", DEVICE_CLS_ENERGY, STATE_CLS_TOTAL_INCREASING, CATEGORY_NONE);
publishDtuSensor("AC Power", "ac/power", "W", "", DEVICE_CLS_PWR, CATEGORY_NONE); publishDtuSensor("AC Power", "ac/power", "W", "", DEVICE_CLS_PWR, STATE_CLS_MEASUREMENT, CATEGORY_NONE);
publishDtuBinarySensor("Status", config.Mqtt.Lwt.Topic, config.Mqtt.Lwt.Value_Online, config.Mqtt.Lwt.Value_Offline, DEVICE_CLS_CONNECTIVITY, CATEGORY_DIAGNOSTIC); publishDtuBinarySensor("Status", config.Mqtt.Lwt.Topic, config.Mqtt.Lwt.Value_Online, config.Mqtt.Lwt.Value_Offline, DEVICE_CLS_CONNECTIVITY, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
// Loop all inverters // Loop all inverters
for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) {
auto inv = Hoymiles.getInverterByPos(i); auto inv = Hoymiles.getInverterByPos(i);
publishInverterButton(inv, "Turn Inverter Off", "cmd/power", "0", "mdi:power-plug-off", DEVICE_CLS_NONE, CATEGORY_CONFIG); publishInverterButton(inv, "Turn Inverter Off", "cmd/power", "0", "mdi:power-plug-off", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_CONFIG);
publishInverterButton(inv, "Turn Inverter On", "cmd/power", "1", "mdi:power-plug", DEVICE_CLS_NONE, CATEGORY_CONFIG); publishInverterButton(inv, "Turn Inverter On", "cmd/power", "1", "mdi:power-plug", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_CONFIG);
publishInverterButton(inv, "Restart Inverter", "cmd/restart", "1", "", DEVICE_CLS_RESTART, CATEGORY_CONFIG); publishInverterButton(inv, "Restart Inverter", "cmd/restart", "1", "", DEVICE_CLS_RESTART, STATE_CLS_NONE, CATEGORY_CONFIG);
publishInverterButton(inv, "Reset Radio Statistics", "cmd/reset_rf_stats", "1", "", DEVICE_CLS_NONE, CATEGORY_CONFIG); publishInverterButton(inv, "Reset Radio Statistics", "cmd/reset_rf_stats", "1", "", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_CONFIG);
publishInverterNumber(inv, "Limit NonPersistent Relative", "status/limit_relative", "cmd/limit_nonpersistent_relative", 0, 100, 0.1, "%", "mdi:speedometer", CATEGORY_CONFIG); publishInverterNumber(inv, "Limit NonPersistent Relative", "status/limit_relative", "cmd/limit_nonpersistent_relative", 0, 100, 0.1, "%", "mdi:speedometer", STATE_CLS_NONE, CATEGORY_CONFIG);
publishInverterNumber(inv, "Limit Persistent Relative", "status/limit_relative", "cmd/limit_persistent_relative", 0, 100, 0.1, "%", "mdi:speedometer", CATEGORY_CONFIG); publishInverterNumber(inv, "Limit Persistent Relative", "status/limit_relative", "cmd/limit_persistent_relative", 0, 100, 0.1, "%", "mdi:speedometer", STATE_CLS_NONE, CATEGORY_CONFIG);
publishInverterNumber(inv, "Limit NonPersistent Absolute", "status/limit_absolute", "cmd/limit_nonpersistent_absolute", 0, MAX_INVERTER_LIMIT, 1, "W", "mdi:speedometer", CATEGORY_CONFIG); publishInverterNumber(inv, "Limit NonPersistent Absolute", "status/limit_absolute", "cmd/limit_nonpersistent_absolute", 0, MAX_INVERTER_LIMIT, 1, "W", "mdi:speedometer", STATE_CLS_NONE, CATEGORY_CONFIG);
publishInverterNumber(inv, "Limit Persistent Absolute", "status/limit_absolute", "cmd/limit_persistent_absolute", 0, MAX_INVERTER_LIMIT, 1, "W", "mdi:speedometer", CATEGORY_CONFIG); publishInverterNumber(inv, "Limit Persistent Absolute", "status/limit_absolute", "cmd/limit_persistent_absolute", 0, MAX_INVERTER_LIMIT, 1, "W", "mdi:speedometer", STATE_CLS_NONE, CATEGORY_CONFIG);
publishInverterBinarySensor(inv, "Reachable", "status/reachable", "1", "0", DEVICE_CLS_CONNECTIVITY, CATEGORY_DIAGNOSTIC); publishInverterBinarySensor(inv, "Reachable", "status/reachable", "1", "0", DEVICE_CLS_CONNECTIVITY, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishInverterBinarySensor(inv, "Producing", "status/producing", "1", "0", DEVICE_CLS_NONE, CATEGORY_NONE); publishInverterBinarySensor(inv, "Producing", "status/producing", "1", "0", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_NONE);
publishInverterSensor(inv, "TX Requests", "radio/tx_request", "", "", DEVICE_CLS_NONE, CATEGORY_DIAGNOSTIC); publishInverterSensor(inv, "TX Requests", "radio/tx_request", "", "", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishInverterSensor(inv, "RX Success", "radio/rx_success", "", "", DEVICE_CLS_NONE, CATEGORY_DIAGNOSTIC); publishInverterSensor(inv, "RX Success", "radio/rx_success", "", "", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishInverterSensor(inv, "RX Fail Receive Nothing", "radio/rx_fail_nothing", "", "", DEVICE_CLS_NONE, CATEGORY_DIAGNOSTIC); publishInverterSensor(inv, "RX Fail Receive Nothing", "radio/rx_fail_nothing", "", "", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishInverterSensor(inv, "RX Fail Receive Partial", "radio/rx_fail_partial", "", "", DEVICE_CLS_NONE, CATEGORY_DIAGNOSTIC); publishInverterSensor(inv, "RX Fail Receive Partial", "radio/rx_fail_partial", "", "", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishInverterSensor(inv, "RX Fail Receive Corrupt", "radio/rx_fail_corrupt", "", "", DEVICE_CLS_NONE, CATEGORY_DIAGNOSTIC); publishInverterSensor(inv, "RX Fail Receive Corrupt", "radio/rx_fail_corrupt", "", "", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishInverterSensor(inv, "TX Re-Request Fragment", "radio/tx_re_request", "", "", DEVICE_CLS_NONE, CATEGORY_DIAGNOSTIC); publishInverterSensor(inv, "TX Re-Request Fragment", "radio/tx_re_request", "", "", DEVICE_CLS_NONE, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
publishInverterSensor(inv, "RSSI", "radio/rssi", "dBm", "", DEVICE_CLS_SIGNAL_STRENGTH, STATE_CLS_NONE, CATEGORY_DIAGNOSTIC);
// Loop all channels // Loop all channels
for (auto& t : inv->Statistics()->getChannelTypes()) { for (auto& t : inv->Statistics()->getChannelTypes()) {
@ -142,7 +143,6 @@ void MqttHandleHassClass::publishInverterField(std::shared_ptr<InverterAbstract>
if (!clear) { if (!clear) {
const String stateTopic = MqttSettings.getPrefix() + MqttHandleInverter.getTopic(inv, type, channel, fieldType.fieldId); const String stateTopic = MqttSettings.getPrefix() + MqttHandleInverter.getTopic(inv, type, channel, fieldType.fieldId);
const char* stateCls = stateClass_name[fieldType.stateClsId];
String name; String name;
if (type != TYPE_DC) { if (type != TYPE_DC) {
@ -155,7 +155,7 @@ void MqttHandleHassClass::publishInverterField(std::shared_ptr<InverterAbstract>
JsonDocument root; JsonDocument root;
createInverterInfo(root, inv); createInverterInfo(root, inv);
addCommonMetadata(root, unit_of_measure, "", fieldType.deviceClsId, CATEGORY_NONE); addCommonMetadata(root, unit_of_measure, "", fieldType.deviceClsId, fieldType.stateClsId, CATEGORY_NONE);
root["name"] = name; root["name"] = name;
root["stat_t"] = stateTopic; root["stat_t"] = stateTopic;
@ -164,9 +164,6 @@ void MqttHandleHassClass::publishInverterField(std::shared_ptr<InverterAbstract>
if (Configuration.get().Mqtt.Hass.Expire) { if (Configuration.get().Mqtt.Hass.Expire) {
root["exp_aft"] = Hoymiles.getNumInverters() * max<uint32_t>(Hoymiles.PollInterval(), Configuration.get().Mqtt.PublishInterval) * inv->getReachableThreshold(); root["exp_aft"] = Hoymiles.getNumInverters() * max<uint32_t>(Hoymiles.PollInterval(), Configuration.get().Mqtt.PublishInterval) * inv->getReachableThreshold();
} }
if (stateCls != 0) {
root["stat_cla"] = stateCls;
}
publish(configTopic, root); publish(configTopic, root);
} else { } else {
@ -174,7 +171,10 @@ void MqttHandleHassClass::publishInverterField(std::shared_ptr<InverterAbstract>
} }
} }
void MqttHandleHassClass::publishInverterButton(std::shared_ptr<InverterAbstract> inv, const String& name, const String& state_topic, const String& payload, const String& icon, const DeviceClassType device_class, const CategoryType category) void MqttHandleHassClass::publishInverterButton(
std::shared_ptr<InverterAbstract> inv, const String& name, const String& state_topic, const String& payload,
const String& icon,
const DeviceClassType device_class, const StateClassType state_class, const CategoryType category)
{ {
const String serial = inv->serialString(); const String serial = inv->serialString();
@ -190,7 +190,7 @@ void MqttHandleHassClass::publishInverterButton(std::shared_ptr<InverterAbstract
JsonDocument root; JsonDocument root;
createInverterInfo(root, inv); createInverterInfo(root, inv);
addCommonMetadata(root, "", icon, device_class, category); addCommonMetadata(root, "", icon, device_class, state_class, category);
root["name"] = name; root["name"] = name;
root["uniq_id"] = serial + "_" + buttonId; root["uniq_id"] = serial + "_" + buttonId;
@ -204,7 +204,8 @@ void MqttHandleHassClass::publishInverterNumber(
std::shared_ptr<InverterAbstract> inv, const String& name, std::shared_ptr<InverterAbstract> inv, const String& name,
const String& stateTopic, const String& command_topic, const String& stateTopic, const String& command_topic,
const int16_t min, const int16_t max, float step, const int16_t min, const int16_t max, float step,
const String& unit_of_measure, const String& icon, const CategoryType category) const String& unit_of_measure, const String& icon,
const StateClassType state_class, const CategoryType category)
{ {
const String serial = inv->serialString(); const String serial = inv->serialString();
@ -221,7 +222,7 @@ void MqttHandleHassClass::publishInverterNumber(
JsonDocument root; JsonDocument root;
createInverterInfo(root, inv); createInverterInfo(root, inv);
addCommonMetadata(root, unit_of_measure, icon, DEVICE_CLS_NONE, category); addCommonMetadata(root, unit_of_measure, icon, DEVICE_CLS_NONE, state_class, category);
root["name"] = name; root["name"] = name;
root["uniq_id"] = serial + "_" + buttonId; root["uniq_id"] = serial + "_" + buttonId;
@ -307,7 +308,10 @@ void MqttHandleHassClass::publish(const String& subtopic, const JsonDocument& do
publish(subtopic, buffer); publish(subtopic, buffer);
} }
void MqttHandleHassClass::addCommonMetadata(JsonDocument& doc, const String& unit_of_measure, const String& icon, const DeviceClassType device_class, const CategoryType category) void MqttHandleHassClass::addCommonMetadata(
JsonDocument& doc,
const String& unit_of_measure, const String& icon,
const DeviceClassType device_class, const StateClassType state_class, const CategoryType category)
{ {
if (unit_of_measure != "") { if (unit_of_measure != "") {
doc["unit_of_meas"] = unit_of_measure; doc["unit_of_meas"] = unit_of_measure;
@ -318,12 +322,18 @@ void MqttHandleHassClass::addCommonMetadata(JsonDocument& doc, const String& uni
if (device_class != DEVICE_CLS_NONE) { if (device_class != DEVICE_CLS_NONE) {
doc["dev_cla"] = deviceClass_name[device_class]; doc["dev_cla"] = deviceClass_name[device_class];
} }
if (state_class != STATE_CLS_NONE) {
doc["stat_cla"] = stateClass_name[state_class];;
}
if (category != CATEGORY_NONE) { if (category != CATEGORY_NONE) {
doc["ent_cat"] = category_name[category]; doc["ent_cat"] = category_name[category];
} }
} }
void MqttHandleHassClass::publishBinarySensor(JsonDocument& doc, const String& root_device, const String& unique_id_prefix, const String& name, const String& state_topic, const String& payload_on, const String& payload_off, const DeviceClassType device_class, const CategoryType category) void MqttHandleHassClass::publishBinarySensor(
JsonDocument& doc,
const String& root_device, const String& unique_id_prefix, const String& name, const String& state_topic, const String& payload_on, const String& payload_off,
const DeviceClassType device_class, const StateClassType state_class, const CategoryType category)
{ {
String sensor_id = name; String sensor_id = name;
sensor_id.toLowerCase(); sensor_id.toLowerCase();
@ -335,31 +345,39 @@ void MqttHandleHassClass::publishBinarySensor(JsonDocument& doc, const String& r
doc["pl_on"] = payload_on; doc["pl_on"] = payload_on;
doc["pl_off"] = payload_off; doc["pl_off"] = payload_off;
addCommonMetadata(doc, "", "", device_class, category); addCommonMetadata(doc, "", "", device_class, state_class, category);
const String configTopic = "binary_sensor/" + root_device + "/" + sensor_id + "/config"; const String configTopic = "binary_sensor/" + root_device + "/" + sensor_id + "/config";
publish(configTopic, doc); publish(configTopic, doc);
} }
void MqttHandleHassClass::publishDtuBinarySensor(const String& name, const String& state_topic, const String& payload_on, const String& payload_off, const DeviceClassType device_class, const CategoryType category) void MqttHandleHassClass::publishDtuBinarySensor(
const String& name, const String& state_topic, const String& payload_on, const String& payload_off,
const DeviceClassType device_class, const StateClassType state_class, const CategoryType category)
{ {
const String dtuId = getDtuUniqueId(); const String dtuId = getDtuUniqueId();
JsonDocument root; JsonDocument root;
createDtuInfo(root); createDtuInfo(root);
publishBinarySensor(root, dtuId, dtuId, name, state_topic, payload_on, payload_off, device_class, category); publishBinarySensor(root, dtuId, dtuId, name, state_topic, payload_on, payload_off, device_class, state_class, category);
} }
void MqttHandleHassClass::publishInverterBinarySensor(std::shared_ptr<InverterAbstract> inv, const String& name, const String& state_topic, const String& payload_on, const String& payload_off, const DeviceClassType device_class, const CategoryType category) void MqttHandleHassClass::publishInverterBinarySensor(
std::shared_ptr<InverterAbstract> inv, const String& name, const String& state_topic, const String& payload_on, const String& payload_off,
const DeviceClassType device_class, const StateClassType state_class, const CategoryType category)
{ {
const String serial = inv->serialString(); const String serial = inv->serialString();
JsonDocument root; JsonDocument root;
createInverterInfo(root, inv); createInverterInfo(root, inv);
publishBinarySensor(root, "dtu_" + serial, serial, name, serial + "/" + state_topic, payload_on, payload_off, device_class, category); publishBinarySensor(root, "dtu_" + serial, serial, name, serial + "/" + state_topic, payload_on, payload_off, device_class, state_class, category);
} }
void MqttHandleHassClass::publishSensor(JsonDocument& doc, const String& root_device, const String& unique_id_prefix, const String& name, const String& state_topic, const String& unit_of_measure, const String& icon, const DeviceClassType device_class, const CategoryType category) void MqttHandleHassClass::publishSensor(
JsonDocument& doc,
const String& root_device, const String& unique_id_prefix, const String& name, const String& state_topic,
const String& unit_of_measure, const String& icon,
const DeviceClassType device_class, const StateClassType state_class, const CategoryType category)
{ {
String sensor_id = name; String sensor_id = name;
sensor_id.toLowerCase(); sensor_id.toLowerCase();
@ -369,7 +387,7 @@ void MqttHandleHassClass::publishSensor(JsonDocument& doc, const String& root_de
doc["uniq_id"] = unique_id_prefix + "_" + sensor_id; doc["uniq_id"] = unique_id_prefix + "_" + sensor_id;
doc["stat_t"] = MqttSettings.getPrefix() + state_topic; doc["stat_t"] = MqttSettings.getPrefix() + state_topic;
addCommonMetadata(doc, unit_of_measure, icon, device_class, category); addCommonMetadata(doc, unit_of_measure, icon, device_class, state_class, category);
const CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
doc["avty_t"] = MqttSettings.getPrefix() + config.Mqtt.Lwt.Topic; doc["avty_t"] = MqttSettings.getPrefix() + config.Mqtt.Lwt.Topic;
@ -380,20 +398,26 @@ void MqttHandleHassClass::publishSensor(JsonDocument& doc, const String& root_de
publish(configTopic, doc); publish(configTopic, doc);
} }
void MqttHandleHassClass::publishDtuSensor(const String& name, const String& state_topic, const String& unit_of_measure, const String& icon, const DeviceClassType device_class, const CategoryType category) void MqttHandleHassClass::publishDtuSensor(
const String& name, const String& state_topic,
const String& unit_of_measure, const String& icon,
const DeviceClassType device_class, const StateClassType state_class, const CategoryType category)
{ {
const String dtuId = getDtuUniqueId(); const String dtuId = getDtuUniqueId();
JsonDocument root; JsonDocument root;
createDtuInfo(root); createDtuInfo(root);
publishSensor(root, dtuId, dtuId, name, state_topic, unit_of_measure, icon, device_class, category); publishSensor(root, dtuId, dtuId, name, state_topic, unit_of_measure, icon, device_class, state_class, category);
} }
void MqttHandleHassClass::publishInverterSensor(std::shared_ptr<InverterAbstract> inv, const String& name, const String& state_topic, const String& unit_of_measure, const String& icon, const DeviceClassType device_class, const CategoryType category) void MqttHandleHassClass::publishInverterSensor(
std::shared_ptr<InverterAbstract> inv, const String& name, const String& state_topic,
const String& unit_of_measure, const String& icon,
const DeviceClassType device_class, const StateClassType state_class, const CategoryType category)
{ {
const String serial = inv->serialString(); const String serial = inv->serialString();
JsonDocument root; JsonDocument root;
createInverterInfo(root, inv); createInverterInfo(root, inv);
publishSensor(root, "dtu_" + serial, serial, name, serial + "/" + state_topic, unit_of_measure, icon, device_class, category); publishSensor(root, "dtu_" + serial, serial, name, serial + "/" + state_topic, unit_of_measure, icon, device_class, state_class, category);
} }

View File

@ -50,6 +50,7 @@ void MqttHandleInverterClass::loop()
MqttSettings.publish(subtopic + "/radio/rx_fail_nothing", String(inv->RadioStats.RxFailNoAnswer)); MqttSettings.publish(subtopic + "/radio/rx_fail_nothing", String(inv->RadioStats.RxFailNoAnswer));
MqttSettings.publish(subtopic + "/radio/rx_fail_partial", String(inv->RadioStats.RxFailPartialAnswer)); MqttSettings.publish(subtopic + "/radio/rx_fail_partial", String(inv->RadioStats.RxFailPartialAnswer));
MqttSettings.publish(subtopic + "/radio/rx_fail_corrupt", String(inv->RadioStats.RxFailCorruptData)); MqttSettings.publish(subtopic + "/radio/rx_fail_corrupt", String(inv->RadioStats.RxFailCorruptData));
MqttSettings.publish(subtopic + "/radio/rssi", String(inv->getLastRssi()));
if (inv->DevInfo()->getLastUpdate() > 0) { if (inv->DevInfo()->getLastUpdate() > 0) {
// Bootloader Version // Bootloader Version
@ -217,7 +218,7 @@ void MqttHandleInverterClass::onMqttMessage(Topic t, const espMqttClientTypes::M
case Topic::Power: case Topic::Power:
// Turn inverter on or off // Turn inverter on or off
MessageOutput.printf("Set inverter power to: %d\r\n", static_cast<int32_t>(payload_val)); MessageOutput.printf("Set inverter power to: %" PRId32 "\r\n", static_cast<int32_t>(payload_val));
inv->sendPowerControlRequest(static_cast<int32_t>(payload_val) > 0); inv->sendPowerControlRequest(static_cast<int32_t>(payload_val) > 0);
break; break;

View File

@ -91,8 +91,7 @@ void MqttSettingsClass::onMqttDisconnect(espMqttClientTypes::DisconnectReason re
void MqttSettingsClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, const size_t len, const size_t index, const size_t total) void MqttSettingsClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, const size_t len, const size_t index, const size_t total)
{ {
MessageOutput.print("Received MQTT message on topic: "); MessageOutput.printf("Received MQTT message on topic: %s\r\n", topic);
MessageOutput.println(topic);
_mqttSubscribeParser.handle_message(properties, topic, payload, len, index, total); _mqttSubscribeParser.handle_message(properties, topic, payload, len, index, total);
} }

View File

@ -34,8 +34,14 @@ void NetworkSettingsClass::init(Scheduler& scheduler)
if (PinMapping.isValidW5500Config()) { if (PinMapping.isValidW5500Config()) {
PinMapping_t& pin = PinMapping.get(); PinMapping_t& pin = PinMapping.get();
_w5500 = std::make_unique<W5500>(pin.w5500_mosi, pin.w5500_miso, pin.w5500_sclk, pin.w5500_cs, pin.w5500_int, pin.w5500_rst); _w5500 = W5500::setup(pin.w5500_mosi, pin.w5500_miso, pin.w5500_sclk, pin.w5500_cs, pin.w5500_int, pin.w5500_rst);
} else if (PinMapping.isValidEthConfig()) { if (_w5500)
MessageOutput.println("W5500: Connection successful");
else
MessageOutput.println("W5500: Connection error!!");
}
#if CONFIG_ETH_USE_ESP32_EMAC
else if (PinMapping.isValidEthConfig()) {
PinMapping_t& pin = PinMapping.get(); PinMapping_t& pin = PinMapping.get();
#if ESP_ARDUINO_VERSION_MAJOR < 3 #if ESP_ARDUINO_VERSION_MAJOR < 3
ETH.begin(pin.eth_phy_addr, pin.eth_power, pin.eth_mdc, pin.eth_mdio, pin.eth_type, pin.eth_clk_mode); ETH.begin(pin.eth_phy_addr, pin.eth_power, pin.eth_mdc, pin.eth_mdio, pin.eth_type, pin.eth_clk_mode);
@ -43,6 +49,7 @@ void NetworkSettingsClass::init(Scheduler& scheduler)
ETH.begin(pin.eth_type, pin.eth_phy_addr, pin.eth_mdc, pin.eth_mdio, pin.eth_power, pin.eth_clk_mode); ETH.begin(pin.eth_type, pin.eth_phy_addr, pin.eth_mdc, pin.eth_mdio, pin.eth_power, pin.eth_clk_mode);
#endif #endif
} }
#endif
setupMode(); setupMode();
@ -91,7 +98,7 @@ void NetworkSettingsClass::NetworkEvent(const WiFiEvent_t event, WiFiEventInfo_t
break; break;
case ARDUINO_EVENT_WIFI_STA_DISCONNECTED: case ARDUINO_EVENT_WIFI_STA_DISCONNECTED:
// Reason codes can be found here: https://github.com/espressif/esp-idf/blob/5454d37d496a8c58542eb450467471404c606501/components/esp_wifi/include/esp_wifi_types_generic.h#L79-L141 // Reason codes can be found here: https://github.com/espressif/esp-idf/blob/5454d37d496a8c58542eb450467471404c606501/components/esp_wifi/include/esp_wifi_types_generic.h#L79-L141
MessageOutput.printf("WiFi disconnected: %d\r\n", info.wifi_sta_disconnected.reason); MessageOutput.printf("WiFi disconnected: %" PRIu8 "\r\n", info.wifi_sta_disconnected.reason);
if (_networkMode == network_mode::WiFi) { if (_networkMode == network_mode::WiFi) {
MessageOutput.println("Try reconnecting"); MessageOutput.println("Try reconnecting");
WiFi.disconnect(true, false); WiFi.disconnect(true, false);
@ -115,7 +122,7 @@ bool NetworkSettingsClass::onEvent(DtuNetworkEventCb cbEvent, const network_even
if (!cbEvent) { if (!cbEvent) {
return pdFALSE; return pdFALSE;
} }
NetworkEventCbList_t newEventHandler; DtuNetworkEventCbList_t newEventHandler;
newEventHandler.cb = cbEvent; newEventHandler.cb = cbEvent;
newEventHandler.event = event; newEventHandler.event = event;
_cbEventList.push_back(newEventHandler); _cbEventList.push_back(newEventHandler);
@ -219,7 +226,7 @@ void NetworkSettingsClass::loop()
if (_adminEnabled && _adminTimeoutCounterMax > 0) { if (_adminEnabled && _adminTimeoutCounterMax > 0) {
_adminTimeoutCounter++; _adminTimeoutCounter++;
if (_adminTimeoutCounter % 10 == 0) { if (_adminTimeoutCounter % 10 == 0) {
MessageOutput.printf("Admin AP remaining seconds: %d / %d\r\n", _adminTimeoutCounter, _adminTimeoutCounterMax); MessageOutput.printf("Admin AP remaining seconds: %" PRId32 " / %" PRId32 "\r\n", _adminTimeoutCounter, _adminTimeoutCounterMax);
} }
} }
_connectTimeoutTimer++; _connectTimeoutTimer++;

View File

@ -4,6 +4,7 @@
*/ */
#include "PinMapping.h" #include "PinMapping.h"
#include "MessageOutput.h" #include "MessageOutput.h"
#include "Utils.h"
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <LittleFS.h> #include <LittleFS.h>
#include <string.h> #include <string.h>
@ -108,6 +109,8 @@
#define W5500_RST -1 #define W5500_RST -1
#endif #endif
#if CONFIG_ETH_USE_ESP32_EMAC
#ifndef ETH_PHY_ADDR #ifndef ETH_PHY_ADDR
#define ETH_PHY_ADDR -1 #define ETH_PHY_ADDR -1
#endif #endif
@ -132,6 +135,8 @@
#define ETH_CLK_MODE ETH_CLOCK_GPIO0_IN #define ETH_CLK_MODE ETH_CLOCK_GPIO0_IN
#endif #endif
#endif
PinMappingClass PinMapping; PinMappingClass PinMapping;
PinMappingClass::PinMappingClass() PinMappingClass::PinMappingClass()
@ -158,18 +163,19 @@ PinMappingClass::PinMappingClass()
_pinMapping.w5500_int = W5500_INT; _pinMapping.w5500_int = W5500_INT;
_pinMapping.w5500_rst = W5500_RST; _pinMapping.w5500_rst = W5500_RST;
#if CONFIG_ETH_USE_ESP32_EMAC
#ifdef OPENDTU_ETHERNET #ifdef OPENDTU_ETHERNET
_pinMapping.eth_enabled = true; _pinMapping.eth_enabled = true;
#else #else
_pinMapping.eth_enabled = false; _pinMapping.eth_enabled = false;
#endif #endif
_pinMapping.eth_phy_addr = ETH_PHY_ADDR; _pinMapping.eth_phy_addr = ETH_PHY_ADDR;
_pinMapping.eth_power = ETH_PHY_POWER; _pinMapping.eth_power = ETH_PHY_POWER;
_pinMapping.eth_mdc = ETH_PHY_MDC; _pinMapping.eth_mdc = ETH_PHY_MDC;
_pinMapping.eth_mdio = ETH_PHY_MDIO; _pinMapping.eth_mdio = ETH_PHY_MDIO;
_pinMapping.eth_type = ETH_PHY_TYPE; _pinMapping.eth_type = ETH_PHY_TYPE;
_pinMapping.eth_clk_mode = ETH_CLK_MODE; _pinMapping.eth_clk_mode = ETH_CLK_MODE;
#endif
_pinMapping.display_type = DISPLAY_TYPE; _pinMapping.display_type = DISPLAY_TYPE;
_pinMapping.display_data = DISPLAY_DATA; _pinMapping.display_data = DISPLAY_DATA;
@ -194,6 +200,8 @@ bool PinMappingClass::init(const String& deviceMapping)
return false; return false;
} }
Utils::skipBom(f);
JsonDocument doc; JsonDocument doc;
// Deserialize the JSON document // Deserialize the JSON document
DeserializationError error = deserializeJson(doc, f); DeserializationError error = deserializeJson(doc, f);
@ -226,18 +234,19 @@ bool PinMappingClass::init(const String& deviceMapping)
_pinMapping.w5500_int = doc[i]["w5500"]["int"] | W5500_INT; _pinMapping.w5500_int = doc[i]["w5500"]["int"] | W5500_INT;
_pinMapping.w5500_rst = doc[i]["w5500"]["rst"] | W5500_RST; _pinMapping.w5500_rst = doc[i]["w5500"]["rst"] | W5500_RST;
#if CONFIG_ETH_USE_ESP32_EMAC
#ifdef OPENDTU_ETHERNET #ifdef OPENDTU_ETHERNET
_pinMapping.eth_enabled = doc[i]["eth"]["enabled"] | true; _pinMapping.eth_enabled = doc[i]["eth"]["enabled"] | true;
#else #else
_pinMapping.eth_enabled = doc[i]["eth"]["enabled"] | false; _pinMapping.eth_enabled = doc[i]["eth"]["enabled"] | false;
#endif #endif
_pinMapping.eth_phy_addr = doc[i]["eth"]["phy_addr"] | ETH_PHY_ADDR; _pinMapping.eth_phy_addr = doc[i]["eth"]["phy_addr"] | ETH_PHY_ADDR;
_pinMapping.eth_power = doc[i]["eth"]["power"] | ETH_PHY_POWER; _pinMapping.eth_power = doc[i]["eth"]["power"] | ETH_PHY_POWER;
_pinMapping.eth_mdc = doc[i]["eth"]["mdc"] | ETH_PHY_MDC; _pinMapping.eth_mdc = doc[i]["eth"]["mdc"] | ETH_PHY_MDC;
_pinMapping.eth_mdio = doc[i]["eth"]["mdio"] | ETH_PHY_MDIO; _pinMapping.eth_mdio = doc[i]["eth"]["mdio"] | ETH_PHY_MDIO;
_pinMapping.eth_type = doc[i]["eth"]["type"] | ETH_PHY_TYPE; _pinMapping.eth_type = doc[i]["eth"]["type"] | ETH_PHY_TYPE;
_pinMapping.eth_clk_mode = doc[i]["eth"]["clk_mode"] | ETH_CLK_MODE; _pinMapping.eth_clk_mode = doc[i]["eth"]["clk_mode"] | ETH_CLK_MODE;
#endif
_pinMapping.display_type = doc[i]["display"]["type"] | DISPLAY_TYPE; _pinMapping.display_type = doc[i]["display"]["type"] | DISPLAY_TYPE;
_pinMapping.display_data = doc[i]["display"]["data"] | DISPLAY_DATA; _pinMapping.display_data = doc[i]["display"]["data"] | DISPLAY_DATA;
@ -283,9 +292,11 @@ bool PinMappingClass::isValidW5500Config() const
&& _pinMapping.w5500_rst >= 0; && _pinMapping.w5500_rst >= 0;
} }
#if CONFIG_ETH_USE_ESP32_EMAC
bool PinMappingClass::isValidEthConfig() const bool PinMappingClass::isValidEthConfig() const
{ {
return _pinMapping.eth_enabled return _pinMapping.eth_enabled
&& _pinMapping.eth_mdc >= 0 && _pinMapping.eth_mdc >= 0
&& _pinMapping.eth_mdio >= 0; && _pinMapping.eth_mdio >= 0;
} }
#endif

View File

@ -7,6 +7,8 @@
#include "Utils.h" #include "Utils.h"
#include <Arduino.h> #include <Arduino.h>
#define CALC_UNIQUE_ID (((timeinfo.tm_year << 9) | (timeinfo.tm_mon << 5) | timeinfo.tm_mday) << 1 | timeinfo.tm_isdst)
SunPositionClass SunPosition; SunPositionClass SunPosition;
SunPositionClass::SunPositionClass() SunPositionClass::SunPositionClass()
@ -57,7 +59,7 @@ bool SunPositionClass::checkRecalcDayChanged() const
time(&now); time(&now);
localtime_r(&now, &timeinfo); // don't use getLocalTime() as there could be a delay of 10ms localtime_r(&now, &timeinfo); // don't use getLocalTime() as there could be a delay of 10ms
const uint32_t ymd = (timeinfo.tm_year << 9) | (timeinfo.tm_mon << 5) | timeinfo.tm_mday; const uint32_t ymd = CALC_UNIQUE_ID;
return _lastSunPositionCalculatedYMD != ymd; return _lastSunPositionCalculatedYMD != ymd;
} }
@ -67,7 +69,7 @@ void SunPositionClass::updateSunData()
struct tm timeinfo; struct tm timeinfo;
const bool gotLocalTime = getLocalTime(&timeinfo, 5); const bool gotLocalTime = getLocalTime(&timeinfo, 5);
_lastSunPositionCalculatedYMD = (timeinfo.tm_year << 9) | (timeinfo.tm_mon << 5) | timeinfo.tm_mday; _lastSunPositionCalculatedYMD = CALC_UNIQUE_ID;
setDoRecalc(false); setDoRecalc(false);
if (!gotLocalTime) { if (!gotLocalTime) {

View File

@ -7,6 +7,7 @@
#include "MessageOutput.h" #include "MessageOutput.h"
#include "PinMapping.h" #include "PinMapping.h"
#include <LittleFS.h> #include <LittleFS.h>
#include <MD5Builder.h>
uint32_t Utils::getChipId() uint32_t Utils::getChipId()
{ {
@ -60,7 +61,7 @@ int Utils::getTimezoneOffset()
bool Utils::checkJsonAlloc(const JsonDocument& doc, const char* function, const uint16_t line) bool Utils::checkJsonAlloc(const JsonDocument& doc, const char* function, const uint16_t line)
{ {
if (doc.overflowed()) { if (doc.overflowed()) {
MessageOutput.printf("Alloc failed: %s, %d\r\n", function, line); MessageOutput.printf("Alloc failed: %s, %" PRId16 "\r\n", function, line);
return false; return false;
} }
@ -80,3 +81,45 @@ void Utils::removeAllFiles()
file = root.getNextFileName(); 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
uint8_t buffer[512];
while (f.available()) {
size_t bytesRead = f.read(buffer, sizeof(buffer) / sizeof(buffer[0]));
md5.add(buffer, bytesRead);
}
// Finalize and calculate the MD5 hash
md5.calculate();
f.close();
return md5.toString();
}
void Utils::skipBom(File& f)
{
// skip Byte Order Mask (BOM). valid JSON docs always start with '{' or '['.
while (f.available() > 0) {
int c = f.peek();
if (c == '{' || c == '[') {
break;
}
f.read();
}
}

View File

@ -12,52 +12,10 @@
void tcpipInit(); void tcpipInit();
void add_esp_interface_netif(esp_interface_t interface, esp_netif_t* esp_netif); void add_esp_interface_netif(esp_interface_t interface, esp_netif_t* esp_netif);
W5500::W5500(int8_t pin_mosi, int8_t pin_miso, int8_t pin_sclk, int8_t pin_cs, int8_t pin_int, int8_t pin_rst) W5500::W5500(spi_device_handle_t spi, gpio_num_t pin_int)
: eth_handle(nullptr) : eth_handle(nullptr)
, eth_netif(nullptr) , eth_netif(nullptr)
{ {
gpio_reset_pin(static_cast<gpio_num_t>(pin_rst));
gpio_set_level(static_cast<gpio_num_t>(pin_rst), 0);
gpio_set_direction(static_cast<gpio_num_t>(pin_rst), GPIO_MODE_OUTPUT);
gpio_reset_pin(static_cast<gpio_num_t>(pin_cs));
gpio_reset_pin(static_cast<gpio_num_t>(pin_int));
esp_err_t err = gpio_install_isr_service(ARDUINO_ISR_FLAG);
if (err != ESP_ERR_INVALID_STATE) // don't raise an error when ISR service is already installed
ESP_ERROR_CHECK(err);
auto bus_config = std::make_shared<SpiBusConfig>(
static_cast<gpio_num_t>(pin_mosi),
static_cast<gpio_num_t>(pin_miso),
static_cast<gpio_num_t>(pin_sclk));
spi_device_interface_config_t device_config {
.command_bits = 16, // actually address phase
.address_bits = 8, // actually command phase
.dummy_bits = 0,
.mode = 0,
.duty_cycle_pos = 0,
.cs_ena_pretrans = 0, // only 0 supported
.cs_ena_posttrans = 0, // only 0 supported
.clock_speed_hz = 20000000, // stable with OpenDTU Fusion shield
.input_delay_ns = 0,
.spics_io_num = pin_cs,
.flags = 0,
.queue_size = 20,
.pre_cb = nullptr,
.post_cb = nullptr,
};
spi_device_handle_t spi = SpiManagerInst.alloc_device("", bus_config, device_config);
if (!spi)
ESP_ERROR_CHECK(ESP_FAIL);
// Reset sequence
delayMicroseconds(500);
gpio_set_level(static_cast<gpio_num_t>(pin_rst), 1);
delayMicroseconds(1000);
// Arduino function to start networking stack if not already started // Arduino function to start networking stack if not already started
tcpipInit(); tcpipInit();
@ -98,6 +56,62 @@ W5500::~W5500()
// TODO(LennartF22): support cleanup at some point? // TODO(LennartF22): support cleanup at some point?
} }
std::unique_ptr<W5500> W5500::setup(int8_t pin_mosi, int8_t pin_miso, int8_t pin_sclk, int8_t pin_cs, int8_t pin_int, int8_t pin_rst)
{
gpio_reset_pin(static_cast<gpio_num_t>(pin_rst));
gpio_set_level(static_cast<gpio_num_t>(pin_rst), 0);
gpio_set_direction(static_cast<gpio_num_t>(pin_rst), GPIO_MODE_OUTPUT);
gpio_reset_pin(static_cast<gpio_num_t>(pin_cs));
gpio_reset_pin(static_cast<gpio_num_t>(pin_int));
auto bus_config = std::make_shared<SpiBusConfig>(
static_cast<gpio_num_t>(pin_mosi),
static_cast<gpio_num_t>(pin_miso),
static_cast<gpio_num_t>(pin_sclk));
spi_device_interface_config_t device_config {
.command_bits = 16, // actually address phase
.address_bits = 8, // actually command phase
.dummy_bits = 0,
.mode = 0,
.duty_cycle_pos = 0,
.cs_ena_pretrans = 0, // only 0 supported
.cs_ena_posttrans = 0, // only 0 supported
.clock_speed_hz = 20000000, // stable with OpenDTU Fusion shield
.input_delay_ns = 0,
.spics_io_num = pin_cs,
.flags = 0,
.queue_size = 20,
.pre_cb = nullptr,
.post_cb = nullptr,
};
spi_device_handle_t spi = SpiManagerInst.alloc_device("", bus_config, device_config);
if (!spi)
return nullptr;
// Reset sequence
delayMicroseconds(500);
gpio_set_level(static_cast<gpio_num_t>(pin_rst), 1);
delayMicroseconds(1000);
if (!connection_check_spi(spi))
return nullptr;
if (!connection_check_interrupt(static_cast<gpio_num_t>(pin_int)))
return nullptr;
// Use Arduino functions to temporarily attach interrupt to enable the GPIO ISR service
// (if we used ESP-IDF functions, a warning would be printed the first time anyone uses attachInterrupt)
attachInterrupt(pin_int, nullptr, FALLING);
detachInterrupt(pin_int);
// Return to default state once again after connection check and temporary interrupt registration
gpio_reset_pin(static_cast<gpio_num_t>(pin_int));
return std::unique_ptr<W5500>(new W5500(spi, static_cast<gpio_num_t>(pin_int)));
}
String W5500::macAddress() String W5500::macAddress()
{ {
uint8_t mac_addr[6] = {}; uint8_t mac_addr[6] = {};
@ -109,3 +123,31 @@ String W5500::macAddress()
mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]); mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
return String(mac_addr_str); return String(mac_addr_str);
} }
bool W5500::connection_check_spi(spi_device_handle_t spi)
{
spi_transaction_t trans = {
.flags = SPI_TRANS_USE_RXDATA,
.cmd = 0x0039, // actually address (VERSIONR)
.addr = (0b00000 << 3) | (0 << 2) | (0b00 < 0), // actually command (common register, read, VDM)
.length = 8,
.rxlength = 8,
.user = nullptr,
.tx_buffer = nullptr,
.rx_data = {},
};
ESP_ERROR_CHECK(spi_device_polling_transmit(spi, &trans));
// Version number (VERSIONR) is always 0x04
return *reinterpret_cast<uint8_t*>(&trans.rx_data) == 0x04;
}
bool W5500::connection_check_interrupt(gpio_num_t pin_int)
{
gpio_set_direction(pin_int, GPIO_MODE_INPUT);
gpio_set_pull_mode(pin_int, GPIO_PULLDOWN_ONLY);
int level = gpio_get_level(pin_int);
// Interrupt line must be high
return level == 1;
}

View File

@ -15,13 +15,14 @@ WebApiClass::WebApiClass()
void WebApiClass::init(Scheduler& scheduler) void WebApiClass::init(Scheduler& scheduler)
{ {
_webApiConfig.init(_server, scheduler);
_webApiDevice.init(_server, scheduler); _webApiDevice.init(_server, scheduler);
_webApiDevInfo.init(_server, scheduler); _webApiDevInfo.init(_server, scheduler);
_webApiDtu.init(_server, scheduler); _webApiDtu.init(_server, scheduler);
_webApiEventlog.init(_server, scheduler); _webApiEventlog.init(_server, scheduler);
_webApiFile.init(_server, scheduler);
_webApiFirmware.init(_server, scheduler); _webApiFirmware.init(_server, scheduler);
_webApiGridprofile.init(_server, scheduler); _webApiGridprofile.init(_server, scheduler);
_webApiI18n.init(_server, scheduler);
_webApiInverter.init(_server, scheduler); _webApiInverter.init(_server, scheduler);
_webApiLimit.init(_server, scheduler); _webApiLimit.init(_server, scheduler);
_webApiMaintenance.init(_server, scheduler); _webApiMaintenance.init(_server, scheduler);
@ -39,9 +40,15 @@ void WebApiClass::init(Scheduler& scheduler)
_server.begin(); _server.begin();
} }
void WebApiClass::reload()
{
_webApiWsConsole.reload();
_webApiWsLive.reload();
}
bool WebApiClass::checkCredentials(AsyncWebServerRequest* request) bool WebApiClass::checkCredentials(AsyncWebServerRequest* request)
{ {
CONFIG_T& config = Configuration.get(); auto const& config = Configuration.get();
if (request->authenticate(AUTH_USERNAME, config.Security.Password)) { if (request->authenticate(AUTH_USERNAME, config.Security.Password)) {
return true; return true;
} }
@ -59,7 +66,7 @@ bool WebApiClass::checkCredentials(AsyncWebServerRequest* request)
bool WebApiClass::checkCredentialsReadonly(AsyncWebServerRequest* request) bool WebApiClass::checkCredentialsReadonly(AsyncWebServerRequest* request)
{ {
CONFIG_T& config = Configuration.get(); auto const& config = Configuration.get();
if (config.Security.AllowReadonly) { if (config.Security.AllowReadonly) {
return true; return true;
} else { } else {
@ -131,7 +138,7 @@ bool WebApiClass::sendJsonResponse(AsyncWebServerRequest* request, AsyncJsonResp
root["code"] = WebApiError::GenericInternalServerError; root["code"] = WebApiError::GenericInternalServerError;
root["type"] = "danger"; root["type"] = "danger";
response->setCode(500); response->setCode(500);
MessageOutput.printf("WebResponse failed: %s, %d\r\n", function, line); MessageOutput.printf("WebResponse failed: %s, %" PRId16 "\r\n", function, line);
ret_val = false; ret_val = false;
} }

View File

@ -58,6 +58,7 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request)
w5500PinObj["int"] = pin.w5500_int; w5500PinObj["int"] = pin.w5500_int;
w5500PinObj["rst"] = pin.w5500_rst; w5500PinObj["rst"] = pin.w5500_rst;
#if CONFIG_ETH_USE_ESP32_EMAC
auto ethPinObj = curPin["eth"].to<JsonObject>(); auto ethPinObj = curPin["eth"].to<JsonObject>();
ethPinObj["enabled"] = pin.eth_enabled; ethPinObj["enabled"] = pin.eth_enabled;
ethPinObj["phy_addr"] = pin.eth_phy_addr; ethPinObj["phy_addr"] = pin.eth_phy_addr;
@ -66,6 +67,7 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request)
ethPinObj["mdio"] = pin.eth_mdio; ethPinObj["mdio"] = pin.eth_mdio;
ethPinObj["type"] = pin.eth_type; ethPinObj["type"] = pin.eth_type;
ethPinObj["clk_mode"] = pin.eth_clk_mode; ethPinObj["clk_mode"] = pin.eth_clk_mode;
#endif
auto displayPinObj = curPin["display"].to<JsonObject>(); auto displayPinObj = curPin["display"].to<JsonObject>();
displayPinObj["type"] = pin.display_type; displayPinObj["type"] = pin.display_type;
@ -84,7 +86,7 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request)
display["power_safe"] = config.Display.PowerSafe; display["power_safe"] = config.Display.PowerSafe;
display["screensaver"] = config.Display.ScreenSaver; display["screensaver"] = config.Display.ScreenSaver;
display["contrast"] = config.Display.Contrast; display["contrast"] = config.Display.Contrast;
display["language"] = config.Display.Language; display["locale"] = config.Display.Locale;
display["diagramduration"] = config.Display.Diagram.Duration; display["diagramduration"] = config.Display.Diagram.Duration;
display["diagrammode"] = config.Display.Diagram.Mode; display["diagrammode"] = config.Display.Diagram.Mode;
@ -127,15 +129,16 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request)
return; return;
} }
CONFIG_T& config = Configuration.get(); {
bool performRestart = root["curPin"]["name"].as<String>() != config.Dev_PinMapping; auto guard = Configuration.getWriteGuard();
auto& config = guard.getConfig();
strlcpy(config.Dev_PinMapping, root["curPin"]["name"].as<String>().c_str(), sizeof(config.Dev_PinMapping)); strlcpy(config.Dev_PinMapping, root["curPin"]["name"].as<String>().c_str(), sizeof(config.Dev_PinMapping));
config.Display.Rotation = root["display"]["rotation"].as<uint8_t>(); config.Display.Rotation = root["display"]["rotation"].as<uint8_t>();
config.Display.PowerSafe = root["display"]["power_safe"].as<bool>(); config.Display.PowerSafe = root["display"]["power_safe"].as<bool>();
config.Display.ScreenSaver = root["display"]["screensaver"].as<bool>(); config.Display.ScreenSaver = root["display"]["screensaver"].as<bool>();
config.Display.Contrast = root["display"]["contrast"].as<uint8_t>(); config.Display.Contrast = root["display"]["contrast"].as<uint8_t>();
config.Display.Language = root["display"]["language"].as<uint8_t>(); strlcpy(config.Display.Locale, root["display"]["locale"].as<String>().c_str(), sizeof(config.Display.Locale));
config.Display.Diagram.Duration = root["display"]["diagramduration"].as<uint32_t>(); config.Display.Diagram.Duration = root["display"]["diagramduration"].as<uint32_t>();
config.Display.Diagram.Mode = root["display"]["diagrammode"].as<DiagramMode_t>(); config.Display.Diagram.Mode = root["display"]["diagrammode"].as<DiagramMode_t>();
@ -143,13 +146,17 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request)
config.Led_Single[i].Brightness = root["led"][i]["brightness"].as<uint8_t>(); config.Led_Single[i].Brightness = root["led"][i]["brightness"].as<uint8_t>();
config.Led_Single[i].Brightness = min<uint8_t>(100, config.Led_Single[i].Brightness); config.Led_Single[i].Brightness = min<uint8_t>(100, config.Led_Single[i].Brightness);
} }
}
auto const& config = Configuration.get();
bool performRestart = root["curPin"]["name"].as<String>() != config.Dev_PinMapping;
Display.setDiagramMode(static_cast<DiagramMode_t>(config.Display.Diagram.Mode)); Display.setDiagramMode(static_cast<DiagramMode_t>(config.Display.Diagram.Mode));
Display.setOrientation(config.Display.Rotation); Display.setOrientation(config.Display.Rotation);
Display.enablePowerSafe = config.Display.PowerSafe; Display.enablePowerSafe = config.Display.PowerSafe;
Display.enableScreensaver = config.Display.ScreenSaver; Display.enableScreensaver = config.Display.ScreenSaver;
Display.setContrast(config.Display.Contrast); Display.setContrast(config.Display.Contrast);
Display.setLanguage(config.Display.Language); Display.setLocale(config.Display.Locale);
Display.Diagram().updatePeriod(); Display.Diagram().updatePeriod();
WebApi.writeConfig(retMsg); WebApi.writeConfig(retMsg);

View File

@ -27,7 +27,7 @@ void WebApiDtuClass::init(AsyncWebServer& server, Scheduler& scheduler)
void WebApiDtuClass::applyDataTaskCb() void WebApiDtuClass::applyDataTaskCb()
{ {
// Execute stuff in main thread to avoid busy SPI bus // Execute stuff in main thread to avoid busy SPI bus
CONFIG_T& config = Configuration.get(); auto const& config = Configuration.get();
Hoymiles.getRadioNrf()->setPALevel((rf24_pa_dbm_e)config.Dtu.Nrf.PaLevel); Hoymiles.getRadioNrf()->setPALevel((rf24_pa_dbm_e)config.Dtu.Nrf.PaLevel);
Hoymiles.getRadioCmt()->setPALevel(config.Dtu.Cmt.PaLevel); Hoymiles.getRadioCmt()->setPALevel(config.Dtu.Cmt.PaLevel);
Hoymiles.getRadioNrf()->setDtuSerial(config.Dtu.Serial); Hoymiles.getRadioNrf()->setDtuSerial(config.Dtu.Serial);
@ -49,9 +49,9 @@ void WebApiDtuClass::onDtuAdminGet(AsyncWebServerRequest* request)
// DTU Serial is read as HEX // DTU Serial is read as HEX
char buffer[sizeof(uint64_t) * 8 + 1]; char buffer[sizeof(uint64_t) * 8 + 1];
snprintf(buffer, sizeof(buffer), "%0x%08x", snprintf(buffer, sizeof(buffer), "%0" PRIx32 "%08" PRIx32,
((uint32_t)((config.Dtu.Serial >> 32) & 0xFFFFFFFF)), static_cast<uint32_t>((config.Dtu.Serial >> 32) & 0xFFFFFFFF),
((uint32_t)(config.Dtu.Serial & 0xFFFFFFFF))); static_cast<uint32_t>(config.Dtu.Serial & 0xFFFFFFFF));
root["serial"] = buffer; root["serial"] = buffer;
root["pollinterval"] = config.Dtu.PollInterval; root["pollinterval"] = config.Dtu.PollInterval;
root["nrf_enabled"] = Hoymiles.getRadioNrf()->isInitialized(); root["nrf_enabled"] = Hoymiles.getRadioNrf()->isInitialized();
@ -153,14 +153,16 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request)
return; return;
} }
CONFIG_T& config = Configuration.get(); {
auto guard = Configuration.getWriteGuard();
auto& config = guard.getConfig();
config.Dtu.Serial = serial; config.Dtu.Serial = serial;
config.Dtu.PollInterval = root["pollinterval"].as<uint32_t>(); config.Dtu.PollInterval = root["pollinterval"].as<uint32_t>();
config.Dtu.Nrf.PaLevel = root["nrf_palevel"].as<uint8_t>(); config.Dtu.Nrf.PaLevel = root["nrf_palevel"].as<uint8_t>();
config.Dtu.Cmt.PaLevel = root["cmt_palevel"].as<int8_t>(); config.Dtu.Cmt.PaLevel = root["cmt_palevel"].as<int8_t>();
config.Dtu.Cmt.Frequency = root["cmt_frequency"].as<uint32_t>(); config.Dtu.Cmt.Frequency = root["cmt_frequency"].as<uint32_t>();
config.Dtu.Cmt.CountryMode = root["cmt_country"].as<CountryModeId_t>(); config.Dtu.Cmt.CountryMode = root["cmt_country"].as<CountryModeId_t>();
}
WebApi.writeConfig(retMsg); WebApi.writeConfig(retMsg);

View File

@ -2,7 +2,7 @@
/* /*
* Copyright (C) 2022-2024 Thomas Basler and others * Copyright (C) 2022-2024 Thomas Basler and others
*/ */
#include "WebApi_config.h" #include "WebApi_file.h"
#include "Configuration.h" #include "Configuration.h"
#include "RestartHelper.h" #include "RestartHelper.h"
#include "Utils.h" #include "Utils.h"
@ -11,7 +11,7 @@
#include <AsyncJson.h> #include <AsyncJson.h>
#include <LittleFS.h> #include <LittleFS.h>
void WebApiConfigClass::init(AsyncWebServer& server, Scheduler& scheduler) void WebApiFileClass::init(AsyncWebServer& server, Scheduler& scheduler)
{ {
using std::placeholders::_1; using std::placeholders::_1;
using std::placeholders::_2; using std::placeholders::_2;
@ -20,15 +20,43 @@ void WebApiConfigClass::init(AsyncWebServer& server, Scheduler& scheduler)
using std::placeholders::_5; using std::placeholders::_5;
using std::placeholders::_6; using std::placeholders::_6;
server.on("/api/config/get", HTTP_GET, std::bind(&WebApiConfigClass::onConfigGet, this, _1)); server.on("/api/file/get", HTTP_GET, std::bind(&WebApiFileClass::onFileGet, this, _1));
server.on("/api/config/delete", HTTP_POST, std::bind(&WebApiConfigClass::onConfigDelete, this, _1)); server.on("/api/file/delete", HTTP_POST, std::bind(&WebApiFileClass::onFileDelete, this, _1));
server.on("/api/config/list", HTTP_GET, std::bind(&WebApiConfigClass::onConfigListGet, this, _1)); server.on("/api/file/delete_all", HTTP_POST, std::bind(&WebApiFileClass::onFileDeleteAll, this, _1));
server.on("/api/config/upload", HTTP_POST, server.on("/api/file/list", HTTP_GET, std::bind(&WebApiFileClass::onFileListGet, this, _1));
std::bind(&WebApiConfigClass::onConfigUploadFinish, this, _1), server.on("/api/file/upload", HTTP_POST,
std::bind(&WebApiConfigClass::onConfigUpload, this, _1, _2, _3, _4, _5, _6)); std::bind(&WebApiFileClass::onFileUploadFinish, this, _1),
std::bind(&WebApiFileClass::onFileUpload, this, _1, _2, _3, _4, _5, _6));
} }
void WebApiConfigClass::onConfigGet(AsyncWebServerRequest* request) void WebApiFileClass::onFileListGet(AsyncWebServerRequest* request)
{
if (!WebApi.checkCredentials(request)) {
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse();
auto& root = response->getRoot();
auto data = root.to<JsonArray>();
File rootfs = LittleFS.open("/");
File file = rootfs.openNextFile();
while (file) {
if (file.isDirectory()) {
continue;
}
JsonObject obj = data.add<JsonObject>();
obj["name"] = String(file.name());
obj["size"] = file.size();
file = rootfs.openNextFile();
}
file.close();
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
}
void WebApiFileClass::onFileGet(AsyncWebServerRequest* request)
{ {
if (!WebApi.checkCredentials(request)) { if (!WebApi.checkCredentials(request)) {
return; return;
@ -48,7 +76,43 @@ void WebApiConfigClass::onConfigGet(AsyncWebServerRequest* request)
request->send(LittleFS, requestFile, String(), true); request->send(LittleFS, requestFile, String(), true);
} }
void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request) void WebApiFileClass::onFileDelete(AsyncWebServerRequest* request)
{
if (!WebApi.checkCredentials(request)) {
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse();
JsonDocument root;
if (!WebApi.parseRequestData(request, response, root)) {
return;
}
auto& retMsg = response->getRoot();
if (!(root["file"].is<String>())) {
retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing;
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return;
}
String name = "/" + root["file"].as<String>();
if (!LittleFS.exists(name)) {
request->send(404);
return;
}
LittleFS.remove(name);
retMsg["type"] = "success";
retMsg["message"] = "File deleted";
retMsg["code"] = WebApiError::FileDeleteSuccess;
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
}
void WebApiFileClass::onFileDeleteAll(AsyncWebServerRequest* request)
{ {
if (!WebApi.checkCredentials(request)) { if (!WebApi.checkCredentials(request)) {
return; return;
@ -71,14 +135,14 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request)
if (root["delete"].as<bool>() == false) { if (root["delete"].as<bool>() == false) {
retMsg["message"] = "Not deleted anything!"; retMsg["message"] = "Not deleted anything!";
retMsg["code"] = WebApiError::ConfigNotDeleted; retMsg["code"] = WebApiError::FileNotDeleted;
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
return; return;
} }
retMsg["type"] = "success"; retMsg["type"] = "success";
retMsg["message"] = "Configuration resettet. Rebooting now..."; retMsg["message"] = "Configuration resettet. Rebooting now...";
retMsg["code"] = WebApiError::ConfigSuccess; retMsg["code"] = WebApiError::FileSuccess;
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
@ -86,49 +150,7 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request)
RestartHelper.triggerRestart(); RestartHelper.triggerRestart();
} }
void WebApiConfigClass::onConfigListGet(AsyncWebServerRequest* request) void WebApiFileClass::onFileUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final)
{
if (!WebApi.checkCredentials(request)) {
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse();
auto& root = response->getRoot();
auto data = root["configs"].to<JsonArray>();
File rootfs = LittleFS.open("/");
File file = rootfs.openNextFile();
while (file) {
if (file.isDirectory()) {
continue;
}
JsonObject obj = data.add<JsonObject>();
obj["name"] = String(file.name());
file = rootfs.openNextFile();
}
file.close();
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
}
void WebApiConfigClass::onConfigUploadFinish(AsyncWebServerRequest* request)
{
if (!WebApi.checkCredentials(request)) {
return;
}
// the request handler is triggered after the upload has finished...
// create the response, add header, and send response
AsyncWebServerResponse* response = request->beginResponse(200, "text/plain", "OK");
response->addHeader("Connection", "close");
response->addHeader("Access-Control-Allow-Origin", "*");
request->send(response);
RestartHelper.triggerRestart();
}
void WebApiConfigClass::onConfigUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final)
{ {
if (!WebApi.checkCredentials(request)) { if (!WebApi.checkCredentials(request)) {
return; return;
@ -154,3 +176,19 @@ void WebApiConfigClass::onConfigUpload(AsyncWebServerRequest* request, String fi
request->_tempFile.close(); request->_tempFile.close();
} }
} }
void WebApiFileClass::onFileUploadFinish(AsyncWebServerRequest* request)
{
if (!WebApi.checkCredentials(request)) {
return;
}
// the request handler is triggered after the upload has finished...
// create the response, add header, and send response
AsyncWebServerResponse* response = request->beginResponse(200, "text/plain", "OK");
response->addHeader("Connection", "close");
response->addHeader("Access-Control-Allow-Origin", "*");
request->send(response);
RestartHelper.triggerRestart();
}

76
src/WebApi_i18n.cpp Normal file
View File

@ -0,0 +1,76 @@
// 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>
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();
String filename = I18n.getFilenameByLocale(code);
if (filename != "") {
String md5 = Utils::generateMd5FromFile(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, 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

@ -45,9 +45,9 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request)
// Inverter Serial is read as HEX // Inverter Serial is read as HEX
char buffer[sizeof(uint64_t) * 8 + 1]; char buffer[sizeof(uint64_t) * 8 + 1];
snprintf(buffer, sizeof(buffer), "%0x%08x", snprintf(buffer, sizeof(buffer), "%0" PRIx32 "%08" PRIx32,
((uint32_t)((config.Inverter[i].Serial >> 32) & 0xFFFFFFFF)), static_cast<uint32_t>((config.Inverter[i].Serial >> 32) & 0xFFFFFFFF),
((uint32_t)(config.Inverter[i].Serial & 0xFFFFFFFF))); static_cast<uint32_t>(config.Inverter[i].Serial & 0xFFFFFFFF));
obj["serial"] = buffer; obj["serial"] = buffer;
obj["poll_enable"] = config.Inverter[i].Poll_Enable; obj["poll_enable"] = config.Inverter[i].Poll_Enable;
obj["poll_enable_night"] = config.Inverter[i].Poll_Enable_Night; obj["poll_enable_night"] = config.Inverter[i].Poll_Enable_Night;
@ -184,9 +184,9 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
} }
// Interpret the string as a hex value and convert it to uint64_t // Interpret the string as a hex value and convert it to uint64_t
const uint64_t serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16); const uint64_t new_serial = strtoll(root["serial"].as<String>().c_str(), NULL, 16);
if (serial == 0) { if (new_serial == 0) {
retMsg["message"] = "Serial must be a number > 0!"; retMsg["message"] = "Serial must be a number > 0!";
retMsg["code"] = WebApiError::InverterSerialZero; retMsg["code"] = WebApiError::InverterSerialZero;
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
@ -209,12 +209,15 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
return; return;
} }
INVERTER_CONFIG_T& inverter = Configuration.get().Inverter[root["id"].as<uint8_t>()]; uint64_t old_serial = 0;
uint64_t new_serial = serial; {
uint64_t old_serial = inverter.Serial; auto guard = Configuration.getWriteGuard();
auto& config = guard.getConfig();
// Interpret the string as a hex value and convert it to uint64_t INVERTER_CONFIG_T& inverter = config.Inverter[root["id"].as<uint8_t>()];
old_serial = inverter.Serial;
inverter.Serial = new_serial; inverter.Serial = new_serial;
strncpy(inverter.Name, root["name"].as<String>().c_str(), INV_MAX_NAME_STRLEN); strncpy(inverter.Name, root["name"].as<String>().c_str(), INV_MAX_NAME_STRLEN);
@ -235,11 +238,13 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request)
strncpy(inverter.channel[arrayCount].Name, channel["name"] | "", sizeof(inverter.channel[arrayCount].Name)); strncpy(inverter.channel[arrayCount].Name, channel["name"] | "", sizeof(inverter.channel[arrayCount].Name));
arrayCount++; arrayCount++;
} }
}
WebApi.writeConfig(retMsg, WebApiError::InverterChanged, "Inverter changed!"); WebApi.writeConfig(retMsg, WebApiError::InverterChanged, "Inverter changed!");
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
INVERTER_CONFIG_T const& inverter = Configuration.get().Inverter[root["id"].as<uint8_t>()];
std::shared_ptr<InverterAbstract> inv = Hoymiles.getInverterBySerial(old_serial); std::shared_ptr<InverterAbstract> inv = Hoymiles.getInverterBySerial(old_serial);
if (inv != nullptr && new_serial != old_serial) { if (inv != nullptr && new_serial != old_serial) {
@ -300,7 +305,7 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request)
} }
uint8_t inverter_id = root["id"].as<uint8_t>(); uint8_t inverter_id = root["id"].as<uint8_t>();
INVERTER_CONFIG_T& inverter = Configuration.get().Inverter[inverter_id]; INVERTER_CONFIG_T const& inverter = Configuration.get().Inverter[inverter_id];
Hoymiles.removeInverterBySerial(inverter.Serial); Hoymiles.removeInverterBySerial(inverter.Serial);
@ -337,14 +342,19 @@ void WebApiInverterClass::onInverterOrder(AsyncWebServerRequest* request)
// The order array contains list or id in the right order // The order array contains list or id in the right order
JsonArray orderArray = root["order"].as<JsonArray>(); JsonArray orderArray = root["order"].as<JsonArray>();
uint8_t order = 0; uint8_t order = 0;
{
auto guard = Configuration.getWriteGuard();
auto& config = guard.getConfig();
for (JsonVariant id : orderArray) { for (JsonVariant id : orderArray) {
uint8_t inverter_id = id.as<uint8_t>(); uint8_t inverter_id = id.as<uint8_t>();
if (inverter_id < INV_MAX_COUNT) { if (inverter_id < INV_MAX_COUNT) {
INVERTER_CONFIG_T& inverter = Configuration.get().Inverter[inverter_id]; INVERTER_CONFIG_T& inverter = config.Inverter[inverter_id];
inverter.Order = order; inverter.Order = order;
} }
order++; order++;
} }
}
WebApi.writeConfig(retMsg, WebApiError::InverterOrdered, "Inverter order saved!"); WebApi.writeConfig(retMsg, WebApiError::InverterOrdered, "Inverter order saved!");

View File

@ -271,7 +271,10 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
} }
} }
CONFIG_T& config = Configuration.get(); {
auto guard = Configuration.getWriteGuard();
auto& config = guard.getConfig();
config.Mqtt.Enabled = root["mqtt_enabled"].as<bool>(); config.Mqtt.Enabled = root["mqtt_enabled"].as<bool>();
config.Mqtt.Retain = root["mqtt_retain"].as<bool>(); config.Mqtt.Retain = root["mqtt_retain"].as<bool>();
config.Mqtt.Tls.Enabled = root["mqtt_tls"].as<bool>(); config.Mqtt.Tls.Enabled = root["mqtt_tls"].as<bool>();
@ -302,6 +305,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request)
strlcpy(config.Mqtt.Topic, root["mqtt_topic"].as<String>().c_str(), sizeof(config.Mqtt.Topic)); strlcpy(config.Mqtt.Topic, root["mqtt_topic"].as<String>().c_str(), sizeof(config.Mqtt.Topic));
MqttHandleInverter.subscribeTopics(); MqttHandleInverter.subscribeTopics();
} }
}
WebApi.writeConfig(retMsg); WebApi.writeConfig(retMsg);

View File

@ -164,7 +164,10 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request)
return; return;
} }
CONFIG_T& config = Configuration.get(); {
auto guard = Configuration.getWriteGuard();
auto& config = guard.getConfig();
config.WiFi.Ip[0] = ipaddress[0]; config.WiFi.Ip[0] = ipaddress[0];
config.WiFi.Ip[1] = ipaddress[1]; config.WiFi.Ip[1] = ipaddress[1];
config.WiFi.Ip[2] = ipaddress[2]; config.WiFi.Ip[2] = ipaddress[2];
@ -195,6 +198,7 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request)
} }
config.WiFi.ApTimeout = root["aptimeout"].as<uint>(); config.WiFi.ApTimeout = root["aptimeout"].as<uint>();
config.Mdns.Enabled = root["mdnsenabled"].as<bool>(); config.Mdns.Enabled = root["mdnsenabled"].as<bool>();
}
WebApi.writeConfig(retMsg); WebApi.writeConfig(retMsg);

View File

@ -135,13 +135,17 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request)
return; return;
} }
CONFIG_T& config = Configuration.get(); {
auto guard = Configuration.getWriteGuard();
auto& config = guard.getConfig();
strlcpy(config.Ntp.Server, root["ntp_server"].as<String>().c_str(), sizeof(config.Ntp.Server)); strlcpy(config.Ntp.Server, root["ntp_server"].as<String>().c_str(), sizeof(config.Ntp.Server));
strlcpy(config.Ntp.Timezone, root["ntp_timezone"].as<String>().c_str(), sizeof(config.Ntp.Timezone)); strlcpy(config.Ntp.Timezone, root["ntp_timezone"].as<String>().c_str(), sizeof(config.Ntp.Timezone));
strlcpy(config.Ntp.TimezoneDescr, root["ntp_timezone_descr"].as<String>().c_str(), sizeof(config.Ntp.TimezoneDescr)); strlcpy(config.Ntp.TimezoneDescr, root["ntp_timezone_descr"].as<String>().c_str(), sizeof(config.Ntp.TimezoneDescr));
config.Ntp.Latitude = root["latitude"].as<double>(); config.Ntp.Latitude = root["latitude"].as<double>();
config.Ntp.Longitude = root["longitude"].as<double>(); config.Ntp.Longitude = root["longitude"].as<double>();
config.Ntp.SunsetType = root["sunsettype"].as<uint8_t>(); config.Ntp.SunsetType = root["sunsettype"].as<uint8_t>();
}
WebApi.writeConfig(retMsg); WebApi.writeConfig(retMsg);

View File

@ -42,23 +42,23 @@ void WebApiPrometheusClass::onPrometheusMetricsGet(AsyncWebServerRequest* reques
stream->print("# HELP opendtu_heap_size System memory size\n"); stream->print("# HELP opendtu_heap_size System memory size\n");
stream->print("# TYPE opendtu_heap_size gauge\n"); stream->print("# TYPE opendtu_heap_size gauge\n");
stream->printf("opendtu_heap_size %zu\n", ESP.getHeapSize()); stream->printf("opendtu_heap_size %" PRId32 "\n", ESP.getHeapSize());
stream->print("# HELP opendtu_free_heap_size System free memory\n"); stream->print("# HELP opendtu_free_heap_size System free memory\n");
stream->print("# TYPE opendtu_free_heap_size gauge\n"); stream->print("# TYPE opendtu_free_heap_size gauge\n");
stream->printf("opendtu_free_heap_size %zu\n", ESP.getFreeHeap()); stream->printf("opendtu_free_heap_size %" PRId32 "\n", ESP.getFreeHeap());
stream->print("# HELP opendtu_biggest_heap_block Biggest free heap block\n"); stream->print("# HELP opendtu_biggest_heap_block Biggest free heap block\n");
stream->print("# TYPE opendtu_biggest_heap_block gauge\n"); stream->print("# TYPE opendtu_biggest_heap_block gauge\n");
stream->printf("opendtu_biggest_heap_block %zu\n", ESP.getMaxAllocHeap()); stream->printf("opendtu_biggest_heap_block %" PRId32 "\n", ESP.getMaxAllocHeap());
stream->print("# HELP opendtu_heap_min_free Minimum free memory since boot\n"); stream->print("# HELP opendtu_heap_min_free Minimum free memory since boot\n");
stream->print("# TYPE opendtu_heap_min_free gauge\n"); stream->print("# TYPE opendtu_heap_min_free gauge\n");
stream->printf("opendtu_heap_min_free %zu\n", ESP.getMinFreeHeap()); stream->printf("opendtu_heap_min_free %" PRId32 "\n", ESP.getMinFreeHeap());
stream->print("# HELP wifi_rssi WiFi RSSI\n"); stream->print("# HELP wifi_rssi WiFi RSSI\n");
stream->print("# TYPE wifi_rssi gauge\n"); stream->print("# TYPE wifi_rssi gauge\n");
stream->printf("wifi_rssi %d\n", WiFi.RSSI()); stream->printf("wifi_rssi %" PRId8 "\n", WiFi.RSSI());
stream->print("# HELP wifi_station WiFi Station info\n"); stream->print("# HELP wifi_station WiFi Station info\n");
stream->print("# TYPE wifi_station gauge\n"); stream->print("# TYPE wifi_station gauge\n");
@ -73,14 +73,14 @@ void WebApiPrometheusClass::onPrometheusMetricsGet(AsyncWebServerRequest* reques
stream->print("# HELP opendtu_last_update last update from inverter in s\n"); stream->print("# HELP opendtu_last_update last update from inverter in s\n");
stream->print("# TYPE opendtu_last_update gauge\n"); stream->print("# TYPE opendtu_last_update gauge\n");
} }
stream->printf("opendtu_last_update{serial=\"%s\",unit=\"%d\",name=\"%s\"} %d\n", stream->printf("opendtu_last_update{serial=\"%s\",unit=\"%" PRId8 "\",name=\"%s\"} %" PRId32 "\n",
serial.c_str(), i, name, inv->Statistics()->getLastUpdate() / 1000); serial.c_str(), i, name, inv->Statistics()->getLastUpdate() / 1000);
if (i == 0) { if (i == 0) {
stream->print("# HELP opendtu_inverter_limit_relative current relative limit of the inverter\n"); stream->print("# HELP opendtu_inverter_limit_relative current relative limit of the inverter\n");
stream->print("# TYPE opendtu_inverter_limit_relative gauge\n"); stream->print("# TYPE opendtu_inverter_limit_relative gauge\n");
} }
stream->printf("opendtu_inverter_limit_relative{serial=\"%s\",unit=\"%d\",name=\"%s\"} %f\n", stream->printf("opendtu_inverter_limit_relative{serial=\"%s\",unit=\"%" PRId8 "\",name=\"%s\"} %f\n",
serial.c_str(), i, name, inv->SystemConfigPara()->getLimitPercent() / 100.0); serial.c_str(), i, name, inv->SystemConfigPara()->getLimitPercent() / 100.0);
if (inv->DevInfo()->getMaxPower() > 0) { if (inv->DevInfo()->getMaxPower() > 0) {
@ -88,7 +88,7 @@ void WebApiPrometheusClass::onPrometheusMetricsGet(AsyncWebServerRequest* reques
stream->print("# HELP opendtu_inverter_limit_absolute current relative limit of the inverter\n"); stream->print("# HELP opendtu_inverter_limit_absolute current relative limit of the inverter\n");
stream->print("# TYPE opendtu_inverter_limit_absolute gauge\n"); stream->print("# TYPE opendtu_inverter_limit_absolute gauge\n");
} }
stream->printf("opendtu_inverter_limit_absolute{serial=\"%s\",unit=\"%d\",name=\"%s\"} %f\n", stream->printf("opendtu_inverter_limit_absolute{serial=\"%s\",unit=\"%" PRId8 "\",name=\"%s\"} %f\n",
serial.c_str(), i, name, inv->SystemConfigPara()->getLimitPercent() * inv->DevInfo()->getMaxPower() / 100.0); serial.c_str(), i, name, inv->SystemConfigPara()->getLimitPercent() * inv->DevInfo()->getMaxPower() / 100.0);
} }
@ -126,7 +126,7 @@ void WebApiPrometheusClass::addField(AsyncResponseStream* stream, const String&
stream->printf("# HELP opendtu_%s in %s\n", chanName, inv->Statistics()->getChannelFieldUnit(type, channel, fieldId)); stream->printf("# HELP opendtu_%s in %s\n", chanName, inv->Statistics()->getChannelFieldUnit(type, channel, fieldId));
stream->printf("# TYPE opendtu_%s %s\n", chanName, metricName); stream->printf("# TYPE opendtu_%s %s\n", chanName, metricName);
} }
stream->printf("opendtu_%s{serial=\"%s\",unit=\"%d\",name=\"%s\",type=\"%s\",channel=\"%d\"} %s\n", stream->printf("opendtu_%s{serial=\"%s\",unit=\"%" PRId8 "\",name=\"%s\",type=\"%s\",channel=\"%d\"} %s\n",
chanName, chanName,
serial.c_str(), serial.c_str(),
idx, idx,
@ -150,7 +150,7 @@ void WebApiPrometheusClass::addPanelInfo(AsyncResponseStream* stream, const Stri
stream->print("# HELP opendtu_PanelInfo panel information\n"); stream->print("# HELP opendtu_PanelInfo panel information\n");
stream->print("# TYPE opendtu_PanelInfo gauge\n"); stream->print("# TYPE opendtu_PanelInfo gauge\n");
} }
stream->printf("opendtu_PanelInfo{serial=\"%s\",unit=\"%d\",name=\"%s\",channel=\"%d\",panelname=\"%s\"} 1\n", stream->printf("opendtu_PanelInfo{serial=\"%s\",unit=\"%" PRId8 "\",name=\"%s\",channel=\"%d\",panelname=\"%s\"} 1\n",
serial.c_str(), serial.c_str(),
idx, idx,
inv->name(), inv->name(),
@ -161,7 +161,7 @@ void WebApiPrometheusClass::addPanelInfo(AsyncResponseStream* stream, const Stri
stream->print("# HELP opendtu_MaxPower panel maximum output power\n"); stream->print("# HELP opendtu_MaxPower panel maximum output power\n");
stream->print("# TYPE opendtu_MaxPower gauge\n"); stream->print("# TYPE opendtu_MaxPower gauge\n");
} }
stream->printf("opendtu_MaxPower{serial=\"%s\",unit=\"%d\",name=\"%s\",channel=\"%d\"} %d\n", stream->printf("opendtu_MaxPower{serial=\"%s\",unit=\"%" PRId8 "\",name=\"%s\",channel=\"%d\"} %d\n",
serial.c_str(), serial.c_str(),
idx, idx,
inv->name(), inv->name(),
@ -172,7 +172,7 @@ void WebApiPrometheusClass::addPanelInfo(AsyncResponseStream* stream, const Stri
stream->print("# HELP opendtu_YieldTotalOffset panel yield offset (for used inverters)\n"); stream->print("# HELP opendtu_YieldTotalOffset panel yield offset (for used inverters)\n");
stream->print("# TYPE opendtu_YieldTotalOffset gauge\n"); stream->print("# TYPE opendtu_YieldTotalOffset gauge\n");
} }
stream->printf("opendtu_YieldTotalOffset{serial=\"%s\",unit=\"%d\",name=\"%s\",channel=\"%d\"} %f\n", stream->printf("opendtu_YieldTotalOffset{serial=\"%s\",unit=\"%" PRId8 "\",name=\"%s\",channel=\"%" PRId16 "\"} %f\n",
serial.c_str(), serial.c_str(),
idx, idx,
inv->name(), inv->name(),

View File

@ -64,13 +64,19 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request)
return; return;
} }
CONFIG_T& config = Configuration.get(); {
auto guard = Configuration.getWriteGuard();
auto& config = guard.getConfig();
strlcpy(config.Security.Password, root["password"].as<String>().c_str(), sizeof(config.Security.Password)); strlcpy(config.Security.Password, root["password"].as<String>().c_str(), sizeof(config.Security.Password));
config.Security.AllowReadonly = root["allow_readonly"].as<bool>(); config.Security.AllowReadonly = root["allow_readonly"].as<bool>();
}
WebApi.writeConfig(retMsg); WebApi.writeConfig(retMsg);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
WebApi.reload();
} }
void WebApiSecurityClass::onAuthenticateGet(AsyncWebServerRequest* request) void WebApiSecurityClass::onAuthenticateGet(AsyncWebServerRequest* request)

View File

@ -52,6 +52,20 @@ void WebApiSysstatusClass::onSystemStatus(AsyncWebServerRequest* request)
root["chipcores"] = ESP.getChipCores(); root["chipcores"] = ESP.getChipCores();
root["flashsize"] = ESP.getFlashChipSize(); root["flashsize"] = ESP.getFlashChipSize();
JsonArray taskDetails = root["task_details"].to<JsonArray>();
static std::array<char const*, 12> constexpr task_names = {
"IDLE0", "IDLE1", "wifi", "tiT", "loopTask", "async_tcp", "mqttclient",
"HUAWEI_CAN_0", "PM:SDM", "PM:HTTP+JSON", "PM:SML", "PM:HTTP+SML"
};
for (char const* task_name : task_names) {
TaskHandle_t const handle = xTaskGetHandle(task_name);
if (!handle) { continue; }
JsonObject task = taskDetails.add<JsonObject>();
task["name"] = task_name;
task["stack_watermark"] = uxTaskGetStackHighWaterMark(handle);
task["priority"] = uxTaskPriorityGet(handle);
}
String reason; String reason;
reason = ResetReason::get_reset_reason_verbose(0); reason = ResetReason::get_reset_reason_verbose(0);
root["resetreason_0"] = reason; root["resetreason_0"] = reason;

View File

@ -21,16 +21,30 @@ void WebApiWsConsoleClass::init(AsyncWebServer& server, Scheduler& scheduler)
scheduler.addTask(_wsCleanupTask); scheduler.addTask(_wsCleanupTask);
_wsCleanupTask.enable(); _wsCleanupTask.enable();
_simpleDigestAuth.setUsername(AUTH_USERNAME);
_simpleDigestAuth.setRealm("console websocket");
reload();
}
void WebApiWsConsoleClass::reload()
{
_ws.removeMiddleware(&_simpleDigestAuth);
auto const& config = Configuration.get();
if (config.Security.AllowReadonly) { return; }
_ws.enable(false);
_simpleDigestAuth.setPassword(config.Security.Password);
_ws.addMiddleware(&_simpleDigestAuth);
_ws.closeAll();
_ws.enable(true);
} }
void WebApiWsConsoleClass::wsCleanupTaskCb() void WebApiWsConsoleClass::wsCleanupTaskCb()
{ {
// see: https://github.com/me-no-dev/ESPAsyncWebServer#limiting-the-number-of-web-socket-clients // see: https://github.com/me-no-dev/ESPAsyncWebServer#limiting-the-number-of-web-socket-clients
_ws.cleanupClients(); _ws.cleanupClients();
if (Configuration.get().Security.AllowReadonly) {
_ws.setAuthentication("", "");
} else {
_ws.setAuthentication(AUTH_USERNAME, Configuration.get().Security.Password);
}
} }

View File

@ -36,18 +36,31 @@ void WebApiWsLiveClass::init(AsyncWebServer& server, Scheduler& scheduler)
scheduler.addTask(_sendDataTask); scheduler.addTask(_sendDataTask);
_sendDataTask.enable(); _sendDataTask.enable();
_simpleDigestAuth.setUsername(AUTH_USERNAME);
_simpleDigestAuth.setRealm("live websocket");
reload();
}
void WebApiWsLiveClass::reload()
{
_ws.removeMiddleware(&_simpleDigestAuth);
auto const& config = Configuration.get();
if (config.Security.AllowReadonly) { return; }
_ws.enable(false);
_simpleDigestAuth.setPassword(config.Security.Password);
_ws.addMiddleware(&_simpleDigestAuth);
_ws.closeAll();
_ws.enable(true);
} }
void WebApiWsLiveClass::wsCleanupTaskCb() void WebApiWsLiveClass::wsCleanupTaskCb()
{ {
// see: https://github.com/me-no-dev/ESPAsyncWebServer#limiting-the-number-of-web-socket-clients // see: https://github.com/me-no-dev/ESPAsyncWebServer#limiting-the-number-of-web-socket-clients
_ws.cleanupClients(); _ws.cleanupClients();
if (Configuration.get().Security.AllowReadonly) {
_ws.setAuthentication("", "");
} else {
_ws.setAuthentication(AUTH_USERNAME, Configuration.get().Security.Password);
}
} }
void WebApiWsLiveClass::sendDataTaskCb() void WebApiWsLiveClass::sendDataTaskCb()
@ -140,6 +153,7 @@ void WebApiWsLiveClass::generateInverterCommonJsonResponse(JsonObject& root, std
root["radio_stats"]["rx_fail_nothing"] = inv->RadioStats.RxFailNoAnswer; root["radio_stats"]["rx_fail_nothing"] = inv->RadioStats.RxFailNoAnswer;
root["radio_stats"]["rx_fail_partial"] = inv->RadioStats.RxFailPartialAnswer; root["radio_stats"]["rx_fail_partial"] = inv->RadioStats.RxFailPartialAnswer;
root["radio_stats"]["rx_fail_corrupt"] = inv->RadioStats.RxFailCorruptData; root["radio_stats"]["rx_fail_corrupt"] = inv->RadioStats.RxFailCorruptData;
root["radio_stats"]["rssi"] = inv->getLastRssi();
} }
void WebApiWsLiveClass::generateInverterChannelJsonResponse(JsonObject& root, std::shared_ptr<InverterAbstract> inv) void WebApiWsLiveClass::generateInverterChannelJsonResponse(JsonObject& root, std::shared_ptr<InverterAbstract> inv)

View File

@ -5,6 +5,7 @@
#include "Configuration.h" #include "Configuration.h"
#include "Datastore.h" #include "Datastore.h"
#include "Display_Graphic.h" #include "Display_Graphic.h"
#include "I18n.h"
#include "InverterSettings.h" #include "InverterSettings.h"
#include "Led_Single.h" #include "Led_Single.h"
#include "MessageOutput.h" #include "MessageOutput.h"
@ -24,11 +25,9 @@
#include "defaults.h" #include "defaults.h"
#include <Arduino.h> #include <Arduino.h>
#include <LittleFS.h> #include <LittleFS.h>
#include <SpiManager.h>
#include <TaskScheduler.h> #include <TaskScheduler.h>
#include <esp_heap_caps.h> #include <esp_heap_caps.h>
#include <SpiManager.h>
#include <driver/uart.h>
void setup() void setup()
{ {
@ -43,10 +42,8 @@ void setup()
// Initialize serial output // Initialize serial output
Serial.begin(SERIAL_BAUDRATE); Serial.begin(SERIAL_BAUDRATE);
#if ARDUINO_USB_CDC_ON_BOOT #if !ARDUINO_USB_CDC_ON_BOOT
Serial.setTxTimeoutMs(0); // Only wait for serial interface to be set up when not using CDC
delay(100);
#else
while (!Serial) while (!Serial)
yield(); yield();
#endif #endif
@ -68,10 +65,9 @@ void setup()
} }
// Read configuration values // Read configuration values
Configuration.init(scheduler);
MessageOutput.print("Reading configuration... "); MessageOutput.print("Reading configuration... ");
if (!Configuration.read()) { if (!Configuration.read()) {
MessageOutput.print("initializing... ");
Configuration.init();
if (Configuration.write()) { if (Configuration.write()) {
MessageOutput.print("written... "); MessageOutput.print("written... ");
} else { } else {
@ -85,6 +81,11 @@ void setup()
auto& config = Configuration.get(); auto& config = Configuration.get();
MessageOutput.println("done"); MessageOutput.println("done");
// Read languate pack
MessageOutput.print("Reading language pack... ");
I18n.init(scheduler);
MessageOutput.println("done");
// Load PinMapping // Load PinMapping
MessageOutput.print("Reading PinMapping... "); MessageOutput.print("Reading PinMapping... ");
if (PinMapping.init(String(Configuration.get().Dev_PinMapping))) { if (PinMapping.init(String(Configuration.get().Dev_PinMapping))) {
@ -139,7 +140,7 @@ void setup()
Display.enablePowerSafe = config.Display.PowerSafe; Display.enablePowerSafe = config.Display.PowerSafe;
Display.enableScreensaver = config.Display.ScreenSaver; Display.enableScreensaver = config.Display.ScreenSaver;
Display.setContrast(config.Display.Contrast); Display.setContrast(config.Display.Contrast);
Display.setLanguage(config.Display.Language); Display.setLocale(config.Display.Locale);
Display.setStartupDisplay(); Display.setStartupDisplay();
MessageOutput.println("done"); MessageOutput.println("done");
@ -148,19 +149,6 @@ void setup()
LedSingle.init(scheduler); LedSingle.init(scheduler);
MessageOutput.println("done"); MessageOutput.println("done");
// Check for default DTU serial
MessageOutput.print("Check for default DTU serial... ");
if (config.Dtu.Serial == DTU_SERIAL) {
MessageOutput.print("generate serial based on ESP chip id: ");
const uint64_t dtuId = Utils::generateDtuSerial();
MessageOutput.printf("%0x%08x... ",
((uint32_t)((dtuId >> 32) & 0xFFFFFFFF)),
((uint32_t)(dtuId & 0xFFFFFFFF)));
config.Dtu.Serial = dtuId;
Configuration.write();
}
MessageOutput.println("done");
InverterSettings.init(scheduler); InverterSettings.init(scheduler);
Datastore.init(scheduler); Datastore.init(scheduler);

2
webapp/env.d.ts vendored
View File

@ -1,7 +1,7 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
import { Router, Route } from 'vue-router' import { Router, Route } from 'vue-router'
declare module '@vue/runtime-core' { declare module 'vue' {
interface ComponentCustomProperties { interface ComponentCustomProperties {
$router: Router $router: Router
$route: Route $route: Route

View File

@ -1,22 +1,12 @@
/* eslint-env node */ /* eslint-env node */
import path from "node:path";
import { fileURLToPath } from "node:url";
import { FlatCompat } from "@eslint/eslintrc";
import js from "@eslint/js"; import js from "@eslint/js";
import pluginVue from 'eslint-plugin-vue' import pluginVue from 'eslint-plugin-vue'
import vueTsEslintConfig from '@vue/eslint-config-typescript'
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
});
export default [ export default [
js.configs.recommended, js.configs.recommended,
...pluginVue.configs['flat/essential'], ...pluginVue.configs['flat/essential'],
...compat.extends("@vue/eslint-config-typescript/recommended"), ...vueTsEslintConfig(),
{ {
files: [ files: [
"**/*.vue", "**/*.vue",

View File

@ -19,33 +19,33 @@
"mitt": "^3.0.1", "mitt": "^3.0.1",
"sortablejs": "^1.15.3", "sortablejs": "^1.15.3",
"spark-md5": "^3.0.2", "spark-md5": "^3.0.2",
"vue": "^3.5.10", "vue": "^3.5.12",
"vue-i18n": "9.13.1", "vue-i18n": "10.0.4",
"vue-router": "^4.4.5" "vue-router": "^4.4.5"
}, },
"devDependencies": { "devDependencies": {
"@intlify/unplugin-vue-i18n": "^4.0.0", "@intlify/unplugin-vue-i18n": "^5.2.0",
"@tsconfig/node22": "^22.0.0", "@tsconfig/node22": "^22.0.0",
"@types/bootstrap": "^5.2.10", "@types/bootstrap": "^5.2.10",
"@types/node": "^22.7.4", "@types/node": "^22.9.0",
"@types/pulltorefreshjs": "^0.1.7", "@types/pulltorefreshjs": "^0.1.7",
"@types/sortablejs": "^1.15.8", "@types/sortablejs": "^1.15.8",
"@types/spark-md5": "^3.0.4", "@types/spark-md5": "^3.0.5",
"@vitejs/plugin-vue": "^5.1.4", "@vitejs/plugin-vue": "^5.1.4",
"@vue/eslint-config-typescript": "^13.0.0", "@vue/eslint-config-typescript": "^14.1.3",
"@vue/tsconfig": "^0.5.1", "@vue/tsconfig": "^0.5.1",
"eslint": "^9.11.1", "eslint": "^9.14.0",
"eslint-plugin-vue": "^9.28.0", "eslint-plugin-vue": "^9.30.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"pulltorefreshjs": "^0.1.22", "pulltorefreshjs": "^0.1.22",
"sass": "^1.77.6", "sass": "=1.77.6",
"terser": "^5.34.0", "terser": "^5.36.0",
"typescript": "^5.6.2", "typescript": "^5.6.3",
"vite": "^5.4.8", "vite": "^5.4.10",
"vite-plugin-compression": "^0.5.1", "vite-plugin-compression": "^0.5.1",
"vite-plugin-css-injected-by-js": "^3.5.2", "vite-plugin-css-injected-by-js": "^3.5.2",
"vue-tsc": "^2.1.6" "vue-tsc": "^2.1.10"
}, },
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
} }

View File

@ -18,7 +18,7 @@ export default defineComponent({
<style> <style>
/* Show it is fixed to the top */ /* Show it is fixed to the top */
body { body {
min-height: 75rem; padding-bottom: 1rem;
padding-top: 4.5rem; padding-top: 4.5rem;
} }
</style> </style>

View File

@ -1,5 +1,5 @@
<template> <template>
<div :class="['card', addSpace ? 'mt-5' : '']"> <div :class="['card', table ? 'card-table' : '', addSpace ? 'mt-5' : '']">
<div :class="['card-header', textVariant]">{{ text }}</div> <div :class="['card-header', textVariant]">{{ text }}</div>
<div :class="['card-body', 'card-text', centerContent ? 'text-center' : '']"> <div :class="['card-body', 'card-text', centerContent ? 'text-center' : '']">
<slot /> <slot />
@ -14,6 +14,7 @@ export default defineComponent({
props: { props: {
text: String, text: String,
textVariant: String, textVariant: String,
table: Boolean,
addSpace: Boolean, addSpace: Boolean,
centerContent: Boolean, centerContent: Boolean,
}, },

View File

@ -1,5 +1,5 @@
<template> <template>
<CardElement :text="$t('firmwareinfo.FirmwareInformation')" textVariant="text-bg-primary"> <CardElement :text="$t('firmwareinfo.FirmwareInformation')" textVariant="text-bg-primary" table>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover table-condensed"> <table class="table table-hover table-condensed">
<tbody> <tbody>

View File

@ -7,3 +7,9 @@
<button type="submit" class="btn btn-primary">{{ $t('base.Save') }}</button> <button type="submit" class="btn btn-primary">{{ $t('base.Save') }}</button>
</div> </div>
</template> </template>
<script lang="ts">
export default {
emits: ['reload'],
};
</script>

View File

@ -19,7 +19,11 @@
</table> </table>
<div class="accordion" id="accordionProfile"> <div class="accordion" id="accordionProfile">
<div class="accordion-item" v-for="(section, index) in gridProfileList.sections" :key="index"> <div
class="accordion-item accordion-table"
v-for="(section, index) in gridProfileList.sections"
:key="index"
>
<h2 class="accordion-header"> <h2 class="accordion-header">
<button <button
class="accordion-button collapsed" class="accordion-button collapsed"

View File

@ -1,5 +1,5 @@
<template> <template>
<CardElement :text="$t('hardwareinfo.HardwareInformation')" textVariant="text-bg-primary"> <CardElement :text="$t('hardwareinfo.HardwareInformation')" textVariant="text-bg-primary" table>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover table-condensed"> <table class="table table-hover table-condensed">
<tbody> <tbody>

View File

@ -1,5 +1,5 @@
<template> <template>
<CardElement :text="$t('heapdetails.HeapDetails')" textVariant="text-bg-primary"> <CardElement :text="$t('heapdetails.HeapDetails')" textVariant="text-bg-primary" table>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover table-condensed"> <table class="table table-hover table-condensed">
<tbody> <tbody>

View File

@ -1,5 +1,5 @@
<template> <template>
<CardElement :text="$t('interfaceapinfo.NetworkInterface')" textVariant="text-bg-primary"> <CardElement :text="$t('interfaceapinfo.NetworkInterface')" textVariant="text-bg-primary" table>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover table-condensed"> <table class="table table-hover table-condensed">
<tbody> <tbody>

View File

@ -2,6 +2,7 @@
<CardElement <CardElement
:text="$t('interfacenetworkinfo.NetworkInterface', { iface: networkStatus.network_mode })" :text="$t('interfacenetworkinfo.NetworkInterface', { iface: networkStatus.network_mode })"
textVariant="text-bg-primary" textVariant="text-bg-primary"
table
> >
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover table-condensed"> <table class="table table-hover table-condensed">

View File

@ -1,11 +1,10 @@
<template> <template>
<div <div
class="card" class="card card-table"
:class="{ :class="{
'border-info': channelType == 'AC', 'border-info': channelType == 'AC',
'border-secondary': channelType == 'INV', 'border-secondary': channelType == 'INV',
}" }"
style="overflow: hidden"
> >
<div v-if="channelType == 'INV'" class="card-header text-bg-secondary"> <div v-if="channelType == 'INV'" class="card-header text-bg-secondary">
{{ $t('inverterchannelinfo.General') }} {{ $t('inverterchannelinfo.General') }}
@ -21,12 +20,12 @@
</div> </div>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped table-hover" style="margin: 0"> <table class="table table-striped table-hover">
<tbody> <tbody>
<tr v-for="(property, key) in channelData" :key="`prop-${key}`"> <tr v-for="(property, key) in channelData" :key="`prop-${key}`">
<template v-if="key != 'name' && property"> <template v-if="key != 'name' && property">
<th scope="row">{{ $t('inverterchannelproperty.' + key) }}</th> <th scope="row">{{ $t('inverterchannelproperty.' + key) }}</th>
<td style="text-align: right; padding-right: 0"> <td class="value">
{{ {{
$n(property.v, 'decimal', { $n(property.v, 'decimal', {
minimumFractionDigits: property.d, minimumFractionDigits: property.d,

View File

@ -1,31 +1,24 @@
<template> <template>
<select class="form-select" @change="updateLanguage()" v-model="$i18n.locale"> <select class="form-select" @change="setLocale(($event.target as HTMLSelectElement).value)" :value="$i18n.locale">
<option v-for="locale in $i18n.availableLocales" :key="`locale-${locale}`" :value="locale"> <option v-for="locale in allLocales" :key="`locale-${locale.code}`" :value="locale.code">
{{ getLocaleName(locale) }} {{ locale.name }}
</option> </option>
</select> </select>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { LOCALES } from '@/locales'; import { allLocales, setLocale } from '@/i18n';
export default defineComponent({ export default defineComponent({
name: 'LocaleSwitcher', name: 'LocaleSwitcher',
data() {
return {
allLocales,
};
},
methods: { methods: {
updateLanguage() { setLocale,
localStorage.setItem('locale', this.$i18n.locale);
},
getLocaleName(locale: string): string {
return LOCALES.find((i) => i.value === locale)?.caption || '';
},
},
mounted() {
if (localStorage.getItem('locale')) {
this.$i18n.locale = localStorage.getItem('locale') || 'en';
} else {
localStorage.setItem('locale', this.$i18n.locale);
}
}, },
}); });
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<CardElement :text="$t('memoryinfo.MemoryInformation')" textVariant="text-bg-primary"> <CardElement :text="$t('memoryinfo.MemoryInformation')" textVariant="text-bg-primary" table>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover table-condensed"> <table class="table table-hover table-condensed">
<thead> <thead>

View File

@ -216,7 +216,9 @@ export default defineComponent({
this.$router.push('/'); this.$router.push('/');
}, },
onClick() { onClick() {
this.$refs.navbarCollapse && (this.$refs.navbarCollapse as HTMLElement).classList.remove('show'); if (this.$refs.navbarCollapse) {
(this.$refs.navbarCollapse as HTMLElement).classList.remove('show');
}
}, },
getEasterSunday(year: number): Date { getEasterSunday(year: number): Date {
const f = Math.floor; const f = Math.floor;

View File

@ -1,40 +1,36 @@
<template> <template>
<CardElement :text="$t('pininfo.PinOverview')" textVariant="text-bg-primary"> <div class="row flex-row flex-wrap g-3">
<div class="col" v-for="category in categories" :key="category">
<CardElement :text="capitalizeFirstLetter(category)" textVariant="text-bg-primary" table>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover table-condensed"> <table class="table table-hover table-condensed">
<thead>
<tr>
<th>{{ $t('pininfo.Category') }}</th>
<th>{{ $t('pininfo.Name') }}</th>
<th>{{ $t('pininfo.ValueSelected') }}</th>
<th>{{ $t('pininfo.ValueActive') }}</th>
</tr>
</thead>
<tbody> <tbody>
<template v-for="category in categories" :key="category"> <tr>
<tr v-for="(prop, prop_idx) in properties(category)" :key="prop"> <th>{{ $t('pininfo.Name') }}</th>
<td v-if="prop_idx == 0" :rowspan="properties(category).length"> <th class="text-center">{{ $t('pininfo.ValueSelected') }}</th>
{{ capitalizeFirstLetter(category) }} <th class="text-center">{{ $t('pininfo.ValueActive') }}</th>
</td> </tr>
<tr v-for="(prop, prop_idx) in properties(category)" :key="prop_idx">
<td :class="{ 'table-danger': !isEqual(category, prop) }"> <td :class="{ 'table-danger': !isEqual(category, prop) }">
{{ prop }} {{ prop }}
</td> </td>
<td> <td class="text-center">
<template v-if="selectedPinAssignment && category in selectedPinAssignment"> <template v-if="selectedPinAssignment && category in selectedPinAssignment">
{{ (selectedPinAssignment as any)[category][prop] }}</template {{ (selectedPinAssignment as any)[category][prop] }}</template
> >
</td> </td>
<td> <td class="text-center">
<template v-if="currentPinAssignment && category in currentPinAssignment"> <template v-if="currentPinAssignment && category in currentPinAssignment">
{{ (currentPinAssignment as any)[category][prop] }}</template {{ (currentPinAssignment as any)[category][prop] }}</template
> >
</td> </td>
</tr> </tr>
</template>
</tbody> </tbody>
</table> </table>
</div> </div>
</CardElement> </CardElement>
</div>
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -60,6 +56,7 @@ export default defineComponent({
let selArray: Array<string> = []; let selArray: Array<string> = [];
if (this.selectedPinAssignment) { if (this.selectedPinAssignment) {
selArray = Object.keys(this.selectedPinAssignment as Device); selArray = Object.keys(this.selectedPinAssignment as Device);
selArray = selArray.filter((item) => curArray.includes(item));
} }
let total: Array<string> = []; let total: Array<string> = [];

View File

@ -1,5 +1,5 @@
<template> <template>
<CardElement :text="$t('radioinfo.RadioInformation')" textVariant="text-bg-primary"> <CardElement :text="$t('radioinfo.RadioInformation')" textVariant="text-bg-primary" table>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover table-condensed"> <table class="table table-hover table-condensed">
<tbody> <tbody>

View File

@ -0,0 +1,40 @@
<template>
<CardElement :text="$t('taskdetails.TaskDetails')" textVariant="text-bg-primary" table>
<div class="table-responsive">
<table class="table table-hover table-condensed">
<tbody>
<tr>
<th>{{ $t('taskdetails.Name') }}</th>
<th>{{ $t('taskdetails.StackFree') }}</th>
<th>{{ $t('taskdetails.Priority') }}</th>
</tr>
<tr v-for="task in taskDetails" v-bind:key="task.name">
<td>{{ $te(taskLangToken(task.name)) ? $t(taskLangToken(task.name)) : task.name }}</td>
<td>{{ $n(task.stack_watermark, 'byte') }}</td>
<td>{{ task.priority }}</td>
</tr>
</tbody>
</table>
</div>
</CardElement>
</template>
<script lang="ts">
import CardElement from '@/components/CardElement.vue';
import type { TaskDetail } from '@/types/SystemStatus';
import { defineComponent, type PropType } from 'vue';
export default defineComponent({
components: {
CardElement,
},
props: {
taskDetails: { type: Array as PropType<TaskDetail[]>, required: true },
},
methods: {
taskLangToken(rawTask: string) {
return 'taskdetails.Task_' + rawTask.replace(/[^A-Za-z0-9]/g, '').toLowerCase();
},
},
});
</script>

View File

@ -1,5 +1,5 @@
<template> <template>
<CardElement :text="$t('wifiapinfo.WifiApInfo')" textVariant="text-bg-primary"> <CardElement :text="$t('wifiapinfo.WifiApInfo')" textVariant="text-bg-primary" table>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover table-condensed"> <table class="table table-hover table-condensed">
<tbody> <tbody>

View File

@ -1,5 +1,5 @@
<template> <template>
<CardElement :text="$t('wifistationinfo.WifiStationInfo')" textVariant="text-bg-primary"> <CardElement :text="$t('wifistationinfo.WifiStationInfo')" textVariant="text-bg-primary" table>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover table-condensed"> <table class="table table-hover table-condensed">
<tbody> <tbody>

View File

@ -1,4 +1,4 @@
declare module '@vue/runtime-core' { declare module 'vue' {
interface ComponentCustomProperties { interface ComponentCustomProperties {
$emitter: Emitter; $emitter: Emitter;
} }

145
webapp/src/i18n.ts Normal file
View File

@ -0,0 +1,145 @@
import { createI18n } from 'vue-i18n';
import messages from '@intlify/unplugin-vue-i18n/messages';
import type { I18nOptions, IntlDateTimeFormat, IntlNumberFormat } from 'vue-i18n';
export const allLocales = [
{ code: 'en', name: 'English' },
{ code: 'de', name: 'Deutsch' },
{ code: 'fr', name: 'Français' },
];
const dateTimeFormatsTemplate: IntlDateTimeFormat = {
datetime: {
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour12: false,
},
};
const numberFormatTemplate: IntlNumberFormat = {
decimal: {
style: 'decimal',
},
decimalNoDigits: {
style: 'decimal',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
},
decimalOneDigit: {
style: 'decimal',
minimumFractionDigits: 1,
maximumFractionDigits: 1,
},
decimalTwoDigits: {
style: 'decimal',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
},
percent: {
style: 'percent',
},
percentOneDigit: {
style: 'percent',
minimumFractionDigits: 1,
maximumFractionDigits: 1,
},
byte: {
style: 'unit',
unit: 'byte',
},
kilobyte: {
style: 'unit',
unit: 'kilobyte',
},
megabyte: {
style: 'unit',
unit: 'megabyte',
},
celsius: {
style: 'unit',
unit: 'celsius',
maximumFractionDigits: 1,
},
};
export const dateTimeFormats: I18nOptions['datetimeFormats'] = {};
export const numberFormats: I18nOptions['numberFormats'] = {};
allLocales.forEach((locale) => {
dateTimeFormats[locale.code] = dateTimeFormatsTemplate;
numberFormats[locale.code] = numberFormatTemplate;
});
export const i18n = createI18n({
legacy: false,
globalInjection: true,
locale: navigator.language.split('-')[0],
fallbackLocale: allLocales[0].code,
messages,
datetimeFormats: dateTimeFormats,
numberFormats: numberFormats,
});
const dynamicLocales = await loadAvailLocales();
allLocales.push(...dynamicLocales);
if (localStorage.getItem('locale')) {
setLocale(localStorage.getItem('locale') || 'en');
} else {
localStorage.setItem('locale', i18n.global.locale.value);
}
// Set new locale.
export async function setLocale(locale: string) {
// Load locale if not available yet.
if (!i18n.global.availableLocales.includes(locale)) {
const messages = await loadLocale(locale);
// fetch() error occurred.
if (messages === undefined) {
i18n.global.locale.value = allLocales[0].code;
return;
}
// Add locale.
i18n.global.setLocaleMessage(locale, messages.webapp);
i18n.global.setNumberFormat(locale, numberFormatTemplate);
i18n.global.setDateTimeFormat(locale, dateTimeFormatsTemplate);
}
// Set locale.
i18n.global.locale.value = locale;
localStorage.setItem('locale', i18n.global.locale.value);
}
// Fetch locale.
async function loadLocale(locale: string) {
return fetch(`/api/i18n/language?code=${locale}`)
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error('Something went wrong!');
})
.catch((error) => {
console.error(error);
});
}
// Fetch available locales
async function loadAvailLocales() {
return fetch('/api/i18n/languages')
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error('Something went wrong!');
})
.catch((error) => {
console.error(error);
});
}

View File

@ -32,6 +32,10 @@
"Release": "Loslassen zum Aktualisieren", "Release": "Loslassen zum Aktualisieren",
"Close": "Schließen" "Close": "Schließen"
}, },
"wait": {
"NotReady": "OpenDTU ist noch nicht bereit",
"PleaseWait": "Bitte warten. Sie werden automatisch auf die Startseite weitergeleitet."
},
"Error": { "Error": {
"Oops": "Oops!" "Oops": "Oops!"
}, },
@ -54,6 +58,7 @@
"2005": "Ungültige Landesauswahl!", "2005": "Ungültige Landesauswahl!",
"3001": "Nichts gelöscht!", "3001": "Nichts gelöscht!",
"3002": "Konfiguration zurückgesetzt. Starte jetzt neu...", "3002": "Konfiguration zurückgesetzt. Starte jetzt neu...",
"3003": "Datei erfolgreich gelöscht. Neustarten um Änderungen anzuwenden!",
"4001": "@:apiresponse.2001", "4001": "@:apiresponse.2001",
"4002": "Der Name muss zwischen 1 und {max} Zeichen lang sein!", "4002": "Der Name muss zwischen 1 und {max} Zeichen lang sein!",
"4003": "Es werden nur {max} Wechselrichter unterstützt!", "4003": "Es werden nur {max} Wechselrichter unterstützt!",
@ -150,7 +155,10 @@
"RxFailCorrupt": "Empfang Fehler: Beschädigt empfangen", "RxFailCorrupt": "Empfang Fehler: Beschädigt empfangen",
"TxReRequest": "Gesendete Fragment Wiederanforderungen", "TxReRequest": "Gesendete Fragment Wiederanforderungen",
"StatsReset": "Statistiken zurücksetzen", "StatsReset": "Statistiken zurücksetzen",
"StatsResetting": "Zurücksetzen..." "StatsResetting": "Zurücksetzen...",
"Rssi": "RSSI des zuletzt empfangenen Paketes",
"RssiHint": "HM-Wechselrichter unterstützen nur RSSI-Werte < -64 dBm und > -64 dBm. In diesem Fall wird -80 dBm und -30 dBm angezeigt.",
"dBm": "{dbm} dBm"
}, },
"eventlog": { "eventlog": {
"Start": "Beginn", "Start": "Beginn",
@ -235,6 +243,24 @@
"MaxUsage": "Maximale Speichernutzung seit Start", "MaxUsage": "Maximale Speichernutzung seit Start",
"Fragmentation": "Grad der Fragmentierung" "Fragmentation": "Grad der Fragmentierung"
}, },
"taskdetails": {
"TaskDetails": "Detailinformationen zu Tasks",
"Name": "Name",
"StackFree": "Stack Frei",
"Priority": "Priorität",
"Task_idle0": "Leerlauf (CPU-Kern 0)",
"Task_idle1": "Leerlauf (CPU-Kern 1)",
"Task_wifi": "Wi-Fi",
"Task_tit": "TCP/IP",
"Task_looptask": "Arduino Hauptschleife (loop)",
"Task_asynctcp": "Async TCP",
"Task_mqttclient": "MQTT Client",
"Task_huaweican0": "AC Ladegerät CAN",
"Task_pmsdm": "Stromzähler (SDM)",
"Task_pmhttpjson": "Stromzähler (HTTP+JSON)",
"Task_pmsml": "Stromzähler (Serial SML)",
"Task_pmhttpsml": "Stromzähler (HTTP+SML)"
},
"radioinfo": { "radioinfo": {
"RadioInformation": "Funkmodulinformationen", "RadioInformation": "Funkmodulinformationen",
"Status": "{module} Status", "Status": "{module} Status",
@ -369,20 +395,20 @@
"dtuadmin": { "dtuadmin": {
"DtuSettings": "DTU-Einstellungen", "DtuSettings": "DTU-Einstellungen",
"DtuConfiguration": "DTU-Konfiguration", "DtuConfiguration": "DTU-Konfiguration",
"Serial": "Seriennummer:", "Serial": "Seriennummer",
"SerialHint": "Sowohl der Wechselrichter als auch die DTU haben eine Seriennummer. Die DTU-Seriennummer wird beim ersten Start zufällig generiert und muss normalerweise nicht geändert werden.", "SerialHint": "Sowohl der Wechselrichter als auch die DTU haben eine Seriennummer. Die DTU-Seriennummer wird beim ersten Start zufällig generiert und muss normalerweise nicht geändert werden.",
"PollInterval": "Abfrageintervall:", "PollInterval": "Abfrageintervall",
"Seconds": "Sekunden", "Seconds": "Sekunden",
"NrfPaLevel": "NRF24 Sendeleistung:", "NrfPaLevel": "NRF24 Sendeleistung",
"CmtPaLevel": "CMT2300A Sendeleistung:", "CmtPaLevel": "CMT2300A Sendeleistung",
"NrfPaLevelHint": "Verwendet für HM-Wechselrichter. Stellen Sie sicher, dass Ihre Stromversorgung stabil genug ist, bevor Sie die Sendeleistung erhöhen.", "NrfPaLevelHint": "Verwendet für HM-Wechselrichter. Stellen Sie sicher, dass Ihre Stromversorgung stabil genug ist, bevor Sie die Sendeleistung erhöhen.",
"CmtPaLevelHint": "Verwendet für HMS/HMT-Wechselrichter. Stellen Sie sicher, dass Ihre Stromversorgung stabil genug ist, bevor Sie die Sendeleistung erhöhen.", "CmtPaLevelHint": "Verwendet für HMS/HMT-Wechselrichter. Stellen Sie sicher, dass Ihre Stromversorgung stabil genug ist, bevor Sie die Sendeleistung erhöhen.",
"CmtCountry": "CMT2300A Region/Land:", "CmtCountry": "CMT2300A Region/Land",
"CmtCountryHint": "Jedes Land hat unterschiedliche Frequenzzuteilungen.", "CmtCountryHint": "Jedes Land hat unterschiedliche Frequenzzuteilungen.",
"country_0": "Europa ({min}MHz - {max}MHz)", "country_0": "Europa ({min}MHz - {max}MHz)",
"country_1": "Nordamerika ({min}MHz - {max}MHz)", "country_1": "Nordamerika ({min}MHz - {max}MHz)",
"country_2": "Brasilien ({min}MHz - {max}MHz)", "country_2": "Brasilien ({min}MHz - {max}MHz)",
"CmtFrequency": "CMT2300A Frequenz:", "CmtFrequency": "CMT2300A Frequenz",
"CmtFrequencyHint": "Stelle sicher, dass nur Frequenzen verwendet werden, welche im entsprechenden Land erlaubt sind! Nach einer Frequenzänderung kann es bis zu 15min dauern bis eine Verbindung hergestellt wird.", "CmtFrequencyHint": "Stelle sicher, dass nur Frequenzen verwendet werden, welche im entsprechenden Land erlaubt sind! Nach einer Frequenzänderung kann es bis zu 15min dauern bis eine Verbindung hergestellt wird.",
"CmtFrequencyWarning": "Die gewählte Frequenz liegt außerhalb des zulässigen Bereichs in der gewählten Region/dem Land. Vergewissere dich, dass mit dieser Auswahl keine lokalen Regularien verletzt werden.", "CmtFrequencyWarning": "Die gewählte Frequenz liegt außerhalb des zulässigen Bereichs in der gewählten Region/dem Land. Vergewissere dich, dass mit dieser Auswahl keine lokalen Regularien verletzt werden.",
"MHz": "{mhz} MHz", "MHz": "{mhz} MHz",
@ -395,8 +421,8 @@
"securityadmin": { "securityadmin": {
"SecuritySettings": "Sicherheitseinstellungen", "SecuritySettings": "Sicherheitseinstellungen",
"AdminPassword": "Administrator-Passwort", "AdminPassword": "Administrator-Passwort",
"Password": "Passwort:", "Password": "Passwort",
"RepeatPassword": "Passwort wiederholen:", "RepeatPassword": "Passwort wiederholen",
"PasswordHint": "<b>Hinweis:</b> Das Administrator-Passwort wird für den Zugriff auf die Webschnittstelle (Benutzer 'admin'), aber auch für die Verbindung mit dem Gerät im AP-Modus verwendet. Es muss zwischen 8 und 64 Zeichen lang sein.", "PasswordHint": "<b>Hinweis:</b> Das Administrator-Passwort wird für den Zugriff auf die Webschnittstelle (Benutzer 'admin'), aber auch für die Verbindung mit dem Gerät im AP-Modus verwendet. Es muss zwischen 8 und 64 Zeichen lang sein.",
"Permissions": "Berechtigungen", "Permissions": "Berechtigungen",
"ReadOnly": "Nur-Lese-Zugriff auf die Weboberfläche ohne Passwort zulassen" "ReadOnly": "Nur-Lese-Zugriff auf die Weboberfläche ohne Passwort zulassen"
@ -404,40 +430,40 @@
"ntpadmin": { "ntpadmin": {
"NtpSettings": "NTP-Einstellungen", "NtpSettings": "NTP-Einstellungen",
"NtpConfiguration": "NTP-Konfiguration", "NtpConfiguration": "NTP-Konfiguration",
"TimeServer": "Zeitserver:", "TimeServer": "Zeitserver",
"TimeServerHint": "Der Standardwert ist in Ordnung, solange OpenDTU direkten Zugang zum Internet hat.", "TimeServerHint": "Der Standardwert ist in Ordnung, solange OpenDTU direkten Zugang zum Internet hat.",
"Timezone": "Zeitzone:", "Timezone": "Zeitzone",
"TimezoneConfig": "Zeitzonenkonfiguration:", "TimezoneConfig": "Zeitzonenkonfiguration",
"LocationConfiguration": "Standortkonfiguration", "LocationConfiguration": "Standortkonfiguration",
"Longitude": "Längengrad:", "Longitude": "Längengrad",
"Latitude": "Breitengrad:", "Latitude": "Breitengrad",
"SunSetType": "Dämmerungstyp:", "SunSetType": "Dämmerungstyp",
"SunSetTypeHint": "Beeinflusst die Tag/Nacht Berechnung. Es kann bis zu einer Minute dauern bis der neue Typ angewendet wurde.", "SunSetTypeHint": "Beeinflusst die Tag/Nacht Berechnung. Es kann bis zu einer Minute dauern bis der neue Typ angewendet wurde.",
"OFFICIAL": "Standard Dämmerung (90.8°)", "OFFICIAL": "Standard Dämmerung (90.8°)",
"NAUTICAL": "Nautische Dämmerung (102°)", "NAUTICAL": "Nautische Dämmerung (102°)",
"CIVIL": "Bürgerliche Dämmerung (96°)", "CIVIL": "Bürgerliche Dämmerung (96°)",
"ASTONOMICAL": "Astronomische Dämmerung (108°)", "ASTONOMICAL": "Astronomische Dämmerung (108°)",
"ManualTimeSynchronization": "Manuelle Zeitsynchronisation", "ManualTimeSynchronization": "Manuelle Zeitsynchronisation",
"CurrentOpenDtuTime": "Aktuelle OpenDTU-Zeit:", "CurrentOpenDtuTime": "Aktuelle OpenDTU-Zeit",
"CurrentLocalTime": "Aktuelle lokale Zeit:", "CurrentLocalTime": "Aktuelle lokale Zeit",
"SynchronizeTime": "Zeit synchronisieren", "SynchronizeTime": "Zeit synchronisieren",
"SynchronizeTimeHint": "<b>Hinweis:</b> Sie können die manuelle Zeitsynchronisation verwenden, um die aktuelle Zeit von OpenDTU einzustellen, wenn kein NTP-Server verfügbar ist. Beachten Sie aber, dass im Falle eines Stromausfalls die Zeit verloren geht. Beachten Sie auch, dass die Zeitgenauigkeit stark verzerrt wird, da sie nicht regelmäßig neu synchronisiert werden kann und der ESP32-Mikrocontroller nicht über eine Echtzeituhr verfügt." "SynchronizeTimeHint": "<b>Hinweis:</b> Sie können die manuelle Zeitsynchronisation verwenden, um die aktuelle Zeit von OpenDTU einzustellen, wenn kein NTP-Server verfügbar ist. Beachten Sie aber, dass im Falle eines Stromausfalls die Zeit verloren geht. Beachten Sie auch, dass die Zeitgenauigkeit stark verzerrt wird, da sie nicht regelmäßig neu synchronisiert werden kann und der ESP32-Mikrocontroller nicht über eine Echtzeituhr verfügt."
}, },
"networkadmin": { "networkadmin": {
"NetworkSettings": "Netzwerkeinstellungen", "NetworkSettings": "Netzwerkeinstellungen",
"WifiConfiguration": "WLAN-Konfiguration", "WifiConfiguration": "WLAN-Konfiguration",
"WifiSsid": "WLAN-SSID:", "WifiSsid": "WLAN-SSID",
"WifiPassword": "WLAN-Passwort:", "WifiPassword": "WLAN-Passwort",
"Hostname": "Hostname:", "Hostname": "Hostname",
"HostnameHint": "<b>Hinweis:</b> Der Text <span class=\"font-monospace\">%06X</span> wird durch die letzten 6 Ziffern der ESP-ChipID im Hex-Format ersetzt.", "HostnameHint": "<b>Hinweis:</b> Der Text <span class=\"font-monospace\">%06X</span> wird durch die letzten 6 Ziffern der ESP-ChipID im Hex-Format ersetzt.",
"EnableDhcp": "DHCP aktivieren", "EnableDhcp": "DHCP aktivieren",
"StaticIpConfiguration": "Statische IP-Konfiguration", "StaticIpConfiguration": "Statische IP-Konfiguration",
"IpAddress": "IP-Adresse:", "IpAddress": "IP-Adresse",
"Netmask": "Netzmaske:", "Netmask": "Netzmaske",
"DefaultGateway": "Standardgateway:", "DefaultGateway": "Standardgateway",
"Dns": "DNS-Server {num}:", "Dns": "DNS-Server {num}",
"AdminAp": "WLAN-Konfiguration (Admin AccessPoint)", "AdminAp": "WLAN-Konfiguration (Admin AccessPoint)",
"ApTimeout": "AccessPoint Zeitlimit:", "ApTimeout": "AccessPoint Zeitlimit",
"ApTimeoutHint": "Zeit die der AccessPoint offen gehalten wird. Ein Wert von 0 bedeutet unendlich.", "ApTimeoutHint": "Zeit die der AccessPoint offen gehalten wird. Ein Wert von 0 bedeutet unendlich.",
"Minutes": "Minuten", "Minutes": "Minuten",
"EnableMdns": "mDNS aktivieren", "EnableMdns": "mDNS aktivieren",
@ -449,38 +475,38 @@
"EnableMqtt": "MQTT aktivieren", "EnableMqtt": "MQTT aktivieren",
"EnableHass": "Home Assistant MQTT-Auto-Discovery aktivieren", "EnableHass": "Home Assistant MQTT-Auto-Discovery aktivieren",
"MqttBrokerParameter": "MQTT-Broker-Parameter", "MqttBrokerParameter": "MQTT-Broker-Parameter",
"Hostname": "Hostname:", "Hostname": "Hostname",
"HostnameHint": "Hostname oder IP-Adresse", "HostnameHint": "Hostname oder IP-Adresse",
"Port": "Port:", "Port": "Port",
"ClientId": "Client ID:", "ClientId": "Client ID",
"Username": "Benutzername:", "Username": "Benutzername",
"UsernameHint": "Benutzername, leer lassen für anonyme Verbindung", "UsernameHint": "Benutzername, leer lassen für anonyme Verbindung",
"Password": "Passwort:", "Password": "Passwort",
"PasswordHint": "Passwort, leer lassen für anonyme Verbindung", "PasswordHint": "Passwort, leer lassen für anonyme Verbindung",
"BaseTopic": "Basis-Topic:", "BaseTopic": "Basis-Topic",
"BaseTopicHint": "Basis-Topic, wird allen veröffentlichten Themen vorangestellt (z.B. inverter/)", "BaseTopicHint": "Basis-Topic, wird allen veröffentlichten Themen vorangestellt (z.B. inverter/)",
"PublishInterval": "Veröffentlichungsintervall:", "PublishInterval": "Veröffentlichungsintervall",
"Seconds": "Sekunden", "Seconds": "Sekunden",
"CleanSession": "CleanSession Flag aktivieren", "CleanSession": "CleanSession Flag aktivieren",
"EnableRetain": "Retain Flag aktivieren", "EnableRetain": "Retain Flag aktivieren",
"EnableTls": "TLS aktivieren", "EnableTls": "TLS aktivieren",
"RootCa": "CA-Root-Zertifikat (Standard Letsencrypt):", "RootCa": "CA-Root-Zertifikat (Standard Letsencrypt)",
"TlsCertLoginEnable": "TLS Zertifikat Login", "TlsCertLoginEnable": "TLS Zertifikat Login",
"ClientCert": "TLS Client-Zertifikat:", "ClientCert": "TLS Client-Zertifikat",
"ClientKey": "TLS Client-Key:", "ClientKey": "TLS Client-Key",
"LwtParameters": "LWT-Parameter", "LwtParameters": "LWT-Parameter",
"LwtTopic": "LWT-Topic:", "LwtTopic": "LWT-Topic",
"LwtTopicHint": "LWT-Topic, wird der Basis-Topic angehängt", "LwtTopicHint": "LWT-Topic, wird der Basis-Topic angehängt",
"LwtOnline": "LWT-Online-Nachricht:", "LwtOnline": "LWT-Online-Nachricht",
"LwtOnlineHint": "Nachricht, die im LWT-Topic veröffentlicht wird, wenn OpenDTU online ist", "LwtOnlineHint": "Nachricht, die im LWT-Topic veröffentlicht wird, wenn OpenDTU online ist",
"LwtOffline": "LWT-Offline-Nachricht:", "LwtOffline": "LWT-Offline-Nachricht",
"LwtOfflineHint": "Nachricht, die im LWT-Topic veröffentlicht wird, wenn OpenDTU offline ist", "LwtOfflineHint": "Nachricht, die im LWT-Topic veröffentlicht wird, wenn OpenDTU offline ist",
"LwtQos": "QoS (Quality of Service):", "LwtQos": "QoS (Quality of Service)",
"QOS0": "0 (Höchstens einmal)", "QOS0": "0 (Höchstens einmal)",
"QOS1": "1 (Mindestens einmal)", "QOS1": "1 (Mindestens einmal)",
"QOS2": "2 (Exakt einmal)", "QOS2": "2 (Exakt einmal)",
"HassParameters": "Home Assistant MQTT-Auto-Discovery-Parameter", "HassParameters": "Home Assistant MQTT-Auto-Discovery-Parameter",
"HassPrefixTopic": "Präfix Topic:", "HassPrefixTopic": "Präfix Topic",
"HassPrefixTopicHint": "The prefix for the discovery topic", "HassPrefixTopicHint": "The prefix for the discovery topic",
"HassRetain": "Retain Flag aktivieren", "HassRetain": "Retain Flag aktivieren",
"HassExpire": "Ablauffunktion aktivieren", "HassExpire": "Ablauffunktion aktivieren",
@ -521,7 +547,7 @@
"StringYtOffset": "Ertragsversatz String {num}:", "StringYtOffset": "Ertragsversatz String {num}:",
"StringYtOffsetHint": "Dieser Offset wird beim Auslesen des Gesamtertragswertes des Wechselrichters angewendet. Damit kann der Gesamtertrag des Wechselrichters auf Null gesetzt werden, wenn ein gebrauchter Wechselrichter verwendet wird.", "StringYtOffsetHint": "Dieser Offset wird beim Auslesen des Gesamtertragswertes des Wechselrichters angewendet. Damit kann der Gesamtertrag des Wechselrichters auf Null gesetzt werden, wenn ein gebrauchter Wechselrichter verwendet wird.",
"InverterHint": "*) Geben Sie die W<sub>p</sub> des Ports ein, um die Einstrahlung zu errechnen.", "InverterHint": "*) Geben Sie die W<sub>p</sub> des Ports ein, um die Einstrahlung zu errechnen.",
"ReachableThreshold": "Erreichbarkeit Schwellenwert:", "ReachableThreshold": "Erreichbarkeit Schwellenwert",
"ReachableThresholdHint": "Legt fest, wie viele Anfragen fehlschlagen dürfen, bis der Wechselrichter als unerreichbar eingestuft wird.", "ReachableThresholdHint": "Legt fest, wie viele Anfragen fehlschlagen dürfen, bis der Wechselrichter als unerreichbar eingestuft wird.",
"ZeroRuntime": "Nulle Laufzeit Daten", "ZeroRuntime": "Nulle Laufzeit Daten",
"ZeroRuntimeHint": "Nulle Laufzeit Daten (keine Ertragsdaten), wenn der Wechselrichter nicht erreichbar ist.", "ZeroRuntimeHint": "Nulle Laufzeit Daten (keine Ertragsdaten), wenn der Wechselrichter nicht erreichbar ist.",
@ -535,11 +561,9 @@
"YieldDayCorrection": "Tagesertragskorrektur", "YieldDayCorrection": "Tagesertragskorrektur",
"YieldDayCorrectionHint": "Summiert den Tagesertrag, auch wenn der Wechselrichter neu gestartet wird. Der Wert wird um Mitternacht zurückgesetzt" "YieldDayCorrectionHint": "Summiert den Tagesertrag, auch wenn der Wechselrichter neu gestartet wird. Der Wert wird um Mitternacht zurückgesetzt"
}, },
"configadmin": { "fileadmin": {
"ConfigManagement": "Konfigurationsverwaltung", "ConfigManagement": "Konfigurationsverwaltung",
"BackupHeader": "Sicherung: Sicherung der Konfigurationsdatei", "BackupHeader": "Sicherung: Sicherung der Konfigurationsdatei",
"BackupConfig": "Sicherung der Konfigurationsdatei",
"Backup": "Sichern",
"Restore": "Wiederherstellen", "Restore": "Wiederherstellen",
"NoFileSelected": "Keine Datei ausgewählt", "NoFileSelected": "Keine Datei ausgewählt",
"RestoreHeader": "Wiederherstellen: Wiederherstellen der Konfigurationsdatei", "RestoreHeader": "Wiederherstellen: Wiederherstellen der Konfigurationsdatei",
@ -552,7 +576,15 @@
"FactoryReset": "Werksreset", "FactoryReset": "Werksreset",
"ResetMsg": "Sind Sie sicher, dass Sie die aktuelle Konfiguration löschen und alle Einstellungen auf die Werkseinstellungen zurücksetzen möchten?", "ResetMsg": "Sind Sie sicher, dass Sie die aktuelle Konfiguration löschen und alle Einstellungen auf die Werkseinstellungen zurücksetzen möchten?",
"ResetConfirm": "Werksreset!", "ResetConfirm": "Werksreset!",
"Cancel": "@:base.Cancel" "Download": "Herunterladen",
"Delete": "Löschen",
"DeleteMsg": "Sind Sie sicher, dass Sie die Datei löschen wollen: '{name}'? Es muss manuell neu gestartet werden um die Konfigurationsänderungen zu übernehmen!",
"Name": "Name",
"Size": "Größe",
"Action": "Aktion",
"Cancel": "@:base.Cancel",
"InvalidJson": "JSON-Datei ist falsch formatiert.",
"InvalidJsonContent": "JSON-Datei hat den falschen Inhalt."
}, },
"login": { "login": {
"Login": "Anmeldung", "Login": "Anmeldung",
@ -601,37 +633,36 @@
"DeviceManager": "Hardware-Einstellungen", "DeviceManager": "Hardware-Einstellungen",
"ParseError": "Syntaxfehler in 'pin_mapping.json': {error}", "ParseError": "Syntaxfehler in 'pin_mapping.json': {error}",
"PinAssignment": "Anschlusseinstellungen", "PinAssignment": "Anschlusseinstellungen",
"SelectedProfile": "Ausgewähltes Profil:", "SelectedProfile": "Ausgewähltes Profil",
"DefaultProfile": "(Standardeinstellungen)", "DefaultProfile": "(Standardeinstellungen)",
"ProfileHint": "Ihr Gerät reagiert möglicherweise nicht mehr, wenn Sie ein inkompatibles Profil wählen. In diesem Fall müssen Sie eine Löschung über das serielle Interface durchführen.", "ProfileHint": "Ihr Gerät reagiert möglicherweise nicht mehr, wenn Sie ein inkompatibles Profil wählen. In diesem Fall müssen Sie eine Löschung über das serielle Interface durchführen.",
"Display": "Display", "Display": "Display",
"PowerSafe": "Ausschalten wenn kein Inverter erreichbar:", "PowerSafe": "Ausschalten wenn kein Inverter erreichbar",
"PowerSafeHint": "Schaltet das Display aus, wenn kein Wechselrichter Strom erzeugt", "PowerSafeHint": "Schaltet das Display aus, wenn kein Wechselrichter Strom erzeugt",
"Screensaver": "OLED-Schutz gegen Einbrennen:", "Screensaver": "OLED-Schutz gegen Einbrennen",
"ScreensaverHint": "Bewegt die Ausgabe bei jeder Aktualisierung um ein Einbrennen zu verhindern (v. a. für OLED-Displays nützlich)", "ScreensaverHint": "Bewegt die Ausgabe bei jeder Aktualisierung um ein Einbrennen zu verhindern (v. a. für OLED-Displays nützlich)",
"DiagramMode": "Diagramm Modus:", "DiagramMode": "Diagramm Modus",
"off": "Deaktiviert", "off": "Deaktiviert",
"small": "Klein", "small": "Klein",
"fullscreen": "Vollbild", "fullscreen": "Vollbild",
"DiagramDuration": "Diagramm Periode:", "DiagramDuration": "Diagramm Periode",
"DiagramDurationHint": "Die Zeitperiode welche im Diagramm dargestellt wird.", "DiagramDurationHint": "Die Zeitperiode welche im Diagramm dargestellt wird.",
"Seconds": "Sekunden", "Seconds": "Sekunden",
"Contrast": "Kontrast ({contrast}):", "Contrast": "Kontrast ({contrast})",
"Rotation": "Rotation:", "Rotation": "Rotation",
"rot0": "Keine Rotation", "rot0": "Keine Rotation",
"rot90": "90 Grad Drehung", "rot90": "90 Grad Drehung",
"rot180": "180 Grad Drehung", "rot180": "180 Grad Drehung",
"rot270": "270 Grad Drehung", "rot270": "270 Grad Drehung",
"DisplayLanguage": "Displaysprache:", "DisplayLanguage": "Displaysprache",
"en": "Englisch", "en": "Englisch",
"de": "Deutsch", "de": "Deutsch",
"fr": "Französisch", "fr": "Französisch",
"Leds": "LEDs", "Leds": "LEDs",
"EqualBrightness": "Gleiche Helligkeit:", "EqualBrightness": "Gleiche Helligkeit",
"LedBrightness": "LED {led} Helligkeit ({brightness}):" "LedBrightness": "LED {led} Helligkeit ({brightness})"
}, },
"pininfo": { "pininfo": {
"PinOverview": "Anschlussübersicht",
"Category": "Kategorie", "Category": "Kategorie",
"Name": "Name", "Name": "Name",
"ValueSelected": "Ausgewählt", "ValueSelected": "Ausgewählt",

View File

@ -32,6 +32,10 @@
"Release": "Release to refresh", "Release": "Release to refresh",
"Close": "Close" "Close": "Close"
}, },
"wait": {
"NotReady": "OpenDTU is not yet ready",
"PleaseWait": "Please wait. You will be automatically redirected to the home page."
},
"Error": { "Error": {
"Oops": "Oops!" "Oops": "Oops!"
}, },
@ -54,6 +58,7 @@
"2005": "Invalid country selection!", "2005": "Invalid country selection!",
"3001": "Not deleted anything!", "3001": "Not deleted anything!",
"3002": "Configuration resettet. Rebooting now...", "3002": "Configuration resettet. Rebooting now...",
"3003": "File successful deleted. Restart to apply changes!",
"4001": "@:apiresponse.2001", "4001": "@:apiresponse.2001",
"4002": "Name must between 1 and {max} characters long!", "4002": "Name must between 1 and {max} characters long!",
"4003": "Only {max} inverters are supported!", "4003": "Only {max} inverters are supported!",
@ -150,7 +155,10 @@
"RxFailCorrupt": "RX Fail: Receive Corrupt", "RxFailCorrupt": "RX Fail: Receive Corrupt",
"TxReRequest": "TX Re-Request Fragment", "TxReRequest": "TX Re-Request Fragment",
"StatsReset": "Reset Statistics", "StatsReset": "Reset Statistics",
"StatsResetting": "Resetting..." "StatsResetting": "Resetting...",
"Rssi": "RSSI of last received packet",
"RssiHint": "HM inverters only support RSSI values < -64 dBm and > -64 dBm. In this case, -80 dbm and -30 dbm is shown.",
"dBm": "{dbm} dBm"
}, },
"eventlog": { "eventlog": {
"Start": "Start", "Start": "Start",
@ -235,6 +243,24 @@
"MaxUsage": "Maximum usage since start", "MaxUsage": "Maximum usage since start",
"Fragmentation": "Level of fragmentation" "Fragmentation": "Level of fragmentation"
}, },
"taskdetails": {
"TaskDetails": "Task Details",
"Name": "Name",
"StackFree": "Stack Free",
"Priority": "Priority",
"Task_idle0": "Idle (CPU Core 0)",
"Task_idle1": "Idle (CPU Core 1)",
"Task_wifi": "Wi-Fi",
"Task_tit": "TCP/IP",
"Task_looptask": "Arduino Main Loop",
"Task_asynctcp": "Async TCP",
"Task_mqttclient": "MQTT Client",
"Task_huaweican0": "AC Charger CAN",
"Task_pmsdm": "PowerMeter (SDM)",
"Task_pmhttpjson": "PowerMeter (HTTP+JSON)",
"Task_pmsml": "PowerMeter (Serial SML)",
"Task_pmhttpsml": "PowerMeter (HTTP+SML)"
},
"radioinfo": { "radioinfo": {
"RadioInformation": "Radio Information", "RadioInformation": "Radio Information",
"Status": "{module} Status", "Status": "{module} Status",
@ -369,20 +395,20 @@
"dtuadmin": { "dtuadmin": {
"DtuSettings": "DTU Settings", "DtuSettings": "DTU Settings",
"DtuConfiguration": "DTU Configuration", "DtuConfiguration": "DTU Configuration",
"Serial": "Serial:", "Serial": "Serial",
"SerialHint": "Both the inverter and the DTU have a serial number. The DTU serial number is randomly generated at the first start and does not normally need to be changed.", "SerialHint": "Both the inverter and the DTU have a serial number. The DTU serial number is randomly generated at the first start and does not normally need to be changed.",
"PollInterval": "Poll Interval:", "PollInterval": "Poll Interval",
"Seconds": "Seconds", "Seconds": "Seconds",
"NrfPaLevel": "NRF24 Transmitting power:", "NrfPaLevel": "NRF24 Transmitting power",
"CmtPaLevel": "CMT2300A Transmitting power:", "CmtPaLevel": "CMT2300A Transmitting power",
"NrfPaLevelHint": "Used for HM-Inverters. Make sure your power supply is stable enough before increasing the transmit power.", "NrfPaLevelHint": "Used for HM-Inverters. Make sure your power supply is stable enough before increasing the transmit power.",
"CmtPaLevelHint": "Used for HMS/HMT-Inverters. Make sure your power supply is stable enough before increasing the transmit power.", "CmtPaLevelHint": "Used for HMS/HMT-Inverters. Make sure your power supply is stable enough before increasing the transmit power.",
"CmtCountry": "CMT2300A Region/Country:", "CmtCountry": "CMT2300A Region/Country",
"CmtCountryHint": "Each country has different frequency allocations.", "CmtCountryHint": "Each country has different frequency allocations.",
"country_0": "Europe ({min}MHz - {max}MHz)", "country_0": "Europe ({min}MHz - {max}MHz)",
"country_1": "North America ({min}MHz - {max}MHz)", "country_1": "North America ({min}MHz - {max}MHz)",
"country_2": "Brazil ({min}MHz - {max}MHz)", "country_2": "Brazil ({min}MHz - {max}MHz)",
"CmtFrequency": "CMT2300A Frequency:", "CmtFrequency": "CMT2300A Frequency",
"CmtFrequencyHint": "Make sure to only use frequencies that are allowed in the respective country! After a frequency change, it can take up to 15min until a connection is established.", "CmtFrequencyHint": "Make sure to only use frequencies that are allowed in the respective country! After a frequency change, it can take up to 15min until a connection is established.",
"CmtFrequencyWarning": "The selected frequency is outside the allowed range in your selected region/country. Make sure that this selection does not violate any local regulations.", "CmtFrequencyWarning": "The selected frequency is outside the allowed range in your selected region/country. Make sure that this selection does not violate any local regulations.",
"MHz": "{mhz} MHz", "MHz": "{mhz} MHz",
@ -395,8 +421,8 @@
"securityadmin": { "securityadmin": {
"SecuritySettings": "Security Settings", "SecuritySettings": "Security Settings",
"AdminPassword": "Admin password", "AdminPassword": "Admin password",
"Password": "Password:", "Password": "Password",
"RepeatPassword": "Repeat Password:", "RepeatPassword": "Repeat Password",
"PasswordHint": "<b>Hint:</b> The administrator password is used to access this web interface (user 'admin'), but also to connect to the device when in AP mode. It must be 8..64 characters.", "PasswordHint": "<b>Hint:</b> The administrator password is used to access this web interface (user 'admin'), but also to connect to the device when in AP mode. It must be 8..64 characters.",
"Permissions": "Permissions", "Permissions": "Permissions",
"ReadOnly": "Allow readonly access to web interface without password" "ReadOnly": "Allow readonly access to web interface without password"
@ -404,10 +430,10 @@
"ntpadmin": { "ntpadmin": {
"NtpSettings": "NTP Settings", "NtpSettings": "NTP Settings",
"NtpConfiguration": "NTP Configuration", "NtpConfiguration": "NTP Configuration",
"TimeServer": "Time Server:", "TimeServer": "Time Server",
"TimeServerHint": "The default value is fine as long as OpenDTU has direct access to the internet.", "TimeServerHint": "The default value is fine as long as OpenDTU has direct access to the internet.",
"Timezone": "Timezone:", "Timezone": "Timezone",
"TimezoneConfig": "Timezone Config:", "TimezoneConfig": "Timezone Config",
"LocationConfiguration": "Location Configuration", "LocationConfiguration": "Location Configuration",
"Longitude": "Longitude", "Longitude": "Longitude",
"Latitude": "Latitude", "Latitude": "Latitude",
@ -418,26 +444,26 @@
"CIVIL": "Civil dawn (96°)", "CIVIL": "Civil dawn (96°)",
"ASTONOMICAL": "Astronomical dawn (108°)", "ASTONOMICAL": "Astronomical dawn (108°)",
"ManualTimeSynchronization": "Manual Time Synchronization", "ManualTimeSynchronization": "Manual Time Synchronization",
"CurrentOpenDtuTime": "Current OpenDTU Time:", "CurrentOpenDtuTime": "Current OpenDTU Time",
"CurrentLocalTime": "Current Local Time:", "CurrentLocalTime": "Current Local Time",
"SynchronizeTime": "Synchronize Time", "SynchronizeTime": "Synchronize Time",
"SynchronizeTimeHint": "<b>Hint:</b> You can use the manual time synchronization to set the current time of OpenDTU if no NTP server is available. But be aware, that in case of power cycle the time gets lost. Also note that time accuracy will be skewed badly, as it can not be resynchronised regularly and the ESP32 microcontroller does not have a real time clock." "SynchronizeTimeHint": "<b>Hint:</b> You can use the manual time synchronization to set the current time of OpenDTU if no NTP server is available. But be aware, that in case of power cycle the time gets lost. Also note that time accuracy will be skewed badly, as it can not be resynchronised regularly and the ESP32 microcontroller does not have a real time clock."
}, },
"networkadmin": { "networkadmin": {
"NetworkSettings": "Network Settings", "NetworkSettings": "Network Settings",
"WifiConfiguration": "WiFi Configuration", "WifiConfiguration": "WiFi Configuration",
"WifiSsid": "WiFi SSID:", "WifiSsid": "WiFi SSID",
"WifiPassword": "WiFi Password:", "WifiPassword": "WiFi Password",
"Hostname": "Hostname:", "Hostname": "Hostname",
"HostnameHint": "<b>Hint:</b> The text <span class=\"font-monospace\">%06X</span> will be replaced with the last 6 digits of the ESP ChipID in hex format.", "HostnameHint": "<b>Hint:</b> The text <span class=\"font-monospace\">%06X</span> will be replaced with the last 6 digits of the ESP ChipID in hex format.",
"EnableDhcp": "Enable DHCP", "EnableDhcp": "Enable DHCP",
"StaticIpConfiguration": "Static IP Configuration", "StaticIpConfiguration": "Static IP Configuration",
"IpAddress": "IP Address:", "IpAddress": "IP Address",
"Netmask": "Netmask:", "Netmask": "Netmask",
"DefaultGateway": "Default Gateway:", "DefaultGateway": "Default Gateway",
"Dns": "DNS Server {num}:", "Dns": "DNS Server {num}",
"AdminAp": "WiFi Configuration (Admin AccessPoint)", "AdminAp": "WiFi Configuration (Admin AccessPoint)",
"ApTimeout": "AccessPoint Timeout:", "ApTimeout": "AccessPoint Timeout",
"ApTimeoutHint": "Time which the AccessPoint is kept open. A value of 0 means infinite.", "ApTimeoutHint": "Time which the AccessPoint is kept open. A value of 0 means infinite.",
"Minutes": "minutes", "Minutes": "minutes",
"EnableMdns": "Enable mDNS", "EnableMdns": "Enable mDNS",
@ -449,38 +475,38 @@
"EnableMqtt": "Enable MQTT", "EnableMqtt": "Enable MQTT",
"EnableHass": "Enable Home Assistant MQTT Auto Discovery", "EnableHass": "Enable Home Assistant MQTT Auto Discovery",
"MqttBrokerParameter": "MQTT Broker Parameter", "MqttBrokerParameter": "MQTT Broker Parameter",
"Hostname": "Hostname:", "Hostname": "Hostname",
"HostnameHint": "Hostname or IP address", "HostnameHint": "Hostname or IP address",
"Port": "Port:", "Port": "Port",
"ClientId": "Client ID:", "ClientId": "Client ID",
"Username": "Username:", "Username": "Username",
"UsernameHint": "Username, leave empty for anonymous connection", "UsernameHint": "Username, leave empty for anonymous connection",
"Password": "Password:", "Password": "Password",
"PasswordHint": "Password, leave empty for anonymous connection", "PasswordHint": "Password, leave empty for anonymous connection",
"BaseTopic": "Base Topic:", "BaseTopic": "Base Topic",
"BaseTopicHint": "Base topic, will be prepend to all published topics (e.g. inverter/)", "BaseTopicHint": "Base topic, will be prepend to all published topics (e.g. inverter/)",
"PublishInterval": "Publish Interval:", "PublishInterval": "Publish Interval",
"Seconds": "seconds", "Seconds": "seconds",
"CleanSession": "Enable CleanSession flag", "CleanSession": "Enable CleanSession flag",
"EnableRetain": "Enable Retain Flag", "EnableRetain": "Enable Retain Flag",
"EnableTls": "Enable TLS", "EnableTls": "Enable TLS",
"RootCa": "CA-Root-Certificate (default Letsencrypt):", "RootCa": "CA-Root-Certificate (default Letsencrypt)",
"TlsCertLoginEnable": "Enable TLS Certificate Login", "TlsCertLoginEnable": "Enable TLS Certificate Login",
"ClientCert": "TLS Client-Certificate:", "ClientCert": "TLS Client-Certificate",
"ClientKey": "TLS Client-Key:", "ClientKey": "TLS Client-Key",
"LwtParameters": "LWT Parameters", "LwtParameters": "LWT Parameters",
"LwtTopic": "LWT Topic:", "LwtTopic": "LWT Topic",
"LwtTopicHint": "LWT topic, will be append base topic", "LwtTopicHint": "LWT topic, will be append base topic",
"LwtOnline": "LWT Online message:", "LwtOnline": "LWT Online message",
"LwtOnlineHint": "Message that will be published to LWT topic when online", "LwtOnlineHint": "Message that will be published to LWT topic when online",
"LwtOffline": "LWT Offline message:", "LwtOffline": "LWT Offline message",
"LwtOfflineHint": "Message that will be published to LWT topic when offline", "LwtOfflineHint": "Message that will be published to LWT topic when offline",
"LwtQos": "QoS (Quality of Service):", "LwtQos": "QoS (Quality of Service)",
"QOS0": "0 (At most once)", "QOS0": "0 (At most once)",
"QOS1": "1 (At least once)", "QOS1": "1 (At least once)",
"QOS2": "2 (Exactly once)", "QOS2": "2 (Exactly once)",
"HassParameters": "Home Assistant MQTT Auto Discovery Parameters", "HassParameters": "Home Assistant MQTT Auto Discovery Parameters",
"HassPrefixTopic": "Prefix Topic:", "HassPrefixTopic": "Prefix Topic",
"HassPrefixTopicHint": "The prefix for the discovery topic", "HassPrefixTopicHint": "The prefix for the discovery topic",
"HassRetain": "Enable Retain Flag", "HassRetain": "Enable Retain Flag",
"HassExpire": "Enable Expiration", "HassExpire": "Enable Expiration",
@ -521,7 +547,7 @@
"StringYtOffset": "Yield total offset string {num}:", "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. But you can still try polling data.", "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. But you can still try polling data.",
"InverterHint": "*) Enter the W<sub>p</sub> of the channel to calculate irradiation.", "InverterHint": "*) Enter the W<sub>p</sub> of the channel to calculate irradiation.",
"ReachableThreshold": "Reachable Threshold:", "ReachableThreshold": "Reachable Threshold",
"ReachableThresholdHint": "Defines how many requests are allowed to fail until the inverter is treated is not reachable.", "ReachableThresholdHint": "Defines how many requests are allowed to fail until the inverter is treated is not reachable.",
"ZeroRuntime": "Zero runtime data", "ZeroRuntime": "Zero runtime data",
"ZeroRuntimeHint": "Zero runtime data (no yield data) if inverter becomes unreachable.", "ZeroRuntimeHint": "Zero runtime data (no yield data) if inverter becomes unreachable.",
@ -535,11 +561,9 @@
"YieldDayCorrection": "Yield Day Correction", "YieldDayCorrection": "Yield Day Correction",
"YieldDayCorrectionHint": "Sum up daily yield even if the inverter is restarted. Value will be reset at midnight" "YieldDayCorrectionHint": "Sum up daily yield even if the inverter is restarted. Value will be reset at midnight"
}, },
"configadmin": { "fileadmin": {
"ConfigManagement": "Config Management", "ConfigManagement": "Config Management",
"BackupHeader": "Backup: Configuration File Backup", "BackupHeader": "Backup: Configuration File Backup",
"BackupConfig": "Backup the configuration file",
"Backup": "Backup",
"Restore": "Restore", "Restore": "Restore",
"NoFileSelected": "No file selected", "NoFileSelected": "No file selected",
"RestoreHeader": "Restore: Restore the Configuration File", "RestoreHeader": "Restore: Restore the Configuration File",
@ -552,7 +576,15 @@
"FactoryReset": "Factory Reset", "FactoryReset": "Factory Reset",
"ResetMsg": "Are you sure you want to delete the current configuration and reset all settings to their factory defaults?", "ResetMsg": "Are you sure you want to delete the current configuration and reset all settings to their factory defaults?",
"ResetConfirm": "Factory Reset!", "ResetConfirm": "Factory Reset!",
"Cancel": "@:base.Cancel" "Download": "Download",
"Delete": "Delete",
"DeleteMsg": "Are you sure you want to delete file: '{name}'? You have to manually reboot the device to apply config changes!",
"Name": "Name",
"Size": "Size",
"Action": "Action",
"Cancel": "@:base.Cancel",
"InvalidJson": "JSON file is formatted incorrectly.",
"InvalidJsonContent": "JSON file has the wrong content."
}, },
"login": { "login": {
"Login": "Login", "Login": "Login",
@ -601,37 +633,36 @@
"DeviceManager": "Device-Manager", "DeviceManager": "Device-Manager",
"ParseError": "Parse error in 'pin_mapping.json': {error}", "ParseError": "Parse error in 'pin_mapping.json': {error}",
"PinAssignment": "Connection settings", "PinAssignment": "Connection settings",
"SelectedProfile": "Selected profile:", "SelectedProfile": "Selected profile",
"DefaultProfile": "(Default settings)", "DefaultProfile": "(Default settings)",
"ProfileHint": "Your device may stop responding if you select an incompatible profile. In this case, you must perform a deletion via the serial interface.", "ProfileHint": "Your device may stop responding if you select an incompatible profile. In this case, you must perform a deletion via the serial interface.",
"Display": "Display", "Display": "Display",
"PowerSafe": "Switch off if no solar:", "PowerSafe": "Switch off if no solar",
"PowerSafeHint": "Turn off the display if no inverter is producing.", "PowerSafeHint": "Turn off the display if no inverter is producing.",
"Screensaver": "OLED Anti burn-in:", "Screensaver": "OLED Anti burn-in",
"ScreensaverHint": "Move the display a little bit on each update to prevent burn-in. (Useful especially for OLED displays)", "ScreensaverHint": "Move the display a little bit on each update to prevent burn-in. (Useful especially for OLED displays)",
"DiagramMode": "Diagram mode:", "DiagramMode": "Diagram mode",
"off": "Off", "off": "Off",
"small": "Small", "small": "Small",
"fullscreen": "Fullscreen", "fullscreen": "Fullscreen",
"DiagramDuration": "Diagram duration:", "DiagramDuration": "Diagram duration",
"DiagramDurationHint": "The time period which is shown in the diagram.", "DiagramDurationHint": "The time period which is shown in the diagram.",
"Seconds": "Seconds", "Seconds": "Seconds",
"Contrast": "Contrast ({contrast}):", "Contrast": "Contrast ({contrast})",
"Rotation": "Rotation:", "Rotation": "Rotation",
"rot0": "No rotation", "rot0": "No rotation",
"rot90": "90 degree rotation", "rot90": "90 degree rotation",
"rot180": "180 degree rotation", "rot180": "180 degree rotation",
"rot270": "270 degree rotation", "rot270": "270 degree rotation",
"DisplayLanguage": "Display language:", "DisplayLanguage": "Display language",
"en": "English", "en": "English",
"de": "German", "de": "German",
"fr": "French", "fr": "French",
"Leds": "LEDs", "Leds": "LEDs",
"EqualBrightness": "Equal brightness:", "EqualBrightness": "Equal brightness",
"LedBrightness": "LED {led} brightness ({brightness}):" "LedBrightness": "LED {led} brightness ({brightness})"
}, },
"pininfo": { "pininfo": {
"PinOverview": "Connection overview",
"Category": "Category", "Category": "Category",
"Name": "Name", "Name": "Name",
"Number": "Number", "Number": "Number",

View File

@ -32,6 +32,10 @@
"Release": "Release to refresh", "Release": "Release to refresh",
"Close": "Fermer" "Close": "Fermer"
}, },
"wait": {
"NotReady": "OpenDTU is not yet ready",
"PleaseWait": "Please wait. You will be automatically redirected to the home page."
},
"Error": { "Error": {
"Oops": "Oops!" "Oops": "Oops!"
}, },
@ -54,6 +58,7 @@
"2005": "Invalid country selection !", "2005": "Invalid country selection !",
"3001": "Rien n'a été supprimé !", "3001": "Rien n'a été supprimé !",
"3002": "Configuration réinitialisée. Redémarrage maintenant...", "3002": "Configuration réinitialisée. Redémarrage maintenant...",
"3003": "File successful deleted. Restart to apply changes!",
"4001": "@:apiresponse.2001", "4001": "@:apiresponse.2001",
"4002": "Le nom doit comporter entre 1 et {max} caractères !", "4002": "Le nom doit comporter entre 1 et {max} caractères !",
"4003": "Seulement {max} onduleurs sont supportés !", "4003": "Seulement {max} onduleurs sont supportés !",
@ -150,7 +155,10 @@
"RxFailCorrupt": "RX Fail: Receive Corrupt", "RxFailCorrupt": "RX Fail: Receive Corrupt",
"TxReRequest": "TX Re-Request Fragment", "TxReRequest": "TX Re-Request Fragment",
"StatsReset": "Reset Statistics", "StatsReset": "Reset Statistics",
"StatsResetting": "Resetting..." "StatsResetting": "Resetting...",
"Rssi": "RSSI of last received packet",
"RssiHint": "HM inverters only support RSSI values < -64 dBm and > -64 dBm. In this case, -80 dbm and -30 dbm is shown.",
"dBm": "{dbm} dBm"
}, },
"eventlog": { "eventlog": {
"Start": "Départ", "Start": "Départ",
@ -377,12 +385,12 @@
"CmtPaLevel": "CMT2300A Niveau de puissance d'émission", "CmtPaLevel": "CMT2300A Niveau de puissance d'émission",
"NrfPaLevelHint": "Used for HM-Inverters. Assurez-vous que votre alimentation est suffisamment stable avant d'augmenter la puissance d'émission.", "NrfPaLevelHint": "Used for HM-Inverters. Assurez-vous que votre alimentation est suffisamment stable avant d'augmenter la puissance d'émission.",
"CmtPaLevelHint": "Used for HMS/HMT-Inverters. Assurez-vous que votre alimentation est suffisamment stable avant d'augmenter la puissance d'émission.", "CmtPaLevelHint": "Used for HMS/HMT-Inverters. Assurez-vous que votre alimentation est suffisamment stable avant d'augmenter la puissance d'émission.",
"CmtCountry": "CMT2300A Region/Country:", "CmtCountry": "CMT2300A Region/Country",
"CmtCountryHint": "Each country has different frequency allocations.", "CmtCountryHint": "Each country has different frequency allocations.",
"country_0": "Europe ({min}MHz - {max}MHz)", "country_0": "Europe ({min}MHz - {max}MHz)",
"country_1": "North America ({min}MHz - {max}MHz)", "country_1": "North America ({min}MHz - {max}MHz)",
"country_2": "Brazil ({min}MHz - {max}MHz)", "country_2": "Brazil ({min}MHz - {max}MHz)",
"CmtFrequency": "CMT2300A Frequency:", "CmtFrequency": "CMT2300A Frequency",
"CmtFrequencyHint": "Make sure to only use frequencies that are allowed in the respective country! After a frequency change, it can take up to 15min until a connection is established.", "CmtFrequencyHint": "Make sure to only use frequencies that are allowed in the respective country! After a frequency change, it can take up to 15min until a connection is established.",
"CmtFrequencyWarning": "The selected frequency is outside the allowed range in your selected region/country. Make sure that this selection does not violate any local regulations.", "CmtFrequencyWarning": "The selected frequency is outside the allowed range in your selected region/country. Make sure that this selection does not violate any local regulations.",
"MHz": "{mhz} MHz", "MHz": "{mhz} MHz",
@ -452,10 +460,10 @@
"Hostname": "Nom d'hôte", "Hostname": "Nom d'hôte",
"HostnameHint": "Nom d'hôte ou adresse IP", "HostnameHint": "Nom d'hôte ou adresse IP",
"Port": "Port", "Port": "Port",
"ClientId": "Client ID:", "ClientId": "Client ID",
"Username": "Nom d'utilisateur", "Username": "Nom d'utilisateur",
"UsernameHint": "Nom d'utilisateur, laisser vide pour une connexion anonyme", "UsernameHint": "Nom d'utilisateur, laisser vide pour une connexion anonyme",
"Password": "Mot de passe:", "Password": "Mot de passe",
"PasswordHint": "Mot de passe, laissez vide pour une connexion anonyme", "PasswordHint": "Mot de passe, laissez vide pour une connexion anonyme",
"BaseTopic": "Sujet de base", "BaseTopic": "Sujet de base",
"BaseTopicHint": "Sujet de base, qui sera ajouté en préambule à tous les sujets publiés (par exemple, inverter/).", "BaseTopicHint": "Sujet de base, qui sera ajouté en préambule à tous les sujets publiés (par exemple, inverter/).",
@ -466,8 +474,8 @@
"EnableTls": "Activer le TLS", "EnableTls": "Activer le TLS",
"RootCa": "Certificat CA-Root (par défaut Letsencrypt)", "RootCa": "Certificat CA-Root (par défaut Letsencrypt)",
"TlsCertLoginEnable": "Activer la connexion par certificat TLS", "TlsCertLoginEnable": "Activer la connexion par certificat TLS",
"ClientCert": "Certificat client TLS:", "ClientCert": "Certificat client TLS",
"ClientKey": "Clé client TLS:", "ClientKey": "Clé client TLS",
"LwtParameters": "Paramètres LWT", "LwtParameters": "Paramètres LWT",
"LwtTopic": "Sujet LWT", "LwtTopic": "Sujet LWT",
"LwtTopicHint": "Sujet LWT, sera ajouté comme sujet de base", "LwtTopicHint": "Sujet LWT, sera ajouté comme sujet de base",
@ -475,7 +483,7 @@
"LwtOnlineHint": "Message qui sera publié sur le sujet LWT lorsqu'il sera en ligne", "LwtOnlineHint": "Message qui sera publié sur le sujet LWT lorsqu'il sera en ligne",
"LwtOffline": "Message hors ligne de LWT", "LwtOffline": "Message hors ligne de LWT",
"LwtOfflineHint": "Message qui sera publié sur le sujet LWT lorsqu'il sera hors ligne", "LwtOfflineHint": "Message qui sera publié sur le sujet LWT lorsqu'il sera hors ligne",
"LwtQos": "QoS (Quality of Service):", "LwtQos": "QoS (Quality of Service)",
"QOS0": "0 (Au maximum une fois)", "QOS0": "0 (Au maximum une fois)",
"QOS1": "1 (Au moins une fois)", "QOS1": "1 (Au moins une fois)",
"QOS2": "2 (Exactement une fois)", "QOS2": "2 (Exactement une fois)",
@ -535,11 +543,9 @@
"YieldDayCorrection": "Yield Day Correction", "YieldDayCorrection": "Yield Day Correction",
"YieldDayCorrectionHint": "Sum up daily yield even if the inverter is restarted. Value will be reset at midnight" "YieldDayCorrectionHint": "Sum up daily yield even if the inverter is restarted. Value will be reset at midnight"
}, },
"configadmin": { "fileadmin": {
"ConfigManagement": "Gestion de la configuration", "ConfigManagement": "Gestion de la configuration",
"BackupHeader": "Sauvegarder le fichier de configuration", "BackupHeader": "Sauvegarder le fichier de configuration",
"BackupConfig": "Fichier de configuration",
"Backup": "Sauvegarder",
"Restore": "Restaurer", "Restore": "Restaurer",
"NoFileSelected": "Aucun fichier sélectionné", "NoFileSelected": "Aucun fichier sélectionné",
"RestoreHeader": "Restaurer le fichier de configuration", "RestoreHeader": "Restaurer le fichier de configuration",
@ -552,7 +558,15 @@
"FactoryReset": "Remise à zéro", "FactoryReset": "Remise à zéro",
"ResetMsg": "Êtes-vous sûr de vouloir supprimer la configuration actuelle et réinitialiser tous les paramètres à leurs valeurs par défaut ?", "ResetMsg": "Êtes-vous sûr de vouloir supprimer la configuration actuelle et réinitialiser tous les paramètres à leurs valeurs par défaut ?",
"ResetConfirm": "Remise à zéro !", "ResetConfirm": "Remise à zéro !",
"Cancel": "@:base.Cancel" "Download": "Download",
"Delete": "Supprimer",
"DeleteMsg": "Are you sure you want to delete file: '{name}'? You have to manually reboot the device to apply config changes!",
"Name": "Name",
"Size": "Size",
"Action": "Action",
"Cancel": "@:base.Cancel",
"InvalidJson": "JSON file is formatted incorrectly.",
"InvalidJsonContent": "JSON file has the wrong content."
}, },
"login": { "login": {
"Login": "Connexion", "Login": "Connexion",
@ -609,15 +623,15 @@
"PowerSafeHint": "Eteindre l'écran si aucun onduleur n'est en production.", "PowerSafeHint": "Eteindre l'écran si aucun onduleur n'est en production.",
"Screensaver": "OLED Anti burn-in", "Screensaver": "OLED Anti burn-in",
"ScreensaverHint": "Déplacez un peu l'écran à chaque mise à jour pour éviter le phénomène de brûlure. (Utile surtout pour les écrans OLED)", "ScreensaverHint": "Déplacez un peu l'écran à chaque mise à jour pour éviter le phénomène de brûlure. (Utile surtout pour les écrans OLED)",
"DiagramMode": "Diagram mode:", "DiagramMode": "Diagram mode",
"off": "Off", "off": "Off",
"small": "Small", "small": "Small",
"fullscreen": "Fullscreen", "fullscreen": "Fullscreen",
"DiagramDuration": "Diagram duration:", "DiagramDuration": "Diagram duration",
"DiagramDurationHint": "The time period which is shown in the diagram.", "DiagramDurationHint": "The time period which is shown in the diagram.",
"Seconds": "Seconds", "Seconds": "Seconds",
"Contrast": "Contraste ({contrast}):", "Contrast": "Contraste ({contrast})",
"Rotation": "Rotation:", "Rotation": "Rotation",
"rot0": "Pas de rotation", "rot0": "Pas de rotation",
"rot90": "Rotation de 90 degrés", "rot90": "Rotation de 90 degrés",
"rot180": "Rotation de 180 degrés", "rot180": "Rotation de 180 degrés",
@ -627,11 +641,10 @@
"de": "Allemand", "de": "Allemand",
"fr": "Français", "fr": "Français",
"Leds": "LEDs", "Leds": "LEDs",
"EqualBrightness": "Même luminosité:", "EqualBrightness": "Même luminosité",
"LedBrightness": "LED {led} luminosité ({brightness}):" "LedBrightness": "LED {led} luminosité ({brightness})"
}, },
"pininfo": { "pininfo": {
"PinOverview": "Vue d'ensemble des connexions",
"Category": "Catégorie", "Category": "Catégorie",
"Name": "Nom", "Name": "Nom",
"ValueSelected": "Sélectionné", "ValueSelected": "Sélectionné",

View File

@ -1,78 +0,0 @@
import type { I18nOptions } from 'vue-i18n';
export enum Locales {
EN = 'en',
DE = 'de',
FR = 'fr',
}
export const LOCALES = [
{ value: Locales.EN, caption: 'English' },
{ value: Locales.DE, caption: 'Deutsch' },
{ value: Locales.FR, caption: 'Français' },
];
export const dateTimeFormats: I18nOptions['datetimeFormats'] = {};
export const numberFormats: I18nOptions['numberFormats'] = {};
LOCALES.forEach((locale) => {
dateTimeFormats[locale.value] = {
datetime: {
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour12: false,
},
};
numberFormats[locale.value] = {
decimal: {
style: 'decimal',
},
decimalNoDigits: {
style: 'decimal',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
},
decimalOneDigit: {
style: 'decimal',
minimumFractionDigits: 1,
maximumFractionDigits: 1,
},
decimalTwoDigits: {
style: 'decimal',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
},
percent: {
style: 'percent',
},
percentOneDigit: {
style: 'percent',
minimumFractionDigits: 1,
maximumFractionDigits: 1,
},
byte: {
style: 'unit',
unit: 'byte',
},
kilobyte: {
style: 'unit',
unit: 'kilobyte',
},
megabyte: {
style: 'unit',
unit: 'megabyte',
},
celsius: {
style: 'unit',
unit: 'celsius',
maximumFractionDigits: 1,
},
};
});
export const defaultLocale = Locales.EN;

View File

@ -1,11 +1,9 @@
import messages from '@intlify/unplugin-vue-i18n/messages';
import mitt from 'mitt'; import mitt from 'mitt';
import { createApp } from 'vue'; import { createApp } from 'vue';
import { createI18n } from 'vue-i18n';
import App from './App.vue'; import App from './App.vue';
import { dateTimeFormats, defaultLocale, numberFormats } from './locales';
import { tooltip } from './plugins/bootstrap'; import { tooltip } from './plugins/bootstrap';
import router from './router'; import router from './router';
import { i18n } from './i18n';
import 'bootstrap'; import 'bootstrap';
import './scss/styles.scss'; import './scss/styles.scss';
@ -17,16 +15,6 @@ app.config.globalProperties.$emitter = emitter;
app.directive('tooltip', tooltip); app.directive('tooltip', tooltip);
const i18n = createI18n({
legacy: false,
globalInjection: true,
locale: navigator.language.split('-')[0],
fallbackLocale: defaultLocale,
messages,
datetimeFormats: dateTimeFormats,
numberFormats: numberFormats,
});
app.use(router); app.use(router);
app.use(i18n); app.use(i18n);

View File

@ -17,11 +17,19 @@ import NtpAdminView from '@/views/NtpAdminView.vue';
import NtpInfoView from '@/views/NtpInfoView.vue'; import NtpInfoView from '@/views/NtpInfoView.vue';
import SecurityAdminView from '@/views/SecurityAdminView.vue'; import SecurityAdminView from '@/views/SecurityAdminView.vue';
import SystemInfoView from '@/views/SystemInfoView.vue'; import SystemInfoView from '@/views/SystemInfoView.vue';
import WaitRestartView from '@/views/WaitRestartView.vue';
import { createRouter, createWebHistory } from 'vue-router'; import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
linkActiveClass: 'active', linkActiveClass: 'active',
scrollBehavior() {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ top: 0 });
}, 100);
});
},
routes: [ routes: [
{ {
path: '/', path: '/',
@ -118,6 +126,11 @@ const router = createRouter({
name: 'Device Reboot', name: 'Device Reboot',
component: MaintenanceRebootView, component: MaintenanceRebootView,
}, },
{
path: '/wait',
name: 'Wait Restart',
component: WaitRestartView,
},
], ],
}); });

Some files were not shown because too many files have changed in this diff Show More