diff --git a/include/Configuration.h b/include/Configuration.h index cd925dcc..6915869b 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -161,6 +161,7 @@ struct CONFIG_T { bool Display_ScreenSaver; uint8_t Display_Rotation; uint8_t Display_Contrast; + uint8_t Display_Language; }; class ConfigurationClass { diff --git a/include/Display_Graphic.h b/include/Display_Graphic.h index 18c1c3aa..ac0512df 100644 --- a/include/Display_Graphic.h +++ b/include/Display_Graphic.h @@ -20,6 +20,7 @@ public: void loop(); void setContrast(uint8_t contrast); void setOrientation(uint8_t rotation = DISPLAY_ROTATION); + void setLanguage(uint8_t language); void setStartupDisplay(); bool enablePowerSafe = true; @@ -33,6 +34,7 @@ private: U8G2* _display; DisplayType_t _display_type = DisplayType_t::None; + uint8_t _display_language = DISPLAY_LANGUAGE; uint8_t _mExtra; uint16_t _period = 1000; uint16_t _interval = 60000; // interval at which to power save (milliseconds) diff --git a/include/defaults.h b/include/defaults.h index 1e686511..e1d7c70c 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -92,6 +92,7 @@ #define DISPLAY_SCREENSAVER true #define DISPLAY_ROTATION 2 #define DISPLAY_CONTRAST 60 +#define DISPLAY_LANGUAGE 0 #define VEDIRECT_ENABLED false #define VEDIRECT_UPDATESONLY true diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.cpp b/lib/Hoymiles/src/inverters/InverterAbstract.cpp index 5830da0a..2383f5fc 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.cpp +++ b/lib/Hoymiles/src/inverters/InverterAbstract.cpp @@ -7,7 +7,7 @@ #include "crc.h" #include -InverterAbstract::InverterAbstract(HoymilesRadio *radio, uint64_t serial) +InverterAbstract::InverterAbstract(HoymilesRadio* radio, uint64_t serial) { _serial.u64 = serial; _radio = radio; @@ -152,26 +152,32 @@ void InverterAbstract::addRxFragment(uint8_t fragment[], uint8_t len) } uint8_t fragmentCount = fragment[9]; - if (fragmentCount == 0) { - Hoymiles.getMessageOutput()->println("ERROR: fragment number zero received and ignored"); + + // Packets with 0x81 will be seen as 1 + uint8_t fragmentId = fragmentCount & 0b01111111; // fragmentId is 1 based + + if (fragmentId == 0) { + Hoymiles.getMessageOutput()->println("ERROR: fragment id zero received and ignored"); return; } - if ((fragmentCount & 0b01111111) < MAX_RF_FRAGMENT_COUNT) { - // Packets with 0x81 will be seen as 1 - memcpy(_rxFragmentBuffer[(fragmentCount & 0b01111111) - 1].fragment, &fragment[10], len - 11); - _rxFragmentBuffer[(fragmentCount & 0b01111111) - 1].len = len - 11; - _rxFragmentBuffer[(fragmentCount & 0b01111111) - 1].mainCmd = fragment[0]; - _rxFragmentBuffer[(fragmentCount & 0b01111111) - 1].wasReceived = true; + if (fragmentId >= MAX_RF_FRAGMENT_COUNT) { + Hoymiles.getMessageOutput()->printf("ERROR: fragment id %d is too large for buffer and ignored\r\n", fragmentId); + return; + } - if ((fragmentCount & 0b01111111) > _rxFragmentLastPacketId) { - _rxFragmentLastPacketId = fragmentCount & 0b01111111; - } + memcpy(_rxFragmentBuffer[fragmentId - 1].fragment, &fragment[10], len - 11); + _rxFragmentBuffer[fragmentId - 1].len = len - 11; + _rxFragmentBuffer[fragmentId - 1].mainCmd = fragment[0]; + _rxFragmentBuffer[fragmentId - 1].wasReceived = true; + + if (fragmentId > _rxFragmentLastPacketId) { + _rxFragmentLastPacketId = fragmentId; } // 0b10000000 == 0x80 if ((fragmentCount & 0b10000000) == 0b10000000) { - _rxFragmentMaxPacketId = fragmentCount & 0b01111111; + _rxFragmentMaxPacketId = fragmentId; } } diff --git a/src/Configuration.cpp b/src/Configuration.cpp index b66098c1..9e22a9cc 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -95,6 +95,7 @@ bool ConfigurationClass::write() display["screensaver"] = config.Display_ScreenSaver; display["rotation"] = config.Display_Rotation; display["contrast"] = config.Display_Contrast; + display["language"] = config.Display_Language; JsonArray inverters = doc.createNestedArray("inverters"); for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { @@ -289,6 +290,7 @@ bool ConfigurationClass::read() config.Display_ScreenSaver = display["screensaver"] | DISPLAY_SCREENSAVER; config.Display_Rotation = display["rotation"] | DISPLAY_ROTATION; config.Display_Contrast = display["contrast"] | DISPLAY_CONTRAST; + config.Display_Language = display["language"] | DISPLAY_LANGUAGE; JsonArray inverters = doc["inverters"]; for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { diff --git a/src/Display_Graphic.cpp b/src/Display_Graphic.cpp index 765ee953..d098c506 100644 --- a/src/Display_Graphic.cpp +++ b/src/Display_Graphic.cpp @@ -11,6 +11,25 @@ std::map { DisplayType_t::SH1106, [](uint8_t reset, uint8_t clock, uint8_t data, uint8_t cs) { return new U8G2_SH1106_128X64_NONAME_F_HW_I2C(U8G2_R0, reset, clock, data); } }, }; +// Language defintion, respect order in languages[] and translation lists +#define I18N_LOCALE_EN 0 +#define I18N_LOCALE_DE 1 +#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_current_power_w[] = { "%3.0f W", "%3.0f W", "%3.0f W" }; +static const char* const i18n_current_power_kw[] = { "%2.1f kW", "%2.1f kW", "%2.1f kW" }; +static const char* const i18n_yield_today_wh[] = { "today: %4.0f Wh", "Heute: %4.0f Wh", "auj.: %4.0f Wh" }; +static const char* const i18n_yield_total_kwh[] = { "total: %.1f kWh", "Ges.: %.1f kWh", "total: %.1f kWh" }; +static const char* const i18n_date_format[] = { "%m/%d/%Y %H:%M", "%d.%m.%Y %H:%M", "%d/%m/%Y %H:%M" }; + DisplayGraphicClass::DisplayGraphicClass() { } @@ -95,6 +114,11 @@ void DisplayGraphicClass::setOrientation(uint8_t rotation) calcLineHeights(); } +void DisplayGraphicClass::setLanguage(uint8_t language) +{ + _display_language = language < sizeof(languages) / sizeof(languages[0]) ? language : DISPLAY_LANGUAGE; +} + void DisplayGraphicClass::setStartupDisplay() { if (_display_type == DisplayType_t::None) { @@ -120,9 +144,9 @@ void DisplayGraphicClass::loop() if (Datastore.isAtLeastOneReachable) { _display->setPowerSave(false); if (Datastore.totalAcPowerEnabled > 999) { - snprintf(_fmtText, sizeof(_fmtText), "%2.1f kW", (Datastore.totalAcPowerEnabled / 1000)); + snprintf(_fmtText, sizeof(_fmtText), i18n_current_power_kw[_display_language], (Datastore.totalAcPowerEnabled / 1000)); } else { - snprintf(_fmtText, sizeof(_fmtText), "%3.0f W", Datastore.totalAcPowerEnabled); + snprintf(_fmtText, sizeof(_fmtText), i18n_current_power_w[_display_language], Datastore.totalAcPowerEnabled); } printText(_fmtText, 0); _previousMillis = millis(); @@ -131,7 +155,7 @@ void DisplayGraphicClass::loop() //=====> Offline =========== else { - printText("offline", 0); + printText(i18n_offline[_display_language], 0); // check if it's time to enter power saving mode if (millis() - _previousMillis >= (_interval * 2)) { _display->setPowerSave(enablePowerSafe); @@ -140,10 +164,10 @@ void DisplayGraphicClass::loop() //<======================= //=====> Today & Total Production ======= - snprintf(_fmtText, sizeof(_fmtText), "today: %4.0f Wh", Datastore.totalAcYieldDayEnabled); + snprintf(_fmtText, sizeof(_fmtText), i18n_yield_today_wh[_display_language], Datastore.totalAcYieldDayEnabled); printText(_fmtText, 1); - snprintf(_fmtText, sizeof(_fmtText), "total: %.1f kWh", Datastore.totalAcYieldTotalEnabled); + snprintf(_fmtText, sizeof(_fmtText), i18n_yield_total_kwh[_display_language], Datastore.totalAcYieldTotalEnabled); printText(_fmtText, 2); //<======================= @@ -153,7 +177,7 @@ void DisplayGraphicClass::loop() } else { // Get current time time_t now = time(nullptr); - strftime(_fmtText, sizeof(_fmtText), "%a %d.%m.%Y %H:%M", localtime(&now)); + strftime(_fmtText, sizeof(_fmtText), i18n_date_format[_display_language], localtime(&now)); printText(_fmtText, 3); } _display->sendBuffer(); diff --git a/src/WebApi_device.cpp b/src/WebApi_device.cpp index 4c226b8b..0e7f114e 100644 --- a/src/WebApi_device.cpp +++ b/src/WebApi_device.cpp @@ -80,6 +80,7 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request) display["power_safe"] = config.Display_PowerSafe; display["screensaver"] = config.Display_ScreenSaver; display["contrast"] = config.Display_Contrast; + display["language"] = config.Display_Language; JsonObject victronPinObj = curPin.createNestedObject("victron"); victronPinObj[F("rx")] = pin.victron_rx; @@ -165,11 +166,13 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request) config.Display_PowerSafe = root["display"]["power_safe"].as(); config.Display_ScreenSaver = root["display"]["screensaver"].as(); config.Display_Contrast = root["display"]["contrast"].as(); + config.Display_Language = root["display"]["language"].as(); Display.setOrientation(config.Display_Rotation); Display.enablePowerSafe = config.Display_PowerSafe; Display.enableScreensaver = config.Display_ScreenSaver; Display.setContrast(config.Display_Contrast); + Display.setLanguage(config.Display_Language); Configuration.write(); diff --git a/src/main.cpp b/src/main.cpp index 83c76613..e3e9ec74 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -133,6 +133,7 @@ void setup() Display.enablePowerSafe = config.Display_PowerSafe; Display.enableScreensaver = config.Display_ScreenSaver; Display.setContrast(config.Display_Contrast); + Display.setLanguage(config.Display_Language); Display.setStartupDisplay(); MessageOutput.println("done"); diff --git a/webapp/package.json b/webapp/package.json index 396ff5e3..9c7096e4 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -34,7 +34,7 @@ "eslint-plugin-vue": "^9.14.0", "npm-run-all": "^4.1.5", "sass": "^1.62.1", - "terser": "^5.17.5", + "terser": "^5.17.6", "typescript": "^5.0.4", "vite": "^4.3.8", "vite-plugin-compression": "^0.5.1", diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 09689d94..120e430c 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -661,6 +661,10 @@ "Rotation": "Rotation:", "rot0": "Keine Rotation", "rot90": "90 Grad Drehung", + "DisplayLanguage": "Displaysprache:", + "en": "Englisch", + "de": "Deutsch", + "fr": "Französisch", "rot180": "180 Grad Drehung", "rot270": "270 Grad Drehung", "Save": "@:dtuadmin.Save" diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 083ac16e..91c59d59 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -667,6 +667,10 @@ "rot90": "90 degree rotation", "rot180": "180 degree rotation", "rot270": "270 degree rotation", + "DisplayLanguage": "Display language:", + "en": "English", + "de": "German", + "fr": "French", "Save": "@:dtuadmin.Save" }, "pininfo": { diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index fce2d3d9..fb414e4e 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -590,6 +590,10 @@ "rot90": "90 degree rotation", "rot180": "180 degree rotation", "rot270": "270 degree rotation", + "DisplayLanguage": "Langue d'affichage", + "en": "Anglais", + "de": "Allemand", + "fr": "Français", "Save": "@:dtuadmin.Save" }, "pininfo": { diff --git a/webapp/src/types/DeviceConfig.ts b/webapp/src/types/DeviceConfig.ts index 8bd87f70..8b77c5b7 100644 --- a/webapp/src/types/DeviceConfig.ts +++ b/webapp/src/types/DeviceConfig.ts @@ -5,6 +5,7 @@ export interface Display { power_safe: boolean; screensaver: boolean; contrast: number; + language: number; } export interface DeviceConfig { diff --git a/webapp/src/views/DeviceAdminView.vue b/webapp/src/views/DeviceAdminView.vue index 574105e9..090bcfb7 100644 --- a/webapp/src/views/DeviceAdminView.vue +++ b/webapp/src/views/DeviceAdminView.vue @@ -56,6 +56,19 @@ v-model="deviceConfigList.display.screensaver" type="checkbox" :tooltip="$t('deviceadmin.ScreensaverHint')" /> +
+ +
+ +
+
+