diff --git a/docs/DeviceProfiles/lilygo_ttgo_t-internet_poe.json b/docs/DeviceProfiles/lilygo_ttgo_t-internet_poe.json index 0bc2857c9..538f3000b 100644 --- a/docs/DeviceProfiles/lilygo_ttgo_t-internet_poe.json +++ b/docs/DeviceProfiles/lilygo_ttgo_t-internet_poe.json @@ -18,5 +18,50 @@ "type": 0, "clk_mode": 3 } + }, + { + "name": "LILYGO TTGO T-Internet-POE, nrf24 direct solder", + "nrf24": { + "miso": 12, + "mosi": 4, + "clk": 15, + "irq": 33, + "en": 14, + "cs": 2 + }, + "eth": { + "enabled": true, + "phy_addr": 0, + "power": -1, + "mdc": 23, + "mdio": 18, + "type": 0, + "clk_mode": 3 + } + }, + { + "name": "LILYGO TTGO T-Internet-POE, nrf24 direct solder, SSD1306", + "nrf24": { + "miso": 12, + "mosi": 4, + "clk": 15, + "irq": 33, + "en": 14, + "cs": 2 + }, + "eth": { + "enabled": true, + "phy_addr": 0, + "power": -1, + "mdc": 23, + "mdio": 18, + "type": 0, + "clk_mode": 3 + }, + "display": { + "type": 2, + "data": 16, + "clk": 32 + } } ] \ No newline at end of file diff --git a/include/Display_Graphic.h b/include/Display_Graphic.h index ac0512df3..9fe202c4b 100644 --- a/include/Display_Graphic.h +++ b/include/Display_Graphic.h @@ -19,6 +19,7 @@ class DisplayGraphicClass { 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 setStatus(bool turnOn); void setOrientation(uint8_t rotation = DISPLAY_ROTATION); void setLanguage(uint8_t language); void setStartupDisplay(); @@ -33,6 +34,8 @@ class DisplayGraphicClass { U8G2* _display; + bool _displayTurnedOn; + DisplayType_t _display_type = DisplayType_t::None; uint8_t _display_language = DISPLAY_LANGUAGE; uint8_t _mExtra; diff --git a/include/Led_Single.h b/include/Led_Single.h index 22fc87a2b..a5c601bda 100644 --- a/include/Led_Single.h +++ b/include/Led_Single.h @@ -19,6 +19,9 @@ class LedSingleClass { void init(); void loop(); + void turnAllOff(); + void turnAllOn(); + private: enum class LedState_t { On, @@ -27,6 +30,7 @@ class LedSingleClass { }; LedState_t _ledState[PINMAPPING_LED_COUNT]; + LedState_t _allState; TimeoutHelper _updateTimeout; TimeoutHelper _blinkTimeout; uint8_t _ledActive = 0; diff --git a/include/MqttHandlePowerLimiter.h b/include/MqttHandlePowerLimiter.h index 82d736ea6..d52d202d6 100644 --- a/include/MqttHandlePowerLimiter.h +++ b/include/MqttHandlePowerLimiter.h @@ -10,7 +10,7 @@ class MqttHandlePowerLimiterClass { void loop(); private: - void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total); + void onCmdMode(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total); uint32_t _lastPublishStats; uint32_t _lastPublish; diff --git a/include/PowerLimiter.h b/include/PowerLimiter.h index 34e6920a1..6bf1497aa 100644 --- a/include/PowerLimiter.h +++ b/include/PowerLimiter.h @@ -51,8 +51,15 @@ class PowerLimiterClass { void loop(); uint8_t getPowerLimiterState(); int32_t getLastRequestedPowerLimit(); - void setMode(uint8_t mode); - bool getMode(); + + enum class Mode : unsigned { + Normal = 0, + Disabled = 1, + UnconditionalFullSolarPassthrough = 2 + }; + + void setMode(Mode m) { _mode = m; } + Mode getMode() const { return _mode; } void calcNextInverterRestart(); private: @@ -63,7 +70,7 @@ class PowerLimiterClass { uint32_t _lastCalculation = 0; static constexpr uint32_t _calculationBackoffMsDefault = 128; uint32_t _calculationBackoffMs = _calculationBackoffMsDefault; - uint8_t _mode = PL_MODE_ENABLE_NORMAL_OP; + Mode _mode = Mode::Normal; std::shared_ptr _inverter = nullptr; bool _batteryDischargeEnabled = false; uint32_t _nextInverterRestart = 0; // Values: 0->not calculated / 1->no restart configured / >1->time of next inverter restart in millis() diff --git a/include/Utils.h b/include/Utils.h index 33887ff96..6de962b02 100644 --- a/include/Utils.h +++ b/include/Utils.h @@ -8,4 +8,5 @@ class Utils { static uint32_t getChipId(); static uint64_t generateDtuSerial(); static int getTimezoneOffset(); + static void restartDtu(); }; diff --git a/include/WebApi_prometheus.h b/include/WebApi_prometheus.h index 4a77acdd4..b03f81786 100644 --- a/include/WebApi_prometheus.h +++ b/include/WebApi_prometheus.h @@ -13,33 +13,38 @@ class WebApiPrometheusClass { private: void onPrometheusMetricsGet(AsyncWebServerRequest* request); - 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 addField(AsyncResponseStream* stream, String& serial, uint8_t idx, std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, const char* metricName, 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 { - METRIC_TYPE_NONE = 0, - METRIC_TYPE_GAUGE, - METRIC_TYPE_COUNTER, + enum MetricType_t { + NONE = 0, + GAUGE, + COUNTER, }; const char* _metricTypes[3] = { 0, "gauge", "counter" }; - std::map _fieldMetricAssignment { - { FLD_UDC, METRIC_TYPE_GAUGE }, - { FLD_IDC, METRIC_TYPE_GAUGE }, - { FLD_PDC, METRIC_TYPE_GAUGE }, - { FLD_YD, METRIC_TYPE_COUNTER }, - { FLD_YT, METRIC_TYPE_COUNTER }, - { FLD_UAC, METRIC_TYPE_GAUGE }, - { FLD_IAC, METRIC_TYPE_GAUGE }, - { FLD_PAC, METRIC_TYPE_GAUGE }, - { FLD_F, METRIC_TYPE_GAUGE }, - { FLD_T, METRIC_TYPE_GAUGE }, - { FLD_PF, METRIC_TYPE_GAUGE }, - { FLD_EFF, METRIC_TYPE_GAUGE }, - { FLD_IRR, METRIC_TYPE_GAUGE }, - { FLD_Q, METRIC_TYPE_GAUGE } + struct publish_type_t { + FieldId_t field; + MetricType_t type; + }; + + const publish_type_t _publishFields[14] = { + { FLD_PAC, MetricType_t::GAUGE }, + { FLD_UAC, MetricType_t::GAUGE }, + { FLD_IAC, MetricType_t::GAUGE }, + { FLD_PDC, MetricType_t::GAUGE }, + { FLD_UDC, MetricType_t::GAUGE }, + { FLD_IDC, MetricType_t::GAUGE }, + { FLD_YD, MetricType_t::COUNTER }, + { FLD_YT, MetricType_t::COUNTER }, + { FLD_F, MetricType_t::GAUGE }, + { FLD_T, MetricType_t::GAUGE }, + { FLD_PF, MetricType_t::GAUGE }, + { FLD_Q, MetricType_t::GAUGE }, + { FLD_EFF, MetricType_t::GAUGE }, + { FLD_IRR, MetricType_t::GAUGE }, }; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/Hoymiles.cpp b/lib/Hoymiles/src/Hoymiles.cpp index e5e6e4ddd..a5f468b56 100644 --- a/lib/Hoymiles/src/Hoymiles.cpp +++ b/lib/Hoymiles/src/Hoymiles.cpp @@ -91,9 +91,21 @@ void HoymilesClass::loop() } // Fetch dev info (but first fetch stats) - if (iv->Statistics()->getLastUpdate() > 0 && (iv->DevInfo()->getLastUpdateAll() == 0 || iv->DevInfo()->getLastUpdateSimple() == 0)) { - _messageOutput->println("Request device info"); - iv->sendDevInfoRequest(); + if (iv->Statistics()->getLastUpdate() > 0) { + bool invalidDevInfo = !iv->DevInfo()->containsValidData() + && iv->DevInfo()->getLastUpdateAll() > 0 + && iv->DevInfo()->getLastUpdateSimple() > 0; + + if (invalidDevInfo) { + _messageOutput->println("DevInfo: No Valid Data"); + } + + if ((iv->DevInfo()->getLastUpdateAll() == 0) + || (iv->DevInfo()->getLastUpdateSimple() == 0) + || invalidDevInfo) { + _messageOutput->println("Request device info"); + iv->sendDevInfoRequest(); + } } if (++inverterPos >= getNumInverters()) { diff --git a/lib/Hoymiles/src/inverters/HMS_4CH.cpp b/lib/Hoymiles/src/inverters/HMS_4CH.cpp index b0ca62072..e2947d8f3 100644 --- a/lib/Hoymiles/src/inverters/HMS_4CH.cpp +++ b/lib/Hoymiles/src/inverters/HMS_4CH.cpp @@ -36,7 +36,7 @@ static const byteAssign_t byteAssignment[] = { { TYPE_AC, CH0, FLD_UAC, UNIT_V, 50, 2, 10, false, 1 }, { TYPE_AC, CH0, FLD_IAC, UNIT_A, 58, 2, 100, false, 2 }, { TYPE_AC, CH0, FLD_PAC, UNIT_W, 54, 2, 10, false, 1 }, - { TYPE_AC, CH0, FLD_Q, UNIT_VAR, 56, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_Q, UNIT_VAR, 56, 2, 10, true, 1 }, { TYPE_AC, CH0, FLD_F, UNIT_HZ, 52, 2, 100, false, 2 }, { TYPE_AC, CH0, FLD_PF, UNIT_NONE, 60, 2, 1000, false, 3 }, diff --git a/lib/Hoymiles/src/parser/DevInfoParser.cpp b/lib/Hoymiles/src/parser/DevInfoParser.cpp index 44fe59e19..eab3a303e 100644 --- a/lib/Hoymiles/src/parser/DevInfoParser.cpp +++ b/lib/Hoymiles/src/parser/DevInfoParser.cpp @@ -196,6 +196,16 @@ String DevInfoParser::getHwModelName() return devInfo[idx].modelName; } +bool DevInfoParser::containsValidData() +{ + time_t t = getFwBuildDateTime(); + + struct tm info; + localtime_r(&t, &info); + + return info.tm_year > (2016 - 1900); +} + uint8_t DevInfoParser::getDevIdx() { uint8_t ret = 0xff; diff --git a/lib/Hoymiles/src/parser/DevInfoParser.h b/lib/Hoymiles/src/parser/DevInfoParser.h index 18c3d7c87..b18bc8a11 100644 --- a/lib/Hoymiles/src/parser/DevInfoParser.h +++ b/lib/Hoymiles/src/parser/DevInfoParser.h @@ -33,6 +33,8 @@ class DevInfoParser : public Parser { uint16_t getMaxPower(); String getHwModelName(); + bool containsValidData(); + private: time_t timegm(struct tm* tm); uint8_t getDevIdx(); diff --git a/lib/Hoymiles/src/parser/StatisticsParser.cpp b/lib/Hoymiles/src/parser/StatisticsParser.cpp index 2218ca4b1..ac61be81b 100644 --- a/lib/Hoymiles/src/parser/StatisticsParser.cpp +++ b/lib/Hoymiles/src/parser/StatisticsParser.cpp @@ -150,6 +150,13 @@ float StatisticsParser::getChannelFieldValue(ChannelType_t type, ChannelNum_t ch return 0; } +String StatisticsParser::getChannelFieldValueString(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) +{ + return String( + getChannelFieldValue(type, channel, fieldId), + static_cast(getChannelFieldDigits(type, channel, fieldId))); +} + bool StatisticsParser::hasChannelFieldValue(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) { const byteAssign_t* pos = getAssignmentByChannelField(type, channel, fieldId); diff --git a/lib/Hoymiles/src/parser/StatisticsParser.h b/lib/Hoymiles/src/parser/StatisticsParser.h index 436dba27d..13d7d4f47 100644 --- a/lib/Hoymiles/src/parser/StatisticsParser.h +++ b/lib/Hoymiles/src/parser/StatisticsParser.h @@ -119,6 +119,7 @@ class StatisticsParser : public Parser { fieldSettings_t* getSettingByChannelField(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); float getChannelFieldValue(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); + String getChannelFieldValueString(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); bool hasChannelFieldValue(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); const char* getChannelFieldUnit(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); const char* getChannelFieldName(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); diff --git a/pio-scripts/auto_firmware_version.py b/pio-scripts/auto_firmware_version.py index 47ded11c2..6a2bec2dc 100644 --- a/pio-scripts/auto_firmware_version.py +++ b/pio-scripts/auto_firmware_version.py @@ -15,6 +15,7 @@ from dulwich import porcelain + def get_firmware_specifier_build_flag(): try: build_version = porcelain.describe('.') # '.' refers to the repository root dir diff --git a/src/Display_Graphic.cpp b/src/Display_Graphic.cpp index 972b3bc2e..26991cb5d 100644 --- a/src/Display_Graphic.cpp +++ b/src/Display_Graphic.cpp @@ -47,6 +47,7 @@ void DisplayGraphicClass::init(DisplayType_t type, uint8_t data, uint8_t clk, ui _display = constructor(reset, clk, data, cs); _display->begin(); setContrast(DISPLAY_CONTRAST); + setStatus(true); } } @@ -139,10 +140,11 @@ void DisplayGraphicClass::loop() if ((millis() - _lastDisplayUpdate) > _period) { _display->clearBuffer(); + bool displayPowerSave = false; //=====> Actual Production ========== if (Datastore.getIsAtLeastOneReachable()) { - _display->setPowerSave(false); + displayPowerSave = false; if (Datastore.getTotalAcPowerEnabled() > 999) { snprintf(_fmtText, sizeof(_fmtText), i18n_current_power_kw[_display_language], (Datastore.getTotalAcPowerEnabled() / 1000)); } else { @@ -158,7 +160,7 @@ void DisplayGraphicClass::loop() printText(i18n_offline[_display_language], 0); // check if it's time to enter power saving mode if (millis() - _previousMillis >= (_interval * 2)) { - _display->setPowerSave(enablePowerSafe); + displayPowerSave = enablePowerSafe; } } //<======================= @@ -184,6 +186,12 @@ void DisplayGraphicClass::loop() _mExtra++; _lastDisplayUpdate = millis(); + + if (!_displayTurnedOn) { + displayPowerSave = true; + } + + _display->setPowerSave(displayPowerSave); } } @@ -195,4 +203,9 @@ void DisplayGraphicClass::setContrast(uint8_t contrast) _display->setContrast(contrast * 2.55f); } +void DisplayGraphicClass::setStatus(bool turnOn) +{ + _displayTurnedOn = turnOn; +} + DisplayGraphicClass Display; \ No newline at end of file diff --git a/src/Led_Single.cpp b/src/Led_Single.cpp index 17690fe0e..7ce6637f9 100644 --- a/src/Led_Single.cpp +++ b/src/Led_Single.cpp @@ -20,6 +20,7 @@ void LedSingleClass::init() { _blinkTimeout.set(500); _updateTimeout.set(LEDSINGLE_UPDATE_INTERVAL); + turnAllOn(); for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) { auto& pin = PinMapping.get(); @@ -40,7 +41,7 @@ void LedSingleClass::loop() return; } - if (_updateTimeout.occured()) { + if (_updateTimeout.occured() && _allState == LedState_t::On) { const CONFIG_T& config = Configuration.get(); // Update network status @@ -68,6 +69,9 @@ void LedSingleClass::loop() } _updateTimeout.reset(); + } else if (_updateTimeout.occured() && _allState == LedState_t::Off) { + _ledState[0] = LedState_t::Off; + _ledState[1] = LedState_t::Off; } for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) { @@ -93,3 +97,13 @@ void LedSingleClass::loop() } } } + +void LedSingleClass::turnAllOff() +{ + _allState = LedState_t::Off; +} + +void LedSingleClass::turnAllOn() +{ + _allState = LedState_t::On; +} diff --git a/src/MqttHandlVedirectHass.cpp b/src/MqttHandlVedirectHass.cpp index d31d2777d..f6fbf19cc 100644 --- a/src/MqttHandlVedirectHass.cpp +++ b/src/MqttHandlVedirectHass.cpp @@ -62,7 +62,7 @@ void MqttHandleVedirectHassClass::publishConfig() publishSensor("MPPT error code", "ERR"); publishSensor("MPPT off reason", "OR"); publishSensor("MPPT tracker operation mode", "MPPT"); - publishSensor("MPPT Day sequence number (0...364)", "HSDS", "duration", "total_increasing", "d"); + publishSensor("MPPT Day sequence number (0...364)", "HSDS", NULL, "total", "d"); // battery info publishSensor("Battery voltage", "V", "voltage", "measurement", "V"); @@ -71,11 +71,10 @@ void MqttHandleVedirectHassClass::publishConfig() // panel info publishSensor("Panel voltage", "VPV", "voltage", "measurement", "V"); publishSensor("Panel power", "PPV", "power", "measurement", "W"); - publishSensor("Panel power", "PPV", "power", "measurement", "W"); publishSensor("Panel yield total", "H19", "energy", "total_increasing", "kWh"); - publishSensor("Panel yield today", "H20", "energy", "total_increasing", "kWh"); + publishSensor("Panel yield today", "H20", "energy", "total", "kWh"); publishSensor("Panel maximum power today", "H21", "power", "measurement", "W"); - publishSensor("Panel yield yesterday", "H22", "energy", "measurement", "kWh"); + publishSensor("Panel yield yesterday", "H22", "energy", "total", "kWh"); publishSensor("Panel maximum power yesterday", "H23", "power", "measurement", "W"); yield(); @@ -87,6 +86,9 @@ void MqttHandleVedirectHassClass::publishSensor(const char* caption, const char* String sensorId = caption; sensorId.replace(" ", "_"); + sensorId.replace(".", ""); + sensorId.replace("(", ""); + sensorId.replace(")", ""); sensorId.toLowerCase(); String configTopic = "sensor/dtu_victron_" + serial @@ -131,6 +133,9 @@ void MqttHandleVedirectHassClass::publishBinarySensor(const char* caption, const String sensorId = caption; sensorId.replace(" ", "_"); + sensorId.replace(".", ""); + sensorId.replace("(", ""); + sensorId.replace(")", ""); sensorId.toLowerCase(); String configTopic = "binary_sensor/dtu_victron_" + serial diff --git a/src/MqttHandleInverter.cpp b/src/MqttHandleInverter.cpp index a42803cdd..c9621e33c 100644 --- a/src/MqttHandleInverter.cpp +++ b/src/MqttHandleInverter.cpp @@ -126,11 +126,7 @@ void MqttHandleInverterClass::publishField(std::shared_ptr inv return; } - String value = String( - inv->Statistics()->getChannelFieldValue(type, channel, fieldId), - static_cast(inv->Statistics()->getChannelFieldDigits(type, channel, fieldId))); - - MqttSettings.publish(topic, value); + MqttSettings.publish(topic, inv->Statistics()->getChannelFieldValueString(type, channel, fieldId)); } String MqttHandleInverterClass::getTopic(std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) diff --git a/src/MqttHandlePowerLimiter.cpp b/src/MqttHandlePowerLimiter.cpp index 56ec1a16c..d5d6987ef 100644 --- a/src/MqttHandlePowerLimiter.cpp +++ b/src/MqttHandlePowerLimiter.cpp @@ -7,8 +7,7 @@ #include "MqttHandlePowerLimiter.h" #include "PowerLimiter.h" #include - -#define TOPIC_SUB_POWER_LIMITER "mode" +#include MqttHandlePowerLimiterClass MqttHandlePowerLimiter; @@ -21,11 +20,10 @@ void MqttHandlePowerLimiterClass::init() using std::placeholders::_5; using std::placeholders::_6; - String topic = MqttSettings.getPrefix(); - MqttSettings.subscribe(String(topic + "powerlimiter/cmd/" + TOPIC_SUB_POWER_LIMITER).c_str(), 0, std::bind(&MqttHandlePowerLimiterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); + String topic = MqttSettings.getPrefix() + "powerlimiter/cmd/mode"; + MqttSettings.subscribe(topic.c_str(), 0, std::bind(&MqttHandlePowerLimiterClass::onCmdMode, this, _1, _2, _3, _4, _5, _6)); _lastPublish = millis(); - } @@ -38,60 +36,52 @@ void MqttHandlePowerLimiterClass::loop() const CONFIG_T& config = Configuration.get(); if ((millis() - _lastPublish) > (config.Mqtt_PublishInterval * 1000) ) { - MqttSettings.publish("powerlimiter/status/mode", String(PowerLimiter.getMode())); + auto val = static_cast(PowerLimiter.getMode()); + MqttSettings.publish("powerlimiter/status/mode", String(val)); - yield(); - _lastPublish = millis(); + yield(); + _lastPublish = millis(); } } -void MqttHandlePowerLimiterClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) +void MqttHandlePowerLimiterClass::onCmdMode(const espMqttClientTypes::MessageProperties& properties, + const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) { const CONFIG_T& config = Configuration.get(); - + // ignore messages if PowerLimiter is disabled if (!config.PowerLimiter_Enabled) { return; } - char token_topic[MQTT_MAX_TOPIC_STRLEN + 40]; // respect all subtopics - strncpy(token_topic, topic, MQTT_MAX_TOPIC_STRLEN + 40); // convert const char* to char* - - char* setting; - char* rest = &token_topic[strlen(config.Mqtt_Topic)]; - - strtok_r(rest, "/", &rest); // Remove "powerlimiter" - strtok_r(rest, "/", &rest); // Remove "cmd" - - setting = strtok_r(rest, "/", &rest); - - if (setting == NULL) { + std::string strValue(reinterpret_cast(payload), len); + int intValue = -1; + try { + intValue = std::stoi(strValue); + } + catch (std::invalid_argument const& e) { + MessageOutput.printf("PowerLimiter MQTT handler: cannot parse payload of topic '%s' as int: %s\r\n", + topic, strValue.c_str()); return; } - char* str = new char[len + 1]; - memcpy(str, payload, len); - str[len] = '\0'; - uint8_t payload_val = atoi(str); - delete[] str; - - if (!strcmp(setting, TOPIC_SUB_POWER_LIMITER)) { - if(payload_val == 2) { - MessageOutput.println("Power limiter full solar PT"); - PowerLimiter.setMode(PL_MODE_SOLAR_PT_ONLY); - return; - } - if(payload_val == 1) { - MessageOutput.println("Power limiter disabled"); - PowerLimiter.setMode(PL_MODE_FULL_DISABLE); - return; - } - if(payload_val == 0) { - MessageOutput.println("Power limiter enabled"); - PowerLimiter.setMode(PL_MODE_ENABLE_NORMAL_OP); - return; - } - MessageOutput.println("Power limiter enable / disable - unknown command received. Please use 0 or 1"); - } + using Mode = PowerLimiterClass::Mode; + switch (static_cast(intValue)) { + case Mode::UnconditionalFullSolarPassthrough: + MessageOutput.println("Power limiter unconditional full solar PT"); + PowerLimiter.setMode(Mode::UnconditionalFullSolarPassthrough); + break; + case Mode::Disabled: + MessageOutput.println("Power limiter disabled (override)"); + PowerLimiter.setMode(Mode::Disabled); + break; + case Mode::Normal: + MessageOutput.println("Power limiter normal operation"); + PowerLimiter.setMode(Mode::Normal); + break; + default: + MessageOutput.printf("PowerLimiter - unknown mode %d\r\n", intValue); + break; + } } \ No newline at end of file diff --git a/src/MqttHandlePylontechHass.cpp b/src/MqttHandlePylontechHass.cpp index 5458119a4..c11c5953c 100644 --- a/src/MqttHandlePylontechHass.cpp +++ b/src/MqttHandlePylontechHass.cpp @@ -95,6 +95,7 @@ void MqttHandlePylontechHassClass::publishSensor(const char* caption, const char { String sensorId = caption; sensorId.replace(" ", "_"); + sensorId.replace(".", ""); sensorId.replace("(", ""); sensorId.replace(")", ""); sensorId.toLowerCase(); @@ -140,6 +141,9 @@ void MqttHandlePylontechHassClass::publishBinarySensor(const char* caption, cons { String sensorId = caption; sensorId.replace(" ", "_"); + sensorId.replace(".", ""); + sensorId.replace("(", ""); + sensorId.replace(")", ""); sensorId.toLowerCase(); String configTopic = "binary_sensor/dtu_battery_" + serial diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index 3e031a98b..354f25853 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -131,7 +131,7 @@ void PowerLimiterClass::loop() return; } - if (PL_MODE_FULL_DISABLE == _mode) { + if (Mode::Disabled == _mode) { shutdown(Status::DisabledByMqtt); return; } @@ -185,7 +185,7 @@ void PowerLimiterClass::loop() return announceStatus(Status::InverterDevInfoPending); } - if (PL_MODE_SOLAR_PT_ONLY == _mode) { + if (Mode::UnconditionalFullSolarPassthrough == _mode) { // handle this mode of operation separately return unconditionalSolarPassthrough(_inverter); } @@ -398,14 +398,6 @@ int32_t PowerLimiterClass::getLastRequestedPowerLimit() { return _lastRequestedPowerLimit; } -bool PowerLimiterClass::getMode() { - return _mode; -} - -void PowerLimiterClass::setMode(uint8_t mode) { - _mode = mode; -} - bool PowerLimiterClass::canUseDirectSolarPower() { CONFIG_T& config = Configuration.get(); diff --git a/src/Utils.cpp b/src/Utils.cpp index db8363ad3..2e59d856e 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -3,6 +3,8 @@ * Copyright (C) 2022 - 2023 Thomas Basler and others */ #include "Utils.h" +#include "Display_Graphic.h" +#include "Led_Single.h" #include uint32_t Utils::getChipId() @@ -52,4 +54,14 @@ int Utils::getTimezoneOffset() gmt = mktime(ptm); return static_cast(difftime(rawtime, gmt)); -} \ No newline at end of file +} + +void Utils::restartDtu() +{ + LedSingle.turnAllOff(); + Display.setStatus(false); + yield(); + delay(1000); + yield(); + ESP.restart(); +} diff --git a/src/WebApi_config.cpp b/src/WebApi_config.cpp index d936be581..08b86d654 100644 --- a/src/WebApi_config.cpp +++ b/src/WebApi_config.cpp @@ -4,6 +4,7 @@ */ #include "WebApi_config.h" #include "Configuration.h" +#include "Utils.h" #include "WebApi.h" #include "WebApi_errors.h" #include @@ -114,7 +115,7 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request) request->send(response); LittleFS.remove(CONFIG_FILENAME); - ESP.restart(); + Utils::restartDtu(); } void WebApiConfigClass::onConfigListGet(AsyncWebServerRequest* request) @@ -157,10 +158,7 @@ void WebApiConfigClass::onConfigUploadFinish(AsyncWebServerRequest* request) response->addHeader("Connection", "close"); response->addHeader("Access-Control-Allow-Origin", "*"); request->send(response); - yield(); - delay(1000); - yield(); - ESP.restart(); + Utils::restartDtu(); } void WebApiConfigClass::onConfigUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final) diff --git a/src/WebApi_device.cpp b/src/WebApi_device.cpp index ab7d7bfbc..521e799ea 100644 --- a/src/WebApi_device.cpp +++ b/src/WebApi_device.cpp @@ -6,6 +6,7 @@ #include "Configuration.h" #include "Display_Graphic.h" #include "PinMapping.h" +#include "Utils.h" #include "WebApi.h" #include "WebApi_errors.h" #include "helper.h" @@ -185,9 +186,6 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request) request->send(response); if (performRestart) { - yield(); - delay(1000); - yield(); - ESP.restart(); + Utils::restartDtu(); } } \ No newline at end of file diff --git a/src/WebApi_firmware.cpp b/src/WebApi_firmware.cpp index 798264a06..62cf56155 100644 --- a/src/WebApi_firmware.cpp +++ b/src/WebApi_firmware.cpp @@ -5,6 +5,7 @@ #include "WebApi_firmware.h" #include "Configuration.h" #include "Update.h" +#include "Utils.h" #include "WebApi.h" #include "helper.h" #include @@ -42,10 +43,7 @@ void WebApiFirmwareClass::onFirmwareUpdateFinish(AsyncWebServerRequest* request) response->addHeader("Connection", "close"); response->addHeader("Access-Control-Allow-Origin", "*"); request->send(response); - yield(); - delay(1000); - yield(); - ESP.restart(); + Utils::restartDtu(); } void WebApiFirmwareClass::onFirmwareUpdateUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final) diff --git a/src/WebApi_inverter.cpp b/src/WebApi_inverter.cpp index a81659253..c6e692b01 100644 --- a/src/WebApi_inverter.cpp +++ b/src/WebApi_inverter.cpp @@ -34,7 +34,7 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request) return; } - AsyncJsonResponse* response = new AsyncJsonResponse(false, 4096U); + AsyncJsonResponse* response = new AsyncJsonResponse(false, 768 * INV_MAX_COUNT); JsonObject root = response->getRoot(); JsonArray data = root.createNestedArray("inverter"); diff --git a/src/WebApi_maintenance.cpp b/src/WebApi_maintenance.cpp index b59ed34ee..ed2d68673 100644 --- a/src/WebApi_maintenance.cpp +++ b/src/WebApi_maintenance.cpp @@ -4,6 +4,7 @@ */ #include "WebApi_maintenance.h" +#include "Utils.h" #include "WebApi.h" #include "WebApi_errors.h" #include @@ -75,10 +76,7 @@ void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request) response->setLength(); request->send(response); - yield(); - delay(1000); - yield(); - ESP.restart(); + Utils::restartDtu(); } else { retMsg["message"] = "Reboot cancled!"; retMsg["code"] = WebApiError::MaintenanceRebootCancled; diff --git a/src/WebApi_powerlimiter.cpp b/src/WebApi_powerlimiter.cpp index cb7c7c4b2..7aa6f4cc6 100644 --- a/src/WebApi_powerlimiter.cpp +++ b/src/WebApi_powerlimiter.cpp @@ -125,7 +125,7 @@ void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request) CONFIG_T& config = Configuration.get(); config.PowerLimiter_Enabled = root[F("enabled")].as(); - PowerLimiter.setMode(PL_MODE_ENABLE_NORMAL_OP); // User input sets PL to normal operation + PowerLimiter.setMode(PowerLimiterClass::Mode::Normal); // User input sets PL to normal operation config.PowerLimiter_VerboseLogging = root[F("verbose_logging")].as(); config.PowerLimiter_SolarPassThroughEnabled = root[F("solar_passthrough_enabled")].as(); config.PowerLimiter_SolarPassThroughLosses = root[F("solar_passthrough_losses")].as(); diff --git a/src/WebApi_prometheus.cpp b/src/WebApi_prometheus.cpp index dbb930534..88b673a9d 100644 --- a/src/WebApi_prometheus.cpp +++ b/src/WebApi_prometheus.cpp @@ -75,24 +75,13 @@ void WebApiPrometheusClass::onPrometheusMetricsGet(AsyncWebServerRequest* reques 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); - if (t == TYPE_AC) { - addField(stream, serial, i, inv, t, c, FLD_PDC, "PowerDC"); - } else { - addField(stream, serial, i, inv, t, c, FLD_PDC); + for (uint8_t f = 0; f < sizeof(_publishFields) / sizeof(_publishFields[0]); f++) { + if (t == TYPE_AC && _publishFields[f].field == FLD_PDC) { + addField(stream, serial, i, inv, t, c, _publishFields[f].field, _metricTypes[_publishFields[f].type], "PowerDC"); + } else { + addField(stream, serial, i, inv, t, c, _publishFields[f].field, _metricTypes[_publishFields[f].type]); + } } - addField(stream, serial, i, inv, t, c, FLD_UDC); - addField(stream, serial, i, inv, t, c, FLD_IDC); - addField(stream, serial, i, inv, t, c, FLD_YD); - addField(stream, serial, i, inv, t, c, FLD_YT); - addField(stream, serial, i, inv, t, c, FLD_F); - addField(stream, serial, i, inv, t, c, FLD_T); - addField(stream, serial, i, inv, t, c, FLD_PF); - addField(stream, serial, i, inv, t, c, FLD_Q); - addField(stream, serial, i, inv, t, c, FLD_EFF); - addField(stream, serial, i, inv, t, c, FLD_IRR); } } } @@ -107,22 +96,22 @@ void WebApiPrometheusClass::onPrometheusMetricsGet(AsyncWebServerRequest* reques } } -void WebApiPrometheusClass::addField(AsyncResponseStream* stream, String& serial, uint8_t idx, std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, const char* channelName) +void WebApiPrometheusClass::addField(AsyncResponseStream* stream, String& serial, uint8_t idx, std::shared_ptr inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, const char* metricName, const char* channelName) { if (inv->Statistics()->hasChannelFieldValue(type, channel, fieldId)) { const char* chanName = (channelName == NULL) ? inv->Statistics()->getChannelFieldName(type, channel, fieldId) : channelName; if (idx == 0 && type == TYPE_AC && channel == 0) { stream->printf("# HELP opendtu_%s in %s\n", chanName, inv->Statistics()->getChannelFieldUnit(type, channel, fieldId)); - stream->printf("# TYPE opendtu_%s %s\n", chanName, _metricTypes[_fieldMetricAssignment[fieldId]]); + stream->printf("# TYPE opendtu_%s %s\n", chanName, metricName); } - stream->printf("opendtu_%s{serial=\"%s\",unit=\"%d\",name=\"%s\",type=\"%s\",channel=\"%d\"} %f\n", + stream->printf("opendtu_%s{serial=\"%s\",unit=\"%d\",name=\"%s\",type=\"%s\",channel=\"%d\"} %s\n", chanName, serial.c_str(), idx, inv->name(), inv->Statistics()->getChannelTypeName(type), channel, - inv->Statistics()->getChannelFieldValue(type, channel, fieldId)); + inv->Statistics()->getChannelFieldValueString(type, channel, fieldId).c_str()); } } diff --git a/webapp/.gitignore b/webapp/.gitignore index 38adffa64..9077710c2 100644 --- a/webapp/.gitignore +++ b/webapp/.gitignore @@ -13,6 +13,7 @@ dist dist-ssr coverage *.local +vite.user.ts /cypress/videos/ /cypress/screenshots/ diff --git a/webapp/package.json b/webapp/package.json index 6b0ca9b79..31f237aa7 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -22,22 +22,22 @@ "vue-router": "^4.2.4" }, "devDependencies": { - "@intlify/unplugin-vue-i18n": "^0.12.2", + "@intlify/unplugin-vue-i18n": "^0.12.3", "@rushstack/eslint-patch": "^1.3.3", - "@tsconfig/node18": "^18.2.0", + "@tsconfig/node18": "^18.2.1", "@types/bootstrap": "^5.2.6", - "@types/node": "^20.4.8", - "@types/sortablejs": "^1.15.1", + "@types/node": "^20.5.7", + "@types/sortablejs": "^1.15.2", "@types/spark-md5": "^3.0.2", - "@vitejs/plugin-vue": "^4.2.3", + "@vitejs/plugin-vue": "^4.3.3", "@vue/eslint-config-typescript": "^11.0.3", "@vue/tsconfig": "^0.4.0", - "eslint": "^8.46.0", - "eslint-plugin-vue": "^9.16.1", + "eslint": "^8.48.0", + "eslint-plugin-vue": "^9.17.0", "npm-run-all": "^4.1.5", "sass": "^1.64.2", "terser": "^5.19.2", - "typescript": "^5.1.6", + "typescript": "^5.2.2", "vite": "^4.4.9", "vite-plugin-compression": "^0.5.1", "vite-plugin-css-injected-by-js": "^3.3.0", diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 472fa23e3..49bfa89bd 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -690,9 +690,9 @@ "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.", "Display": "Display", - "PowerSafe": "Power Safe aktivieren:", + "PowerSafe": "Stromsparen aktivieren:", "PowerSafeHint": "Schaltet das Display aus, wenn kein Wechselrichter Strom erzeugt", - "Screensaver": "Screensaver aktivieren:", + "Screensaver": "Bildschirmschoner aktivieren:", "ScreensaverHint": "Bewegt die Ausgabe bei jeder Aktualisierung um ein Einbrennen zu verhindern (v. a. für OLED-Displays nützlich)", "Contrast": "Kontrast ({contrast}):", "Rotation": "Rotation:", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 7d70a01aa..c6e289c6b 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -699,7 +699,7 @@ "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.", "Display": "Display", - "PowerSafe": "Enable Power Safe:", + "PowerSafe": "Enable Power Save:", "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)", diff --git a/webapp/vite.config.ts b/webapp/vite.config.ts index f9d6286dc..71bb17ffd 100644 --- a/webapp/vite.config.ts +++ b/webapp/vite.config.ts @@ -9,6 +9,14 @@ import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite' const path = require('path') +// example 'vite.user.ts': export const proxy_target = '192.168.16.107' +let proxy_target; +try { + proxy_target = require('./vite.user.ts').proxy_target; +} catch (error) { + proxy_target = '192.168.20.110'; +} + // https://vitejs.dev/config/ export default defineConfig({ plugins: [ @@ -52,20 +60,25 @@ export default defineConfig({ server: { proxy: { '^/api': { - target: 'http://192.168.178.87/' + target: 'http://' + proxy_target }, '^/livedata': { - target: 'ws://192.168.178.87/', + target: 'ws://' + proxy_target, ws: true, changeOrigin: true }, '^/vedirectlivedata': { - target: 'ws://192.168.178.87/', + target: 'ws://' + proxy_target, + ws: true, + changeOrigin: true + }, + '^/batterylivedata': { + target: 'ws://' + proxy_target, ws: true, changeOrigin: true }, '^/console': { - target: 'ws://192.168.178.87/', + target: 'ws://' + proxy_target, ws: true, changeOrigin: true } diff --git a/webapp/yarn.lock b/webapp/yarn.lock index 97ea93fc8..ae7a4028c 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -151,10 +151,10 @@ resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.6.2.tgz#1816b5f6948029c5eaacb0703b850ee0cb37d8f8" integrity sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw== -"@eslint/eslintrc@^2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.1.tgz#18d635e24ad35f7276e8a49d135c7d3ca6a46f93" - integrity sha512-9t7ZA7NGGK8ckelF0PQCfcxIUzs1Md5rrO6U/c+FIQNanea5UZC0wqKXH4vHBccmu4ZJgZ2idtPeW7+Q2npOEA== +"@eslint/eslintrc@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.2.tgz#c6936b4b328c64496692f76944e755738be62396" + integrity sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g== dependencies: ajv "^6.12.4" debug "^4.3.2" @@ -166,10 +166,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@^8.46.0": - version "8.46.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.46.0.tgz#3f7802972e8b6fe3f88ed1aabc74ec596c456db6" - integrity sha512-a8TLtmPi8xzPkCbp/OGFUo5yhRkHM2Ko9kOWP4znJr0WAhWyThaw3PnwX4vOTWOAMsV2uRt32PPDcEz63esSaA== +"@eslint/js@8.48.0": + version "8.48.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.48.0.tgz#642633964e217905436033a2bd08bf322849b7fb" + integrity sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw== "@humanwhocodes/config-array@^0.11.10": version "0.11.10" @@ -249,10 +249,10 @@ resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-9.3.0-beta.24.tgz#23e08af9fc904fe3ef896786f9e659da6bb567b5" integrity sha512-AKxJ8s7eKIQWkNaf4wyyoLRwf4puCuQgjSChlDJm5JBEt6T8HGgnYTJLRXu6LD/JACn3Qwu6hM/XRX1c9yvjmQ== -"@intlify/unplugin-vue-i18n@^0.12.2": - version "0.12.2" - resolved "https://registry.yarnpkg.com/@intlify/unplugin-vue-i18n/-/unplugin-vue-i18n-0.12.2.tgz#64f7aad79cff1c4e8ff199cc059ea2bb9c36b2bb" - integrity sha512-IIgzLRSPUKZM1FBdUAZ9NwVPiLUr4ea5g/HLWe2lB7gNtPDz4FOfUNUllIT504hT+3pDoJmjaYJ6pyqT7F4Wuw== +"@intlify/unplugin-vue-i18n@^0.12.3": + version "0.12.3" + resolved "https://registry.yarnpkg.com/@intlify/unplugin-vue-i18n/-/unplugin-vue-i18n-0.12.3.tgz#fae7d92d3e7bfe9e710fb332b28d22e0f8d999f2" + integrity sha512-0riPtSfTM58JmGNMmJho/aHD2z3K24BESYAmkLvKlo61/LbaPvnjYU1DbSbJEm6bSjE2oEjUj+di3QaYxXei/w== dependencies: "@intlify/bundle-utils" "^7.0.2" "@intlify/shared" "9.3.0-beta.24" @@ -360,10 +360,10 @@ resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.3.3.tgz#16ab6c727d8c2020a5b6e4a176a243ecd88d8d69" integrity sha512-0xd7qez0AQ+MbHatZTlI1gu5vkG8r7MYRUJAHPAHJBmGLs16zpkrpAVLvjQKQOqaXPDUBwOiJzNc00znHSCVBw== -"@tsconfig/node18@^18.2.0": - version "18.2.0" - resolved "https://registry.yarnpkg.com/@tsconfig/node18/-/node18-18.2.0.tgz#d6b5358b3fa85fe89b13b46cb1e996e4d79d6a07" - integrity sha512-yhxwIlFVSVcMym3O31HoMnRXpoenmpIxcj4Yoes2DUpe+xCJnA7ECQP1Vw889V0jTt/2nzvpLQ/UuMYCd3JPIg== +"@tsconfig/node18@^18.2.1": + version "18.2.1" + resolved "https://registry.yarnpkg.com/@tsconfig/node18/-/node18-18.2.1.tgz#ebf5e6b8d94e9de072e712bc197d6441a325ed61" + integrity sha512-RDDZFuofwkcKpl8Vpj5wFbY+H53xOtqK7ckEL1sXsbPwvKwDdjQf3LkHbtt9sxIHn9nWIEwkmCwBRZ6z5TKU2A== "@types/bootstrap@^5.2.6": version "5.2.6" @@ -382,20 +382,20 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== -"@types/node@^20.4.8": - version "20.4.8" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.8.tgz#b5dda19adaa473a9bf0ab5cbd8f30ec7d43f5c85" - integrity sha512-0mHckf6D2DiIAzh8fM8f3HQCvMKDpK94YQ0DSVkfWTG9BZleYIWudw9cJxX8oCk9bM+vAkDyujDV6dmKHbvQpg== +"@types/node@^20.5.7": + version "20.5.7" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.7.tgz#4b8ecac87fbefbc92f431d09c30e176fc0a7c377" + integrity sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA== "@types/semver@^7.3.12": version "7.3.13" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91" integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw== -"@types/sortablejs@^1.15.1": - version "1.15.1" - resolved "https://registry.yarnpkg.com/@types/sortablejs/-/sortablejs-1.15.1.tgz#123abafbe936f754fee5eb5b49009ce1f1075aa5" - integrity sha512-g/JwBNToh6oCTAwNS8UGVmjO7NLDKsejVhvE4x1eWiPTC3uCuNsa/TD4ssvX3du+MLiM+SHPNDuijp8y76JzLQ== +"@types/sortablejs@^1.15.2": + version "1.15.2" + resolved "https://registry.yarnpkg.com/@types/sortablejs/-/sortablejs-1.15.2.tgz#d51e5ecac00a9782aa256c1401309ce1c4031ba2" + integrity sha512-mOIv/EnPMzAZAVbuh9uGjOZ1BBdimP9Y6IPGntsvQJtko5yapSDKB7GwB3AOlF5N3bkpk4sBwQRpS3aEkiUbaA== "@types/spark-md5@^3.0.2": version "3.0.2" @@ -486,10 +486,10 @@ "@typescript-eslint/types" "5.59.1" eslint-visitor-keys "^3.3.0" -"@vitejs/plugin-vue@^4.2.3": - version "4.2.3" - resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-4.2.3.tgz#ee0b6dfcc62fe65364e6395bf38fa2ba10bb44b6" - integrity sha512-R6JDUfiZbJA9cMiguQ7jxALsgiprjBeHL5ikpXfJCH62pPHtI+JdJ5xWj6Ev73yXSlYl86+blXn1kZHQ7uElxw== +"@vitejs/plugin-vue@^4.3.3": + version "4.3.3" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-4.3.3.tgz#3b2337f64495f95cfea5b1497d2d3f4a0b3382b2" + integrity sha512-ssxyhIAZqB0TrpUg6R0cBpCuMk9jTIlO1GNSKKQD6S8VjnXi6JXKfUXjSsxey9IwQiaRGsO1WnW9Rkl1L6AJVw== "@volar/language-core@1.10.0", "@volar/language-core@~1.10.0": version "1.10.0" @@ -1104,10 +1104,10 @@ escodegen@^2.0.0: optionalDependencies: source-map "~0.6.1" -eslint-plugin-vue@^9.16.1: - version "9.16.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-9.16.1.tgz#3508d9279d797b40889db76da2fd26524e9144e6" - integrity sha512-2FtnTqazA6aYONfDuOZTk0QzwhAwi7Z4+uJ7+GHeGxcKapjqWlDsRWDenvyG/utyOfAS5bVRmAG3cEWiYEz2bA== +eslint-plugin-vue@^9.17.0: + version "9.17.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-9.17.0.tgz#4501547373f246547083482838b4c8f4b28e5932" + integrity sha512-r7Bp79pxQk9I5XDP0k2dpUC7Ots3OSWgvGZNu3BxmKK6Zg7NgVtcOB6OCna5Kb9oQwJPl5hq183WD0SY5tZtIQ== dependencies: "@eslint-community/eslint-utils" "^4.4.0" natural-compare "^1.4.0" @@ -1163,20 +1163,20 @@ eslint-visitor-keys@^3.4.1: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994" integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA== -eslint-visitor-keys@^3.4.2: - version "3.4.2" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.2.tgz#8c2095440eca8c933bedcadf16fefa44dbe9ba5f" - integrity sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw== +eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== -eslint@^8.46.0: - version "8.46.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.46.0.tgz#a06a0ff6974e53e643acc42d1dcf2e7f797b3552" - integrity sha512-cIO74PvbW0qU8e0mIvk5IV3ToWdCq5FYG6gWPHHkx6gNdjlbAYvtfHmlCMXxjcoVaIdwy/IAt3+mDkZkfvb2Dg== +eslint@^8.48.0: + version "8.48.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.48.0.tgz#bf9998ba520063907ba7bfe4c480dc8be03c2155" + integrity sha512-sb6DLeIuRXxeM1YljSe1KEx9/YYeZFQWcV8Rq9HfigmdDEugjLEVEa1ozDjL6YDjBpQHPJxJzze+alxi4T3OLg== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.6.1" - "@eslint/eslintrc" "^2.1.1" - "@eslint/js" "^8.46.0" + "@eslint/eslintrc" "^2.1.2" + "@eslint/js" "8.48.0" "@humanwhocodes/config-array" "^0.11.10" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" @@ -1187,7 +1187,7 @@ eslint@^8.46.0: doctrine "^3.0.0" escape-string-regexp "^4.0.0" eslint-scope "^7.2.2" - eslint-visitor-keys "^3.4.2" + eslint-visitor-keys "^3.4.3" espree "^9.6.1" esquery "^1.4.2" esutils "^2.0.2" @@ -2469,10 +2469,10 @@ type-fest@^0.20.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== -typescript@^5.1.6: - version "5.1.6" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274" - integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== +typescript@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78" + integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w== ufo@^1.1.2: version "1.1.2" diff --git a/webapp_dist/js/app.js.gz b/webapp_dist/js/app.js.gz index 3995a6ee9..7e0989bc9 100644 Binary files a/webapp_dist/js/app.js.gz and b/webapp_dist/js/app.js.gz differ