diff --git a/README.md b/README.md index 5e99dc91..b5408ab3 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,10 @@ This project is still under development and adds following features: [![OpenDTU Build](https://github.com/tbnobody/OpenDTU/actions/workflows/build.yml/badge.svg)](https://github.com/tbnobody/OpenDTU/actions/workflows/build.yml) [![cpplint](https://github.com/tbnobody/OpenDTU/actions/workflows/cpplint.yml/badge.svg)](https://github.com/tbnobody/OpenDTU/actions/workflows/cpplint.yml) +## !! IMPORTANT UPGRADE NOTES !! + +If you are upgrading from a version before 15.03.2023 you have to upgrade the partition table of the ESP32. Please follow the [this](docs/UpgradePartition.md) documentation! + ## Background This project was started from [this](https://www.mikrocontroller.net/topic/525778) discussion (Mikrocontroller.net). It was the goal to replace the original Hoymiles DTU (Telemetry Gateway) with their cloud access. With a lot of reverse engineering the Hoymiles protocol was decrypted and analyzed. @@ -235,6 +239,7 @@ Firmware version seems to play not a significant role and cannot be read from th ## Breaking changes Generated using: `git log --date=short --pretty=format:"* %h%x09%ad%x09%s" | grep BREAKING` ``` +* 318136d 2023-03-15 BREAKING CHANGE: Updated partition table: Make sure you have a configuration backup and completly reflash the device! * 3b7aef6 2023-02-13 BREAKING CHANGE: Web API! * d4c838a 2023-02-06 BREAKING CHANGE: Prometheus API! * daf847e 2022-11-14 BREAKING CHANGE: Removed deprecated config parsing method diff --git a/docs/UpgradePartition.md b/docs/UpgradePartition.md new file mode 100644 index 00000000..dce998ae --- /dev/null +++ b/docs/UpgradePartition.md @@ -0,0 +1,24 @@ +# Upgrade Partition + +To be able to install further updates you have to update the partition table of the ESP32. Doing so will **erase** all configuration data. Over The Air update using the web interface is **NOT** possible! + +**So make sure you export a backup of your configuration files before continuing.** + +There are several possibilities to update the partition table: +- Using Visual Studio Code or PlatformIO CLI + + If you have already used Visual Studio Code or the `platformio` command you can use it again to install the latest version. The partition table is upgraded automatically. + +- Any kind of flash interface + + If you like to use any kind of flash interface like `esptool.py`, Espressif Flash Download Tool, ESP_Flasher or esptool-js you have to make sure to upload **ALL** provided .bin files. It is important to enter the correct target addresses. + + | Address | File | + | ---------| ---------------------- | + | 0x1000 | bootloader.bin | + | 0x8000 | partitions.bin | + | 0xe000 | boot_app0.bin | + | 0x10000 | opendtu-*.bin | + + +After upgrading the ESP32 will open the intergrated access point (AP) again. Just connect to it using the default password ("openDTU42"). If you are connected, just visit http://192.168.4.1 and enter the "Configuration Management". Recover the previously backuped config files. \ No newline at end of file diff --git a/include/Configuration.h b/include/Configuration.h index f0cceb74..59687612 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -101,6 +101,7 @@ struct CONFIG_T { bool PowerLimiter_Enabled; bool PowerLimiter_SolarPassTroughEnabled; + uint8_t PowerLimiter_BatteryDrainStategy; uint32_t PowerLimiter_Interval; char PowerLimiter_MqttTopicPowerMeter1[MQTT_MAX_TOPIC_STRLEN + 1]; char PowerLimiter_MqttTopicPowerMeter2[MQTT_MAX_TOPIC_STRLEN + 1]; @@ -108,10 +109,10 @@ struct CONFIG_T { bool PowerLimiter_IsInverterBehindPowerMeter; uint8_t PowerLimiter_InverterId; uint8_t PowerLimiter_InverterChannelId; - uint32_t PowerLimiter_TargetPowerConsumption; - uint32_t PowerLimiter_TargetPowerConsumptionHysteresis; - uint32_t PowerLimiter_LowerPowerLimit; - uint32_t PowerLimiter_UpperPowerLimit; + int32_t PowerLimiter_TargetPowerConsumption; + int32_t PowerLimiter_TargetPowerConsumptionHysteresis; + int32_t PowerLimiter_LowerPowerLimit; + int32_t PowerLimiter_UpperPowerLimit; uint32_t PowerLimiter_BatterySocStartThreshold; uint32_t PowerLimiter_BatterySocStopThreshold; float PowerLimiter_VoltageStartThreshold; @@ -128,7 +129,7 @@ struct CONFIG_T { bool Display_PowerSafe; bool Display_ScreenSaver; - bool Display_ShowLogo; + uint8_t Display_Rotation; uint8_t Display_Contrast; }; diff --git a/include/Display_Graphic.h b/include/Display_Graphic.h index 13f6c62d..18c1c3aa 100644 --- a/include/Display_Graphic.h +++ b/include/Display_Graphic.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once +#include "defaults.h" #include enum DisplayType_t { @@ -17,25 +18,29 @@ public: void init(DisplayType_t type, uint8_t data, uint8_t clk, uint8_t cs, uint8_t reset); void loop(); + void setContrast(uint8_t contrast); + void setOrientation(uint8_t rotation = DISPLAY_ROTATION); + void setStartupDisplay(); bool enablePowerSafe = true; bool enableScreensaver = true; - bool showLogo = true; - uint8_t contrast = 60; private: void printText(const char* text, uint8_t line); + void calcLineHeights(); + void setFont(uint8_t line); U8G2* _display; DisplayType_t _display_type = DisplayType_t::None; uint8_t _mExtra; - uint16_t _dispY = 0; uint16_t _period = 1000; uint16_t _interval = 60000; // interval at which to power save (milliseconds) uint32_t _lastDisplayUpdate = 0; uint32_t _previousMillis = 0; char _fmtText[32]; + bool _isLarge = false; + uint8_t _lineOffsets[5]; }; extern DisplayGraphicClass Display; \ No newline at end of file diff --git a/include/PinMapping.h b/include/PinMapping.h index 920be6c8..de916955 100644 --- a/include/PinMapping.h +++ b/include/PinMapping.h @@ -2,8 +2,8 @@ #pragma once #include -#include #include +#include #define PINMAPPING_FILENAME "/pin_mapping.json" diff --git a/include/PowerLimiter.h b/include/PowerLimiter.h index 35d66c32..a2ad7ebf 100644 --- a/include/PowerLimiter.h +++ b/include/PowerLimiter.h @@ -13,6 +13,11 @@ typedef enum { STATE_CONSUME_SOLAR_POWER_ONLY, STATE_NORMAL_OPERATION } plStates; + +typedef enum { + EMPTY_WHEN_FULL= 0, + EMPTY_AT_NIGTH +} batDrainStrategy; class PowerLimiterClass { @@ -20,14 +25,14 @@ public: void init(); void loop(); plStates getPowerLimiterState(); - uint16_t getLastRequestedPowewrLimit(); + int32_t getLastRequestedPowewrLimit(); void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total); private: uint32_t _lastCommandSent; uint32_t _lastLoop; uint32_t _lastPowerMeterUpdate; - uint16_t _lastRequestedPowerLimit; + int32_t _lastRequestedPowerLimit; plStates _plState = STATE_DISCOVER; float _powerMeter1Power; @@ -36,8 +41,8 @@ private: bool canUseDirectSolarPower(); int32_t calcPowerLimit(std::shared_ptr inverter, bool consumeSolarPowerOnly); - void setNewPowerLimit(std::shared_ptr inverter, uint32_t newPowerLimit); - uint16_t getDirectSolarPower(); + void setNewPowerLimit(std::shared_ptr inverter, int32_t newPowerLimit); + int32_t getDirectSolarPower(); float getLoadCorrectedVoltage(std::shared_ptr inverter); bool isStartThresholdReached(std::shared_ptr inverter); bool isStopThresholdReached(std::shared_ptr inverter); diff --git a/include/WebApi_prometheus.h b/include/WebApi_prometheus.h index 5eb894ee..f2b5b66a 100644 --- a/include/WebApi_prometheus.h +++ b/include/WebApi_prometheus.h @@ -15,6 +15,8 @@ private: void addField(AsyncResponseStream* stream, String& serial, uint8_t idx, std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, const char* channelName = NULL); + void addPanelInfo(AsyncResponseStream* stream, String& serial, uint8_t idx, std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel); + AsyncWebServer* _server; enum { diff --git a/include/defaults.h b/include/defaults.h index 7fffe192..163f0565 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -85,7 +85,7 @@ #define DISPLAY_POWERSAFE true #define DISPLAY_SCREENSAVER true -#define DISPLAY_SHOWLOGO true +#define DISPLAY_ROTATION 2 #define DISPLAY_CONTRAST 60 #define VEDIRECT_ENABLED false @@ -94,6 +94,7 @@ #define POWERLIMITER_ENABLED false #define POWERLIMITER_SOLAR_PASSTROUGH_ENABLED true +#define POWERLIMITER_BATTERY_DRAIN_STRATEGY 0 #define POWERLIMITER_INTERVAL 10 #define POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER true #define POWERLIMITER_INVERTER_ID 0 diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHandler.h b/lib/VeDirectFrameHandler/VeDirectFrameHandler.h index fc732c0c..df97dc49 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHandler.h +++ b/lib/VeDirectFrameHandler/VeDirectFrameHandler.h @@ -33,16 +33,16 @@ typedef struct { uint8_t ERR; // error code uint32_t OR; // off reason uint8_t MPPT; // state of MPP tracker - uint16_t HSDS; // day sequence number 1...365 + uint32_t HSDS; // day sequence number 1...365 double V; // battery voltage in V double I; // battery current in A double VPV; // panel voltage in V - uint16_t PPV; // panel power in W + int32_t PPV; // panel power in W double H19; // yield total kWh double H20; // yield today kWh - uint16_t H21; // maximum power today W + int32_t H21; // maximum power today W double H22; // yield yesterday kWh - uint16_t H23; // maximum power yesterday W + int32_t H23; // maximum power yesterday W } veStruct; class VeDirectFrameHandler { diff --git a/partitions_custom.csv b/partitions_custom.csv index abf9a200..d18ab18f 100644 --- a/partitions_custom.csv +++ b/partitions_custom.csv @@ -1,6 +1,6 @@ -# Name, Type, SubType, Offset, Size, Flags -nvs, data, nvs, 0x9000, 0x5000, -otadata, data, ota, 0xe000, 0x2000, -app0, app, ota_0, 0x10000, 0x180000, -app1, app, ota_1, 0x190000,0x180000, -spiffs, data, spiffs, ,0x50000 \ No newline at end of file +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +app0, app, ota_0, 0x10000, 0x1E0000, +app1, app, ota_1, 0x1F0000, 0x1E0000, +spiffs, data, spiffs, 0x3D0000, 0x30000, \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index ac78a9bb..538f64a5 100644 --- a/platformio.ini +++ b/platformio.ini @@ -23,8 +23,8 @@ build_flags = lib_deps = https://github.com/yubox-node-org/ESPAsyncWebServer - bblanchon/ArduinoJson @ ^6.20.1 - https://github.com/bertmelis/espMqttClient.git#v1.3.3 + bblanchon/ArduinoJson @ ^6.21.0 + https://github.com/bertmelis/espMqttClient.git#v1.4.1 nrf24/RF24 @ ^1.4.5 olikraus/U8g2 @ ^2.34.13 buelowp/sunset @ ^1.1.7 diff --git a/src/Configuration.cpp b/src/Configuration.cpp index bf5b70d2..5797f11f 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -88,7 +88,7 @@ bool ConfigurationClass::write() JsonObject display = device.createNestedObject("display"); display["powersafe"] = config.Display_PowerSafe; display["screensaver"] = config.Display_ScreenSaver; - display["showlogo"] = config.Display_ShowLogo; + display["rotation"] = config.Display_Rotation; display["contrast"] = config.Display_Contrast; JsonArray inverters = doc.createNestedArray("inverters"); @@ -118,6 +118,7 @@ bool ConfigurationClass::write() JsonObject powerlimiter = doc.createNestedObject("powerlimiter"); powerlimiter["enabled"] = config.PowerLimiter_Enabled; powerlimiter["solar_passtrough_enabled"] = config.PowerLimiter_SolarPassTroughEnabled; + powerlimiter["battery_drain_strategy"] = config.PowerLimiter_BatteryDrainStategy; powerlimiter["interval"] = config.PowerLimiter_Interval; powerlimiter["mqtt_topic_powermeter_1"] = config.PowerLimiter_MqttTopicPowerMeter1; powerlimiter["mqtt_topic_powermeter_2"] = config.PowerLimiter_MqttTopicPowerMeter2; @@ -256,7 +257,7 @@ bool ConfigurationClass::read() JsonObject display = device["display"]; config.Display_PowerSafe = display["powersafe"] | DISPLAY_POWERSAFE; config.Display_ScreenSaver = display["screensaver"] | DISPLAY_SCREENSAVER; - config.Display_ShowLogo = display["showlogo"] | DISPLAY_SHOWLOGO; + config.Display_Rotation = display["rotation"] | DISPLAY_ROTATION; config.Display_Contrast = display["contrast"] | DISPLAY_CONTRAST; JsonArray inverters = doc["inverters"]; @@ -286,6 +287,7 @@ bool ConfigurationClass::read() JsonObject powerlimiter = doc["powerlimiter"]; config.PowerLimiter_Enabled = powerlimiter["enabled"] | POWERLIMITER_ENABLED; config.PowerLimiter_SolarPassTroughEnabled = powerlimiter["solar_passtrough_enabled"] | POWERLIMITER_SOLAR_PASSTROUGH_ENABLED; + config.PowerLimiter_BatteryDrainStategy = powerlimiter["battery_drain_strategy"] | POWERLIMITER_BATTERY_DRAIN_STRATEGY; config.PowerLimiter_Interval = POWERLIMITER_INTERVAL; strlcpy(config.PowerLimiter_MqttTopicPowerMeter1, powerlimiter["mqtt_topic_powermeter_1"] | "", sizeof(config.PowerLimiter_MqttTopicPowerMeter1)); strlcpy(config.PowerLimiter_MqttTopicPowerMeter2, powerlimiter["mqtt_topic_powermeter_2"] | "", sizeof(config.PowerLimiter_MqttTopicPowerMeter2)); diff --git a/src/Display_Graphic.cpp b/src/Display_Graphic.cpp index 23c826ea..eac532cc 100644 --- a/src/Display_Graphic.cpp +++ b/src/Display_Graphic.cpp @@ -5,31 +5,6 @@ #include #include -static uint8_t bmp_logo[] PROGMEM = { - B00000000, B00000000, // ................ - B11101100, B00110111, // ..##.######.##.. - B11101100, B00110111, // ..##.######.##.. - B11100000, B00000111, // .....######..... - B11010000, B00001011, // ....#.####.#.... - B10011000, B00011001, // ...##..##..##... - B10000000, B00000001, // .......##....... - B00000000, B00000000, // ................ - B01111000, B00011110, // ...####..####... - B11111100, B00111111, // ..############.. - B01111100, B00111110, // ..#####..#####.. - B00000000, B00000000, // ................ - B11111100, B00111111, // ..############.. - B11111110, B01111111, // .##############. - B01111110, B01111110, // .######..######. - B00000000, B00000000 // ................ -}; - -static uint8_t bmp_arrow[] PROGMEM = { - B00000000, B00011100, B00011100, B00001110, B00001110, B11111110, B01111111, - B01110000, B01110000, B00110000, B00111000, B00011000, B01111111, B00111111, - B00011110, B00001110, B00000110, B00000000, B00000000, B00000000, B00000000 -}; - std::map> display_types = { { DisplayType_t::PCD8544, [](uint8_t reset, uint8_t clock, uint8_t data, uint8_t cs) { return new U8G2_PCD8544_84X48_F_4W_HW_SPI(U8G2_R0, cs, data, reset); } }, { DisplayType_t::SSD1306, [](uint8_t reset, uint8_t clock, uint8_t data, uint8_t cs) { return new U8G2_SSD1306_128X64_NONAME_F_HW_I2C(U8G2_R0, reset, clock, data); } }, @@ -52,56 +27,83 @@ void DisplayGraphicClass::init(DisplayType_t type, uint8_t data, uint8_t clk, ui auto constructor = display_types[_display_type]; _display = constructor(reset, clk, data, cs); _display->begin(); + setContrast(DISPLAY_CONTRAST); + } +} + +void DisplayGraphicClass::calcLineHeights() +{ + uint8_t yOff = 0; + for (uint8_t i = 0; i < 4; i++) { + setFont(i); + yOff += (_display->getMaxCharHeight()); + _lineOffsets[i] = yOff; + } +} + +void DisplayGraphicClass::setFont(uint8_t line) +{ + switch (line) { + case 0: + _display->setFont((_isLarge) ? u8g2_font_ncenB14_tr : u8g2_font_logisoso16_tr); + break; + case 3: + _display->setFont(u8g2_font_5x8_tr); + break; + default: + _display->setFont((_isLarge) ? u8g2_font_ncenB10_tr : u8g2_font_5x8_tr); + break; } } void DisplayGraphicClass::printText(const char* text, uint8_t line) { - // get the width and height of the display - uint16_t maxWidth = _display->getWidth(); - uint16_t maxHeight = _display->getHeight(); - - // pxMovement +x (0 - 6 px) - uint8_t ex = enableScreensaver ? (_mExtra % 7) : 0; - - // set the font size based on the display size - switch (line) { - case 1: - if (maxWidth > 120 && maxHeight > 60) { - _display->setFont(u8g2_font_ncenB14_tr); // large display - } else { - _display->setFont(u8g2_font_logisoso16_tr); // small display - } - break; - case 4: - if (maxWidth > 120 && maxHeight > 60) { - _display->setFont(u8g2_font_5x8_tr); // large display - } else { - _display->setFont(u8g2_font_5x8_tr); // small display - } - break; - default: - if (maxWidth > 120 && maxHeight > 60) { - _display->setFont(u8g2_font_ncenB10_tr); // large display - } else { - _display->setFont(u8g2_font_5x8_tr); // small display - } - break; - } - - // get the font height, to calculate the textheight - _dispY += (_display->getMaxCharHeight()) + 1; - - // calculate the starting position of the text - uint16_t dispX; - if (line == 1) { - dispX = 20 + ex; + uint8_t dispX; + if (!_isLarge) { + dispX = (line == 0) ? 5 : 0; } else { - dispX = 5 + ex; + dispX = (line == 0) ? 20 : 5; + } + setFont(line); + + dispX += enableScreensaver ? (_mExtra % 7) : 0; + _display->drawStr(dispX, _lineOffsets[line], text); +} + +void DisplayGraphicClass::setOrientation(uint8_t rotation) +{ + if (_display_type == DisplayType_t::None) { + return; } - // draw the Text, on the calculated pos - _display->drawStr(dispX, _dispY, text); + switch (rotation) { + case 0: + _display->setDisplayRotation(U8G2_R0); + break; + case 1: + _display->setDisplayRotation(U8G2_R1); + break; + case 2: + _display->setDisplayRotation(U8G2_R2); + break; + case 3: + _display->setDisplayRotation(U8G2_R3); + break; + } + + _isLarge = (_display->getWidth() > 100); + calcLineHeights(); +} + +void DisplayGraphicClass::setStartupDisplay() +{ + if (_display_type == DisplayType_t::None) { + return; + } + + _display->clearBuffer(); + printText("OpenDTU!", 0); + _display->sendBuffer(); } void DisplayGraphicClass::loop() @@ -136,20 +138,6 @@ void DisplayGraphicClass::loop() _display->clearBuffer(); - // set Contrast of the Display to raise the lifetime - _display->setContrast(contrast); - - //=====> Logo and Lighting ========== - // pxMovement +x (0 - 6 px) - uint8_t ex = enableScreensaver ? (_mExtra % 7) : 0; - if (isprod > 0) { - _display->drawXBMP(5 + ex, 1, 8, 17, bmp_arrow); - if (showLogo) { - _display->drawXBMP(_display->getWidth() - 24 + ex, 2, 16, 16, bmp_logo); - } - } - //<======================= - //=====> Actual Production ========== if ((totalPower > 0) && (isprod > 0)) { _display->setPowerSave(false); @@ -158,14 +146,14 @@ void DisplayGraphicClass::loop() } else { snprintf(_fmtText, sizeof(_fmtText), "%3.0f W", totalPower); } - printText(_fmtText, 1); + printText(_fmtText, 0); _previousMillis = millis(); } //<======================= //=====> Offline =========== else { - printText("offline", 1); + printText("offline", 0); // check if it's time to enter power saving mode if (millis() - _previousMillis >= (_interval * 2)) { _display->setPowerSave(enablePowerSafe); @@ -175,27 +163,34 @@ void DisplayGraphicClass::loop() //=====> Today & Total Production ======= snprintf(_fmtText, sizeof(_fmtText), "today: %4.0f Wh", totalYieldDay); - printText(_fmtText, 2); + printText(_fmtText, 1); snprintf(_fmtText, sizeof(_fmtText), "total: %.1f kWh", totalYieldTotal); - printText(_fmtText, 3); + printText(_fmtText, 2); //<======================= //=====> IP or Date-Time ======== if (!(_mExtra % 10) && NetworkSettings.localIP()) { - printText(NetworkSettings.localIP().toString().c_str(), 4); + printText(NetworkSettings.localIP().toString().c_str(), 3); } else { // Get current time time_t now = time(nullptr); strftime(_fmtText, sizeof(_fmtText), "%a %d.%m.%Y %H:%M", localtime(&now)); - printText(_fmtText, 4); + printText(_fmtText, 3); } _display->sendBuffer(); - _dispY = 0; _mExtra++; _lastDisplayUpdate = millis(); } } +void DisplayGraphicClass::setContrast(uint8_t contrast) +{ + if (_display_type == DisplayType_t::None) { + return; + } + _display->setContrast(contrast * 2.55f); +} + DisplayGraphicClass Display; \ No newline at end of file diff --git a/src/MqttHandleInverter.cpp b/src/MqttHandleInverter.cpp index 3703630c..67196fec 100644 --- a/src/MqttHandleInverter.cpp +++ b/src/MqttHandleInverter.cpp @@ -126,7 +126,9 @@ void MqttHandleInverterClass::publishField(std::shared_ptr inv return; } - MqttSettings.publish(topic, String(inv->Statistics()->getChannelFieldValue(type, channel, fieldId))); + MqttSettings.publish(topic, String( + inv->Statistics()->getChannelFieldValue(type, channel, fieldId), + static_cast(inv->Statistics()->getChannelFieldDigits(type, channel, fieldId)))); } String MqttHandleInverterClass::getTopic(std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index 26dbcc08..56f0711b 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -117,13 +117,7 @@ void PowerLimiterClass::loop() if (inverter->isProducing()) { MessageOutput.printf("[PowerLimiterClass::loop] DC voltage: %.2f Corrected DC voltage: %.2f...\r\n", dcVoltage, correctedDcVoltage); - MessageOutput.println("[PowerLimiterClass::loop] Stopping inverter..."); - inverter->sendPowerControlRequest(Hoymiles.getRadio(), false); - - uint16_t newPowerLimit = (uint16_t)config.PowerLimiter_LowerPowerLimit; - inverter->sendActivePowerControlRequest(Hoymiles.getRadio(), newPowerLimit, PowerLimitControlType::AbsolutNonPersistent); - _lastRequestedPowerLimit = newPowerLimit; - _lastCommandSent = millis(); + setNewPowerLimit(inverter, -1); return; } @@ -131,54 +125,54 @@ void PowerLimiterClass::loop() if (isStopThresholdReached(inverter)) return; // check for possible state changes - if (isStartThresholdReached(inverter) && calcPowerLimit(inverter, false) >= config.PowerLimiter_LowerPowerLimit) { - _plState = STATE_NORMAL_OPERATION; - } - else if (canUseDirectSolarPower() && calcPowerLimit(inverter, true) >= config.PowerLimiter_LowerPowerLimit) { + if (canUseDirectSolarPower()) { _plState = STATE_CONSUME_SOLAR_POWER_ONLY; } - - // inverter on on state change - if (_plState != STATE_OFF) { - // DC voltage high enough, start the inverter - MessageOutput.println("[PowerLimiterClass::loop] Starting up inverter..."); - inverter->sendPowerControlRequest(Hoymiles.getRadio(), true); - _lastCommandSent = millis(); - return; + if (isStartThresholdReached(inverter)) { + _plState = STATE_NORMAL_OPERATION; } - else - return; + return; break; case STATE_CONSUME_SOLAR_POWER_ONLY: { int32_t newPowerLimit = calcPowerLimit(inverter, true); - if (!inverter->isProducing() - || isStopThresholdReached(inverter) - || newPowerLimit < config.PowerLimiter_LowerPowerLimit) { + if (isStopThresholdReached(inverter)) { _plState = STATE_OFF; break; } - else if (!canUseDirectSolarPower() || isStartThresholdReached(inverter)) { + if (isStartThresholdReached(inverter)) { _plState = STATE_NORMAL_OPERATION; break; } + + if (!canUseDirectSolarPower()) { + if (config.PowerLimiter_BatteryDrainStategy == EMPTY_AT_NIGTH) + _plState = STATE_NORMAL_OPERATION; + else + _plState = STATE_OFF; + break; + } + setNewPowerLimit(inverter, newPowerLimit); return; break; } case STATE_NORMAL_OPERATION: { int32_t newPowerLimit = calcPowerLimit(inverter, false); - if (!inverter->isProducing() - || isStopThresholdReached(inverter) - || newPowerLimit < config.PowerLimiter_LowerPowerLimit) { + if (isStopThresholdReached(inverter)) { _plState = STATE_OFF; break; } - // check if grid power consumption is within the upper an lower threshold of the target consumption - else if (newPowerLimit >= (config.PowerLimiter_TargetPowerConsumption - config.PowerLimiter_TargetPowerConsumptionHysteresis) && - newPowerLimit <= (config.PowerLimiter_TargetPowerConsumption + config.PowerLimiter_TargetPowerConsumptionHysteresis)) { - return; + if (canUseDirectSolarPower() && (config.PowerLimiter_BatteryDrainStategy == EMPTY_AT_NIGTH)) { + _plState = STATE_CONSUME_SOLAR_POWER_ONLY; + break; } - setNewPowerLimit(inverter, newPowerLimit); + + // check if grid power consumption is not within the upper and lower threshold of the target consumption + if (newPowerLimit >= (config.PowerLimiter_TargetPowerConsumption - config.PowerLimiter_TargetPowerConsumptionHysteresis) && + newPowerLimit <= (config.PowerLimiter_TargetPowerConsumption + config.PowerLimiter_TargetPowerConsumptionHysteresis)) { + return; + } + setNewPowerLimit(inverter, newPowerLimit);; return; break; } @@ -190,7 +184,7 @@ plStates PowerLimiterClass::getPowerLimiterState() { return _plState; } -uint16_t PowerLimiterClass::getLastRequestedPowewrLimit() { +int32_t PowerLimiterClass::getLastRequestedPowewrLimit() { return _lastRequestedPowerLimit; } @@ -203,7 +197,7 @@ bool PowerLimiterClass::canUseDirectSolarPower() return false; } - if (VeDirect.veFrame.PPV < 10.0) { + if (VeDirect.veFrame.PPV < 20) { // Not enough power return false; } @@ -215,13 +209,14 @@ int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr inve { CONFIG_T& config = Configuration.get(); - int32_t newPowerLimit = _powerMeter1Power + _powerMeter2Power + _powerMeter3Power; + int32_t newPowerLimit = round(_powerMeter1Power + _powerMeter2Power + _powerMeter3Power); float efficency = inverter->Statistics()->getChannelFieldValue(TYPE_AC, (ChannelNum_t) config.PowerLimiter_InverterChannelId, FLD_EFF); - uint32_t victronChargePower = this->getDirectSolarPower(); - uint32_t adjustedVictronChargePower = victronChargePower * (efficency > 0.0 ? (efficency / 100.0) : 1.0); // if inverter is off, use 1.0 + int32_t victronChargePower = this->getDirectSolarPower(); + int32_t adjustedVictronChargePower = victronChargePower * (efficency > 0.0 ? (efficency / 100.0) : 1.0); // if inverter is off, use 1.0 - MessageOutput.printf("[PowerLimiterClass::loop] victronChargePower: %d, efficiency: %.2f, consumeSolarPowerOnly: %s \r\n", victronChargePower, efficency, consumeSolarPowerOnly ? "true" : "false"); + MessageOutput.printf("[PowerLimiterClass::loop] victronChargePower: %d, efficiency: %.2f, consumeSolarPowerOnly: %s, powerConsumption: %d \r\n", + victronChargePower, efficency, consumeSolarPowerOnly ? "true" : "false", newPowerLimit); if (millis() - _lastPowerMeterUpdate < (30 * 1000)) { if (config.PowerLimiter_IsInverterBehindPowerMeter) { @@ -234,7 +229,7 @@ int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr inve newPowerLimit -= config.PowerLimiter_TargetPowerConsumption; - uint16_t upperPowerLimit = config.PowerLimiter_UpperPowerLimit; + int32_t upperPowerLimit = config.PowerLimiter_UpperPowerLimit; if (consumeSolarPowerOnly && (upperPowerLimit > adjustedVictronChargePower)) { // Battery voltage too low, use Victron solar power (corrected by efficency factor) only upperPowerLimit = adjustedVictronChargePower; @@ -247,18 +242,35 @@ int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr inve // set the limit to config.PowerLimiter_LowerPowerLimit for safety reasons. newPowerLimit = config.PowerLimiter_LowerPowerLimit; } + MessageOutput.printf("[PowerLimiterClass::loop] newPowerLimit: %d\r\n", newPowerLimit); return newPowerLimit; } -void PowerLimiterClass::setNewPowerLimit(std::shared_ptr inverter, uint32_t newPowerLimit) +void PowerLimiterClass::setNewPowerLimit(std::shared_ptr inverter, int32_t newPowerLimit) { - MessageOutput.printf("[PowerLimiterClass::loop] Limit Non-Persistent: %d W\r\n", newPowerLimit); - inverter->sendActivePowerControlRequest(Hoymiles.getRadio(), newPowerLimit, PowerLimitControlType::AbsolutNonPersistent); - _lastRequestedPowerLimit = newPowerLimit; - _lastCommandSent = millis(); + if(_lastRequestedPowerLimit != newPowerLimit) { + CONFIG_T& config = Configuration.get(); + + // if limit too low turn inverter offf + if (newPowerLimit < config.PowerLimiter_LowerPowerLimit) { + if (inverter->isProducing()) { + MessageOutput.println("[PowerLimiterClass::loop] Stopping inverter..."); + inverter->sendPowerControlRequest(Hoymiles.getRadio(), false); + _lastCommandSent = millis(); + } + newPowerLimit = config.PowerLimiter_LowerPowerLimit; + } else if (!inverter->isProducing()) { + MessageOutput.println("[PowerLimiterClass::loop] Starting up inverter..."); + inverter->sendPowerControlRequest(Hoymiles.getRadio(), true); + _lastCommandSent = millis(); + } + MessageOutput.printf("[PowerLimiterClass::loop] Limit Non-Persistent: %d W\r\n", newPowerLimit); + inverter->sendActivePowerControlRequest(Hoymiles.getRadio(), newPowerLimit, PowerLimitControlType::AbsolutNonPersistent); + _lastRequestedPowerLimit = newPowerLimit; + } } -uint16_t PowerLimiterClass::getDirectSolarPower() +int32_t PowerLimiterClass::getDirectSolarPower() { if (!canUseDirectSolarPower()) { return 0; diff --git a/src/PylontechCanReceiver.cpp b/src/PylontechCanReceiver.cpp index 19c50f44..ea013627 100644 --- a/src/PylontechCanReceiver.cpp +++ b/src/PylontechCanReceiver.cpp @@ -31,16 +31,16 @@ void PylontechCanReceiverClass::enable() // Install TWAI driver if (twai_driver_install(&g_config, &t_config, &f_config) == ESP_OK) { - MessageOutput.printf("Driver installed\n"); + MessageOutput.printf("[Pylontech] Twai driver installed\n"); } else { - MessageOutput.printf("Failed to install driver\n"); + MessageOutput.printf("[Pylontech] Failed to install Twai driver\n"); } // Start TWAI driver if (twai_start() == ESP_OK) { - MessageOutput.printf("Driver started\n"); + MessageOutput.printf("[Pylontech] Twai driver started\n"); } else { - MessageOutput.printf("Failed to start driver\n"); + MessageOutput.printf("[Pylontech] Failed to start Twai driver\n"); } } @@ -102,7 +102,7 @@ void PylontechCanReceiverClass::parseCanPackets() // Check for messages. twai_recive is blocking when there is no data so we return if there are no frames in the buffer twai_status_info_t status_info; if (twai_get_status_info(&status_info) != ESP_OK) { - MessageOutput .printf("Failed to get status info\n"); + MessageOutput.printf("[Pylontech]Failed to get Twai status info\n"); return; } if (status_info.msgs_to_rx == 0) { @@ -112,7 +112,7 @@ void PylontechCanReceiverClass::parseCanPackets() // Wait for message to be received, function is blocking twai_message_t rx_message; if (twai_receive(&rx_message, pdMS_TO_TICKS(100)) != ESP_OK) { - MessageOutput.printf("Failed to receive message\n"); + MessageOutput.printf("[Pylontech] Failed to receive message\n"); return; } @@ -201,7 +201,6 @@ void PylontechCanReceiverClass::parseCanPackets() } case 0x35E: { - String manufacturer = String(rx_message.data, rx_message.data_length_code); //CAN.readString(); @@ -230,7 +229,6 @@ void PylontechCanReceiverClass::parseCanPackets() Battery.chargeImmediately); #endif - // this->readUnsignedInt8(); break; } } diff --git a/src/WebApi_device.cpp b/src/WebApi_device.cpp index 2cc191cf..1a4674d7 100644 --- a/src/WebApi_device.cpp +++ b/src/WebApi_device.cpp @@ -64,7 +64,7 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request) displayPinObj[F("reset")] = pin.display_reset; JsonObject display = root.createNestedObject("display"); - display[F("show_logo")] = config.Display_ShowLogo; + display[F("rotation")] = config.Display_Rotation; display[F("power_safe")] = config.Display_PowerSafe; display[F("screensaver")] = config.Display_ScreenSaver; display[F("contrast")] = config.Display_Contrast; @@ -141,15 +141,15 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request) bool performRestart = root[F("curPin")][F("name")].as() != config.Dev_PinMapping; strlcpy(config.Dev_PinMapping, root[F("curPin")][F("name")].as().c_str(), sizeof(config.Dev_PinMapping)); - config.Display_ShowLogo = root[F("display")][F("show_logo")].as(); + config.Display_Rotation = root[F("display")][F("rotation")].as(); config.Display_PowerSafe = root[F("display")][F("power_safe")].as(); config.Display_ScreenSaver = root[F("display")][F("screensaver")].as(); config.Display_Contrast = root[F("display")][F("contrast")].as(); - Display.showLogo = config.Display_ShowLogo; + Display.setOrientation(config.Display_Rotation); Display.enablePowerSafe = config.Display_PowerSafe; Display.enableScreensaver = config.Display_ScreenSaver; - Display.contrast = config.Display_Contrast; + Display.setContrast(config.Display_Contrast); Configuration.write(); diff --git a/src/WebApi_powerlimiter.cpp b/src/WebApi_powerlimiter.cpp index 05e75ad8..08f08355 100644 --- a/src/WebApi_powerlimiter.cpp +++ b/src/WebApi_powerlimiter.cpp @@ -37,6 +37,7 @@ void WebApiPowerLimiterClass::onStatus(AsyncWebServerRequest* request) root[F("enabled")] = config.PowerLimiter_Enabled; root[F("solar_passtrough_enabled")] = config.PowerLimiter_SolarPassTroughEnabled; + root[F("battery_drain_strategy")] = config.PowerLimiter_BatteryDrainStategy; root[F("mqtt_topic_powermeter_1")] = config.PowerLimiter_MqttTopicPowerMeter1; root[F("mqtt_topic_powermeter_2")] = config.PowerLimiter_MqttTopicPowerMeter2; root[F("mqtt_topic_powermeter_3")] = config.PowerLimiter_MqttTopicPowerMeter3; @@ -120,6 +121,7 @@ void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request) CONFIG_T& config = Configuration.get(); config.PowerLimiter_Enabled = root[F("enabled")].as(); config.PowerLimiter_SolarPassTroughEnabled = root[F("solar_passtrough_enabled")].as(); + config.PowerLimiter_BatteryDrainStategy= root[F("battery_drain_strategy")].as(); strlcpy(config.PowerLimiter_MqttTopicPowerMeter1, root[F("mqtt_topic_powermeter_1")].as().c_str(), sizeof(config.PowerLimiter_MqttTopicPowerMeter1)); strlcpy(config.PowerLimiter_MqttTopicPowerMeter2, root[F("mqtt_topic_powermeter_2")].as().c_str(), sizeof(config.PowerLimiter_MqttTopicPowerMeter2)); strlcpy(config.PowerLimiter_MqttTopicPowerMeter3, root[F("mqtt_topic_powermeter_3")].as().c_str(), sizeof(config.PowerLimiter_MqttTopicPowerMeter3)); diff --git a/src/WebApi_prometheus.cpp b/src/WebApi_prometheus.cpp index 1a92d31f..cef15e59 100644 --- a/src/WebApi_prometheus.cpp +++ b/src/WebApi_prometheus.cpp @@ -70,6 +70,7 @@ void WebApiPrometheusClass::onPrometheusMetricsGet(AsyncWebServerRequest* reques if (inv->Statistics()->getLastUpdate() > 0) { for (auto& t : inv->Statistics()->getChannelTypes()) { for (auto& c : inv->Statistics()->getChannelsByType(t)) { + addPanelInfo(stream, serial, i, inv, t, c); addField(stream, serial, i, inv, t, c, FLD_PAC); addField(stream, serial, i, inv, t, c, FLD_UAC); addField(stream, serial, i, inv, t, c, FLD_IAC); @@ -119,4 +120,50 @@ void WebApiPrometheusClass::addField(AsyncResponseStream* stream, String& serial channel, inv->Statistics()->getChannelFieldValue(type, channel, fieldId)); } -} \ No newline at end of file +} + +void WebApiPrometheusClass::addPanelInfo(AsyncResponseStream* stream, String& serial, uint8_t idx, std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel) +{ + if (type != TYPE_DC) { + return; + } + + const CONFIG_T& config = Configuration.get(); + + const bool printHelp = (idx == 0 && channel == 0); + if (printHelp) { + stream->print(F("# HELP opendtu_PanelInfo panel information\n")); + stream->print(F("# TYPE opendtu_PanelInfo gauge\n")); + } + stream->printf("opendtu_PanelInfo{serial=\"%s\",unit=\"%d\",name=\"%s\",channel=\"%d\",panelname=\"%s\"} 1\n", + serial.c_str(), + idx, + inv->name(), + channel, + config.Inverter[idx].channel[channel].Name + ); + + if (printHelp) { + stream->print(F("# HELP opendtu_MaxPower panel maximum output power\n")); + stream->print(F("# TYPE opendtu_MaxPower gauge\n")); + } + stream->printf("opendtu_MaxPower{serial=\"%s\",unit=\"%d\",name=\"%s\",channel=\"%d\"} %d\n", + serial.c_str(), + idx, + inv->name(), + channel, + config.Inverter[idx].channel[channel].MaxChannelPower + ); + + if (printHelp) { + stream->print(F("# HELP opendtu_YieldTotalOffset panel yield offset (for used inverters)\n")); + stream->print(F("# TYPE opendtu_YieldTotalOffset gauge\n")); + } + stream->printf("opendtu_YieldTotalOffset{serial=\"%s\",unit=\"%d\",name=\"%s\",channel=\"%d\"} %f\n", + serial.c_str(), + idx, + inv->name(), + channel, + config.Inverter[idx].channel[channel].YieldTotalOffset + ); +} diff --git a/src/main.cpp b/src/main.cpp index 083c6ba9..4b3dd00a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -117,10 +117,11 @@ void setup() pin.display_clk, pin.display_cs, pin.display_reset); - Display.showLogo = config.Display_ShowLogo; + Display.setOrientation(config.Display_Rotation); Display.enablePowerSafe = config.Display_PowerSafe; Display.enableScreensaver = config.Display_ScreenSaver; - Display.contrast = config.Display_Contrast; + Display.setContrast(config.Display_Contrast); + Display.setStartupDisplay(); MessageOutput.println(F("done")); // Check for default DTU serial diff --git a/webapp/package.json b/webapp/package.json index 538e91a7..27bad5ac 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -13,7 +13,7 @@ "dependencies": { "@popperjs/core": "^2.11.6", "bootstrap": "^5.3.0-alpha1", - "bootstrap-icons-vue": "^1.8.1", + "bootstrap-icons-vue": "^1.10.3", "mitt": "^3.0.0", "spark-md5": "^3.0.2", "vue": "^3.2.47", @@ -21,23 +21,23 @@ "vue-router": "^4.1.6" }, "devDependencies": { - "@intlify/unplugin-vue-i18n": "^0.8.2", + "@intlify/unplugin-vue-i18n": "^0.9.2", "@rushstack/eslint-patch": "^1.2.0", "@types/bootstrap": "^5.2.6", - "@types/node": "^18.14.6", + "@types/node": "^18.15.3", "@types/spark-md5": "^3.0.2", - "@vitejs/plugin-vue": "^4.0.0", + "@vitejs/plugin-vue": "^4.1.0", "@vue/eslint-config-typescript": "^11.0.2", "@vue/tsconfig": "^0.1.3", - "eslint": "^8.35.0", + "eslint": "^8.36.0", "eslint-plugin-vue": "^9.9.0", "npm-run-all": "^4.1.5", - "sass": "^1.58.3", - "terser": "^5.16.5", + "sass": "^1.59.3", + "terser": "^5.16.6", "typescript": "^4.9.5", - "vite": "^4.1.4", + "vite": "^4.2.0", "vite-plugin-compression": "^0.5.1", - "vite-plugin-css-injected-by-js": "^3.0.1", + "vite-plugin-css-injected-by-js": "^3.1.0", "vue-tsc": "^1.2.0" } } diff --git a/webapp/src/components/PinInfo.vue b/webapp/src/components/PinInfo.vue index 45d4ccaa..0770e3d5 100644 --- a/webapp/src/components/PinInfo.vue +++ b/webapp/src/components/PinInfo.vue @@ -11,126 +11,21 @@ - - NRF24 - MISO - {{ selectedPinAssignment?.nrf24?.miso }} - {{ currentPinAssignment?.nrf24?.miso }} - - - MOSI - {{ selectedPinAssignment?.nrf24?.mosi }} - {{ currentPinAssignment?.nrf24?.mosi }} - - - CLK - {{ selectedPinAssignment?.nrf24?.clk }} - {{ currentPinAssignment?.nrf24?.clk }} - - - IRQ - {{ selectedPinAssignment?.nrf24?.irq }} - {{ currentPinAssignment?.nrf24?.irq }} - - - EN - {{ selectedPinAssignment?.nrf24?.en }} - {{ currentPinAssignment?.nrf24?.en }} - - - CS - {{ selectedPinAssignment?.nrf24?.cs }} - {{ currentPinAssignment?.nrf24?.cs }} - - - - Ethernet - enabled - {{ selectedPinAssignment?.eth?.enabled }} - {{ currentPinAssignment?.eth?.enabled }} - - - phy_addr - {{ selectedPinAssignment?.eth?.phy_addr }} - {{ currentPinAssignment?.eth?.phy_addr }} - - - power - {{ selectedPinAssignment?.eth?.power }} - {{ currentPinAssignment?.eth?.power }} - - - mdc - {{ selectedPinAssignment?.eth?.mdc }} - {{ currentPinAssignment?.eth?.mdc }} - - - mdio - {{ selectedPinAssignment?.eth?.mdio }} - {{ currentPinAssignment?.eth?.mdio }} - - - type - {{ selectedPinAssignment?.eth?.type }} - {{ currentPinAssignment?.eth?.type }} - - - clk_mode - {{ selectedPinAssignment?.eth?.clk_mode }} - {{ currentPinAssignment?.eth?.clk_mode }} - - - - Display - type - {{ selectedPinAssignment?.display?.type }} - {{ currentPinAssignment?.display?.type }} - - - data - {{ selectedPinAssignment?.display?.data }} - {{ currentPinAssignment?.display?.data }} - - - clk - {{ selectedPinAssignment?.display?.clk }} - {{ currentPinAssignment?.display?.clk }} - - - cs - {{ selectedPinAssignment?.display?.cs }} - {{ currentPinAssignment?.display?.cs }} - - - reset - {{ selectedPinAssignment?.display?.reset }} - {{ currentPinAssignment?.display?.reset }} - - - - Victron - RX - {{ selectedPinAssignment?.victron?.rx }} - {{ currentPinAssignment?.victron?.rx }} - - - TX - {{ selectedPinAssignment?.victron?.tx }} - {{ currentPinAssignment?.victron?.tx }} - - - - Battery - RX - {{ selectedPinAssignment?.battery?.rx }} - {{ currentPinAssignment?.battery?.rx }} - - - TX - {{ selectedPinAssignment?.battery?.tx }} - {{ currentPinAssignment?.battery?.tx }} - - + @@ -150,5 +45,53 @@ export default defineComponent({ selectedPinAssignment: { type: Object as PropType, required: true }, currentPinAssignment: { type: Object as PropType, required: true }, }, + computed: { + categories(): string[] { + let curArray: Array = []; + if (this.currentPinAssignment) { + curArray = Object.keys(this.currentPinAssignment as Device); + } + + let selArray: Array = []; + if (this.selectedPinAssignment) { + selArray = Object.keys(this.selectedPinAssignment as Device); + } + + let total: Array = []; + total = total.concat(curArray, selArray); + return Array.from(new Set(total)).filter(cat => cat != 'name').sort(); + }, + }, + methods: { + properties(category: string): string[] { + let curArray: Array = []; + if ((this.currentPinAssignment as Device)[category as keyof Device]) { + curArray = Object.keys((this.currentPinAssignment as Device)[category as keyof Device]); + } + + let selArray: Array = []; + if ((this.selectedPinAssignment as Device)[category as keyof Device]) { + selArray = Object.keys((this.selectedPinAssignment as Device)[category as keyof Device]); + } + + let total: Array = []; + total = total.concat(curArray, selArray); + + return Array.from(new Set(total)).sort(); + }, + isEqual(category: string, prop: string): boolean { + if (!((this.selectedPinAssignment as Device)[category as keyof Device])) { + return false; + } + if (!((this.currentPinAssignment as Device)[category as keyof Device])) { + return false; + } + + return (this.selectedPinAssignment as any)[category][prop] == (this.currentPinAssignment as any)[category][prop]; + }, + capitalizeFirstLetter(value: string): string { + return value.charAt(0).toUpperCase() + value.slice(1); + }, + } }); diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 931f46c3..09efef40 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -459,6 +459,9 @@ "General": "Allgemein", "Enable": "Aktiviert", "EnableSolarPasstrough": "Aktiviere Solar Pass-trough", + "BatteryDrainStrategy": "Strategie zur Batterieentleerung", + "BatteryDrainWhenFull": "Leeren, wenn voll", + "BatteryDrainAtNight": "Leeren zur Nacht", "SolarpasstroughInfo": "Diese Einstellung aktiviert die direkte Weitergabe der aktuell vom Laderegler gemeldeten Solarleistung an den Wechselrichter um eine unnötige Speicherung zu vermeiden und die Energieverluste zu minimieren.", "InverterId": "Wechselrichter ID", "InverterIdHint": "Wähle den Wechselrichter an dem die Batterie hängt.", @@ -599,8 +602,12 @@ "PowerSafeHint": "Schaltet das Display aus, wenn kein Wechselrichter Strom erzeugt", "Screensaver": "Screensaver aktivieren:", "ScreensaverHint": "Bewegt die Ausgabe bei jeder Aktualisierung um ein Einbrennen zu verhindern (v. a. für OLED-Displays nützlich)", - "ShowLogo": "Logo anzeigen:", "Contrast": "Kontrast ({contrast}):", + "Rotation": "Rotation:", + "rot0": "Keine Rotation", + "rot90": "90 Grad Drehung", + "rot180": "180 Grad Drehung", + "rot270": "270 Grad Drehung", "Save": "@:dtuadmin.Save" }, "pininfo": { diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index bfb49013..e77c480e 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -459,6 +459,9 @@ "General": "General", "Enable": "Enable", "EnableSolarPasstrough": "Enable Solar-Passtrough", + "BatteryDrainStrategy": "Battery drain strategy", + "BatteryDrainWhenFull": "Empty when full", + "BatteryDrainAtNight": "Empty at night", "SolarpasstroughInfo": "When the sun is shining, this setting enables the sychronization of the inverter limit with the current solar power of the Victron MPPT charger. This optimizes battery degradation and loses.", "InverterId": "Inverter ID", "InverterIdHint": "Select proper inverter ID where battery is connected to.", @@ -599,8 +602,12 @@ "PowerSafeHint": "Turn off the display if no inverter is producing.", "Screensaver": "Enable Screensaver:", "ScreensaverHint": "Move the display a little bit on each update to prevent burn-in. (Useful especially for OLED displays)", - "ShowLogo": "Show Logo:", "Contrast": "Contrast ({contrast}):", + "Rotation": "Rotation:", + "rot0": "No rotation", + "rot90": "90 degree rotation", + "rot180": "180 degree rotation", + "rot270": "270 degree rotation", "Save": "@:dtuadmin.Save" }, "pininfo": { diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index c90613d0..89b10d4b 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -559,8 +559,12 @@ "PowerSafeHint": "Eteindre l'écran si aucun onduleur n'est en production.", "Screensaver": "Activer l'écran de veille", "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)", - "ShowLogo": "Afficher le logo", "Contrast": "Contraste ({contrast}):", + "Rotation": "Rotation:", + "rot0": "No rotation", + "rot90": "90 degree rotation", + "rot180": "180 degree rotation", + "rot270": "270 degree rotation", "Save": "@:dtuadmin.Save" }, "pininfo": { diff --git a/webapp/src/types/DeviceConfig.ts b/webapp/src/types/DeviceConfig.ts index 99ce70c2..8bd87f70 100644 --- a/webapp/src/types/DeviceConfig.ts +++ b/webapp/src/types/DeviceConfig.ts @@ -1,7 +1,7 @@ import type { Device } from "./PinMapping"; export interface Display { - show_logo: boolean; + rotation: number; power_safe: boolean; screensaver: boolean; contrast: number; diff --git a/webapp/src/types/PowerLimiterConfig.ts b/webapp/src/types/PowerLimiterConfig.ts index 6cbdd612..09521dfe 100644 --- a/webapp/src/types/PowerLimiterConfig.ts +++ b/webapp/src/types/PowerLimiterConfig.ts @@ -1,6 +1,7 @@ export interface PowerLimiterConfig { enabled: boolean; solar_passtrough_enabled: boolean; + battery_drain_strategy: number; mqtt_topic_powermeter_1: string; mqtt_topic_powermeter_2: string; mqtt_topic_powermeter_3: string; diff --git a/webapp/src/views/DeviceAdminView.vue b/webapp/src/views/DeviceAdminView.vue index 0eccf5b3..b8da5770 100644 --- a/webapp/src/views/DeviceAdminView.vue +++ b/webapp/src/views/DeviceAdminView.vue @@ -57,8 +57,18 @@ v-model="deviceConfigList.display.screensaver" type="checkbox" :tooltip="$t('deviceadmin.ScreensaverHint')" /> - +
+ +
+ +
+