Merge branch 'development'

This commit is contained in:
helgeerbe 2023-08-29 10:16:33 +02:00
commit d5308b1029
36 changed files with 318 additions and 199 deletions

View File

@ -18,5 +18,50 @@
"type": 0, "type": 0,
"clk_mode": 3 "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
}
} }
] ]

View File

@ -19,6 +19,7 @@ public:
void init(DisplayType_t type, uint8_t data, uint8_t clk, uint8_t cs, uint8_t reset); void init(DisplayType_t type, uint8_t data, uint8_t clk, uint8_t cs, uint8_t reset);
void loop(); void loop();
void setContrast(uint8_t contrast); void setContrast(uint8_t contrast);
void setStatus(bool turnOn);
void setOrientation(uint8_t rotation = DISPLAY_ROTATION); void setOrientation(uint8_t rotation = DISPLAY_ROTATION);
void setLanguage(uint8_t language); void setLanguage(uint8_t language);
void setStartupDisplay(); void setStartupDisplay();
@ -33,6 +34,8 @@ private:
U8G2* _display; U8G2* _display;
bool _displayTurnedOn;
DisplayType_t _display_type = DisplayType_t::None; DisplayType_t _display_type = DisplayType_t::None;
uint8_t _display_language = DISPLAY_LANGUAGE; uint8_t _display_language = DISPLAY_LANGUAGE;
uint8_t _mExtra; uint8_t _mExtra;

View File

@ -19,6 +19,9 @@ public:
void init(); void init();
void loop(); void loop();
void turnAllOff();
void turnAllOn();
private: private:
enum class LedState_t { enum class LedState_t {
On, On,
@ -27,6 +30,7 @@ private:
}; };
LedState_t _ledState[PINMAPPING_LED_COUNT]; LedState_t _ledState[PINMAPPING_LED_COUNT];
LedState_t _allState;
TimeoutHelper _updateTimeout; TimeoutHelper _updateTimeout;
TimeoutHelper _blinkTimeout; TimeoutHelper _blinkTimeout;
uint8_t _ledActive = 0; uint8_t _ledActive = 0;

View File

@ -10,7 +10,7 @@ public:
void loop(); void loop();
private: 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 _lastPublishStats;
uint32_t _lastPublish; uint32_t _lastPublish;

View File

@ -51,8 +51,15 @@ public:
void loop(); void loop();
uint8_t getPowerLimiterState(); uint8_t getPowerLimiterState();
int32_t getLastRequestedPowerLimit(); 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(); void calcNextInverterRestart();
private: private:
@ -63,7 +70,7 @@ private:
uint32_t _lastCalculation = 0; uint32_t _lastCalculation = 0;
static constexpr uint32_t _calculationBackoffMsDefault = 128; static constexpr uint32_t _calculationBackoffMsDefault = 128;
uint32_t _calculationBackoffMs = _calculationBackoffMsDefault; uint32_t _calculationBackoffMs = _calculationBackoffMsDefault;
uint8_t _mode = PL_MODE_ENABLE_NORMAL_OP; Mode _mode = Mode::Normal;
std::shared_ptr<InverterAbstract> _inverter = nullptr; std::shared_ptr<InverterAbstract> _inverter = nullptr;
bool _batteryDischargeEnabled = false; bool _batteryDischargeEnabled = false;
uint32_t _nextInverterRestart = 0; // Values: 0->not calculated / 1->no restart configured / >1->time of next inverter restart in millis() uint32_t _nextInverterRestart = 0; // Values: 0->not calculated / 1->no restart configured / >1->time of next inverter restart in millis()

View File

@ -8,4 +8,5 @@ public:
static uint32_t getChipId(); static uint32_t getChipId();
static uint64_t generateDtuSerial(); static uint64_t generateDtuSerial();
static int getTimezoneOffset(); static int getTimezoneOffset();
static void restartDtu();
}; };

View File

@ -13,33 +13,38 @@ public:
private: private:
void onPrometheusMetricsGet(AsyncWebServerRequest* request); void onPrometheusMetricsGet(AsyncWebServerRequest* request);
void addField(AsyncResponseStream* stream, String& serial, uint8_t idx, std::shared_ptr<InverterAbstract> 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<InverterAbstract> 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<InverterAbstract> inv, ChannelType_t type, ChannelNum_t channel); void addPanelInfo(AsyncResponseStream* stream, String& serial, uint8_t idx, std::shared_ptr<InverterAbstract> inv, ChannelType_t type, ChannelNum_t channel);
AsyncWebServer* _server; AsyncWebServer* _server;
enum { enum MetricType_t {
METRIC_TYPE_NONE = 0, NONE = 0,
METRIC_TYPE_GAUGE, GAUGE,
METRIC_TYPE_COUNTER, COUNTER,
}; };
const char* _metricTypes[3] = { 0, "gauge", "counter" }; const char* _metricTypes[3] = { 0, "gauge", "counter" };
std::map<FieldId_t, uint8_t> _fieldMetricAssignment { struct publish_type_t {
{ FLD_UDC, METRIC_TYPE_GAUGE }, FieldId_t field;
{ FLD_IDC, METRIC_TYPE_GAUGE }, MetricType_t type;
{ FLD_PDC, METRIC_TYPE_GAUGE }, };
{ FLD_YD, METRIC_TYPE_COUNTER },
{ FLD_YT, METRIC_TYPE_COUNTER }, const publish_type_t _publishFields[14] = {
{ FLD_UAC, METRIC_TYPE_GAUGE }, { FLD_PAC, MetricType_t::GAUGE },
{ FLD_IAC, METRIC_TYPE_GAUGE }, { FLD_UAC, MetricType_t::GAUGE },
{ FLD_PAC, METRIC_TYPE_GAUGE }, { FLD_IAC, MetricType_t::GAUGE },
{ FLD_F, METRIC_TYPE_GAUGE }, { FLD_PDC, MetricType_t::GAUGE },
{ FLD_T, METRIC_TYPE_GAUGE }, { FLD_UDC, MetricType_t::GAUGE },
{ FLD_PF, METRIC_TYPE_GAUGE }, { FLD_IDC, MetricType_t::GAUGE },
{ FLD_EFF, METRIC_TYPE_GAUGE }, { FLD_YD, MetricType_t::COUNTER },
{ FLD_IRR, METRIC_TYPE_GAUGE }, { FLD_YT, MetricType_t::COUNTER },
{ FLD_Q, METRIC_TYPE_GAUGE } { 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 },
}; };
}; };

View File

@ -91,10 +91,22 @@ void HoymilesClass::loop()
} }
// Fetch dev info (but first fetch stats) // Fetch dev info (but first fetch stats)
if (iv->Statistics()->getLastUpdate() > 0 && (iv->DevInfo()->getLastUpdateAll() == 0 || iv->DevInfo()->getLastUpdateSimple() == 0)) { 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"); _messageOutput->println("Request device info");
iv->sendDevInfoRequest(); iv->sendDevInfoRequest();
} }
}
if (++inverterPos >= getNumInverters()) { if (++inverterPos >= getNumInverters()) {
inverterPos = 0; inverterPos = 0;

View File

@ -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_UAC, UNIT_V, 50, 2, 10, false, 1 },
{ TYPE_AC, CH0, FLD_IAC, UNIT_A, 58, 2, 100, false, 2 }, { 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_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_F, UNIT_HZ, 52, 2, 100, false, 2 },
{ TYPE_AC, CH0, FLD_PF, UNIT_NONE, 60, 2, 1000, false, 3 }, { TYPE_AC, CH0, FLD_PF, UNIT_NONE, 60, 2, 1000, false, 3 },

View File

@ -196,6 +196,16 @@ String DevInfoParser::getHwModelName()
return devInfo[idx].modelName; 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 DevInfoParser::getDevIdx()
{ {
uint8_t ret = 0xff; uint8_t ret = 0xff;

View File

@ -33,6 +33,8 @@ public:
uint16_t getMaxPower(); uint16_t getMaxPower();
String getHwModelName(); String getHwModelName();
bool containsValidData();
private: private:
time_t timegm(struct tm* tm); time_t timegm(struct tm* tm);
uint8_t getDevIdx(); uint8_t getDevIdx();

View File

@ -150,6 +150,13 @@ float StatisticsParser::getChannelFieldValue(ChannelType_t type, ChannelNum_t ch
return 0; return 0;
} }
String StatisticsParser::getChannelFieldValueString(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId)
{
return String(
getChannelFieldValue(type, channel, fieldId),
static_cast<unsigned int>(getChannelFieldDigits(type, channel, fieldId)));
}
bool StatisticsParser::hasChannelFieldValue(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) bool StatisticsParser::hasChannelFieldValue(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId)
{ {
const byteAssign_t* pos = getAssignmentByChannelField(type, channel, fieldId); const byteAssign_t* pos = getAssignmentByChannelField(type, channel, fieldId);

View File

@ -119,6 +119,7 @@ public:
fieldSettings_t* getSettingByChannelField(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); fieldSettings_t* getSettingByChannelField(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId);
float getChannelFieldValue(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); 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* getChannelFieldUnit(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId);
const char* getChannelFieldName(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId); const char* getChannelFieldName(ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId);

View File

@ -15,6 +15,7 @@ if missing_pkgs:
from dulwich import porcelain from dulwich import porcelain
def get_firmware_specifier_build_flag(): def get_firmware_specifier_build_flag():
try: try:
build_version = porcelain.describe('.') # '.' refers to the repository root dir build_version = porcelain.describe('.') # '.' refers to the repository root dir

View File

@ -47,6 +47,7 @@ void DisplayGraphicClass::init(DisplayType_t type, uint8_t data, uint8_t clk, ui
_display = constructor(reset, clk, data, cs); _display = constructor(reset, clk, data, cs);
_display->begin(); _display->begin();
setContrast(DISPLAY_CONTRAST); setContrast(DISPLAY_CONTRAST);
setStatus(true);
} }
} }
@ -139,10 +140,11 @@ void DisplayGraphicClass::loop()
if ((millis() - _lastDisplayUpdate) > _period) { if ((millis() - _lastDisplayUpdate) > _period) {
_display->clearBuffer(); _display->clearBuffer();
bool displayPowerSave = false;
//=====> Actual Production ========== //=====> Actual Production ==========
if (Datastore.getIsAtLeastOneReachable()) { if (Datastore.getIsAtLeastOneReachable()) {
_display->setPowerSave(false); displayPowerSave = false;
if (Datastore.getTotalAcPowerEnabled() > 999) { if (Datastore.getTotalAcPowerEnabled() > 999) {
snprintf(_fmtText, sizeof(_fmtText), i18n_current_power_kw[_display_language], (Datastore.getTotalAcPowerEnabled() / 1000)); snprintf(_fmtText, sizeof(_fmtText), i18n_current_power_kw[_display_language], (Datastore.getTotalAcPowerEnabled() / 1000));
} else { } else {
@ -158,7 +160,7 @@ void DisplayGraphicClass::loop()
printText(i18n_offline[_display_language], 0); printText(i18n_offline[_display_language], 0);
// check if it's time to enter power saving mode // check if it's time to enter power saving mode
if (millis() - _previousMillis >= (_interval * 2)) { if (millis() - _previousMillis >= (_interval * 2)) {
_display->setPowerSave(enablePowerSafe); displayPowerSave = enablePowerSafe;
} }
} }
//<======================= //<=======================
@ -184,6 +186,12 @@ void DisplayGraphicClass::loop()
_mExtra++; _mExtra++;
_lastDisplayUpdate = millis(); _lastDisplayUpdate = millis();
if (!_displayTurnedOn) {
displayPowerSave = true;
}
_display->setPowerSave(displayPowerSave);
} }
} }
@ -195,4 +203,9 @@ void DisplayGraphicClass::setContrast(uint8_t contrast)
_display->setContrast(contrast * 2.55f); _display->setContrast(contrast * 2.55f);
} }
void DisplayGraphicClass::setStatus(bool turnOn)
{
_displayTurnedOn = turnOn;
}
DisplayGraphicClass Display; DisplayGraphicClass Display;

View File

@ -20,6 +20,7 @@ void LedSingleClass::init()
{ {
_blinkTimeout.set(500); _blinkTimeout.set(500);
_updateTimeout.set(LEDSINGLE_UPDATE_INTERVAL); _updateTimeout.set(LEDSINGLE_UPDATE_INTERVAL);
turnAllOn();
for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) { for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) {
auto& pin = PinMapping.get(); auto& pin = PinMapping.get();
@ -40,7 +41,7 @@ void LedSingleClass::loop()
return; return;
} }
if (_updateTimeout.occured()) { if (_updateTimeout.occured() && _allState == LedState_t::On) {
const CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
// Update network status // Update network status
@ -68,6 +69,9 @@ void LedSingleClass::loop()
} }
_updateTimeout.reset(); _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++) { 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;
}

View File

@ -62,7 +62,7 @@ void MqttHandleVedirectHassClass::publishConfig()
publishSensor("MPPT error code", "ERR"); publishSensor("MPPT error code", "ERR");
publishSensor("MPPT off reason", "OR"); publishSensor("MPPT off reason", "OR");
publishSensor("MPPT tracker operation mode", "MPPT"); 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 // battery info
publishSensor("Battery voltage", "V", "voltage", "measurement", "V"); publishSensor("Battery voltage", "V", "voltage", "measurement", "V");
@ -71,11 +71,10 @@ void MqttHandleVedirectHassClass::publishConfig()
// panel info // panel info
publishSensor("Panel voltage", "VPV", "voltage", "measurement", "V"); publishSensor("Panel voltage", "VPV", "voltage", "measurement", "V");
publishSensor("Panel power", "PPV", "power", "measurement", "W"); 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 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 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"); publishSensor("Panel maximum power yesterday", "H23", "power", "measurement", "W");
yield(); yield();
@ -87,6 +86,9 @@ void MqttHandleVedirectHassClass::publishSensor(const char* caption, const char*
String sensorId = caption; String sensorId = caption;
sensorId.replace(" ", "_"); sensorId.replace(" ", "_");
sensorId.replace(".", "");
sensorId.replace("(", "");
sensorId.replace(")", "");
sensorId.toLowerCase(); sensorId.toLowerCase();
String configTopic = "sensor/dtu_victron_" + serial String configTopic = "sensor/dtu_victron_" + serial
@ -131,6 +133,9 @@ void MqttHandleVedirectHassClass::publishBinarySensor(const char* caption, const
String sensorId = caption; String sensorId = caption;
sensorId.replace(" ", "_"); sensorId.replace(" ", "_");
sensorId.replace(".", "");
sensorId.replace("(", "");
sensorId.replace(")", "");
sensorId.toLowerCase(); sensorId.toLowerCase();
String configTopic = "binary_sensor/dtu_victron_" + serial String configTopic = "binary_sensor/dtu_victron_" + serial

View File

@ -126,11 +126,7 @@ void MqttHandleInverterClass::publishField(std::shared_ptr<InverterAbstract> inv
return; return;
} }
String value = String( MqttSettings.publish(topic, inv->Statistics()->getChannelFieldValueString(type, channel, fieldId));
inv->Statistics()->getChannelFieldValue(type, channel, fieldId),
static_cast<unsigned int>(inv->Statistics()->getChannelFieldDigits(type, channel, fieldId)));
MqttSettings.publish(topic, value);
} }
String MqttHandleInverterClass::getTopic(std::shared_ptr<InverterAbstract> inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId) String MqttHandleInverterClass::getTopic(std::shared_ptr<InverterAbstract> inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId)

View File

@ -7,8 +7,7 @@
#include "MqttHandlePowerLimiter.h" #include "MqttHandlePowerLimiter.h"
#include "PowerLimiter.h" #include "PowerLimiter.h"
#include <ctime> #include <ctime>
#include <string>
#define TOPIC_SUB_POWER_LIMITER "mode"
MqttHandlePowerLimiterClass MqttHandlePowerLimiter; MqttHandlePowerLimiterClass MqttHandlePowerLimiter;
@ -21,11 +20,10 @@ void MqttHandlePowerLimiterClass::init()
using std::placeholders::_5; using std::placeholders::_5;
using std::placeholders::_6; using std::placeholders::_6;
String topic = MqttSettings.getPrefix(); String topic = MqttSettings.getPrefix() + "powerlimiter/cmd/mode";
MqttSettings.subscribe(String(topic + "powerlimiter/cmd/" + TOPIC_SUB_POWER_LIMITER).c_str(), 0, std::bind(&MqttHandlePowerLimiterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); MqttSettings.subscribe(topic.c_str(), 0, std::bind(&MqttHandlePowerLimiterClass::onCmdMode, this, _1, _2, _3, _4, _5, _6));
_lastPublish = millis(); _lastPublish = millis();
} }
@ -38,7 +36,8 @@ void MqttHandlePowerLimiterClass::loop()
const CONFIG_T& config = Configuration.get(); const CONFIG_T& config = Configuration.get();
if ((millis() - _lastPublish) > (config.Mqtt_PublishInterval * 1000) ) { if ((millis() - _lastPublish) > (config.Mqtt_PublishInterval * 1000) ) {
MqttSettings.publish("powerlimiter/status/mode", String(PowerLimiter.getMode())); auto val = static_cast<unsigned>(PowerLimiter.getMode());
MqttSettings.publish("powerlimiter/status/mode", String(val));
yield(); yield();
_lastPublish = millis(); _lastPublish = millis();
@ -46,7 +45,8 @@ void MqttHandlePowerLimiterClass::loop()
} }
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(); const CONFIG_T& config = Configuration.get();
@ -55,43 +55,33 @@ void MqttHandlePowerLimiterClass::onMqttMessage(const espMqttClientTypes::Messag
return; return;
} }
char token_topic[MQTT_MAX_TOPIC_STRLEN + 40]; // respect all subtopics std::string strValue(reinterpret_cast<const char*>(payload), len);
strncpy(token_topic, topic, MQTT_MAX_TOPIC_STRLEN + 40); // convert const char* to char* int intValue = -1;
try {
char* setting; intValue = std::stoi(strValue);
char* rest = &token_topic[strlen(config.Mqtt_Topic)]; }
catch (std::invalid_argument const& e) {
strtok_r(rest, "/", &rest); // Remove "powerlimiter" MessageOutput.printf("PowerLimiter MQTT handler: cannot parse payload of topic '%s' as int: %s\r\n",
strtok_r(rest, "/", &rest); // Remove "cmd" topic, strValue.c_str());
setting = strtok_r(rest, "/", &rest);
if (setting == NULL) {
return; return;
} }
char* str = new char[len + 1]; using Mode = PowerLimiterClass::Mode;
memcpy(str, payload, len); switch (static_cast<Mode>(intValue)) {
str[len] = '\0'; case Mode::UnconditionalFullSolarPassthrough:
uint8_t payload_val = atoi(str); MessageOutput.println("Power limiter unconditional full solar PT");
delete[] str; PowerLimiter.setMode(Mode::UnconditionalFullSolarPassthrough);
break;
if (!strcmp(setting, TOPIC_SUB_POWER_LIMITER)) { case Mode::Disabled:
if(payload_val == 2) { MessageOutput.println("Power limiter disabled (override)");
MessageOutput.println("Power limiter full solar PT"); PowerLimiter.setMode(Mode::Disabled);
PowerLimiter.setMode(PL_MODE_SOLAR_PT_ONLY); break;
return; case Mode::Normal:
} MessageOutput.println("Power limiter normal operation");
if(payload_val == 1) { PowerLimiter.setMode(Mode::Normal);
MessageOutput.println("Power limiter disabled"); break;
PowerLimiter.setMode(PL_MODE_FULL_DISABLE); default:
return; MessageOutput.printf("PowerLimiter - unknown mode %d\r\n", intValue);
} break;
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");
} }
} }

View File

@ -95,6 +95,7 @@ void MqttHandlePylontechHassClass::publishSensor(const char* caption, const char
{ {
String sensorId = caption; String sensorId = caption;
sensorId.replace(" ", "_"); sensorId.replace(" ", "_");
sensorId.replace(".", "");
sensorId.replace("(", ""); sensorId.replace("(", "");
sensorId.replace(")", ""); sensorId.replace(")", "");
sensorId.toLowerCase(); sensorId.toLowerCase();
@ -140,6 +141,9 @@ void MqttHandlePylontechHassClass::publishBinarySensor(const char* caption, cons
{ {
String sensorId = caption; String sensorId = caption;
sensorId.replace(" ", "_"); sensorId.replace(" ", "_");
sensorId.replace(".", "");
sensorId.replace("(", "");
sensorId.replace(")", "");
sensorId.toLowerCase(); sensorId.toLowerCase();
String configTopic = "binary_sensor/dtu_battery_" + serial String configTopic = "binary_sensor/dtu_battery_" + serial

View File

@ -131,7 +131,7 @@ void PowerLimiterClass::loop()
return; return;
} }
if (PL_MODE_FULL_DISABLE == _mode) { if (Mode::Disabled == _mode) {
shutdown(Status::DisabledByMqtt); shutdown(Status::DisabledByMqtt);
return; return;
} }
@ -185,7 +185,7 @@ void PowerLimiterClass::loop()
return announceStatus(Status::InverterDevInfoPending); return announceStatus(Status::InverterDevInfoPending);
} }
if (PL_MODE_SOLAR_PT_ONLY == _mode) { if (Mode::UnconditionalFullSolarPassthrough == _mode) {
// handle this mode of operation separately // handle this mode of operation separately
return unconditionalSolarPassthrough(_inverter); return unconditionalSolarPassthrough(_inverter);
} }
@ -398,14 +398,6 @@ int32_t PowerLimiterClass::getLastRequestedPowerLimit() {
return _lastRequestedPowerLimit; return _lastRequestedPowerLimit;
} }
bool PowerLimiterClass::getMode() {
return _mode;
}
void PowerLimiterClass::setMode(uint8_t mode) {
_mode = mode;
}
bool PowerLimiterClass::canUseDirectSolarPower() bool PowerLimiterClass::canUseDirectSolarPower()
{ {
CONFIG_T& config = Configuration.get(); CONFIG_T& config = Configuration.get();

View File

@ -3,6 +3,8 @@
* Copyright (C) 2022 - 2023 Thomas Basler and others * Copyright (C) 2022 - 2023 Thomas Basler and others
*/ */
#include "Utils.h" #include "Utils.h"
#include "Display_Graphic.h"
#include "Led_Single.h"
#include <Esp.h> #include <Esp.h>
uint32_t Utils::getChipId() uint32_t Utils::getChipId()
@ -53,3 +55,13 @@ int Utils::getTimezoneOffset()
return static_cast<int>(difftime(rawtime, gmt)); return static_cast<int>(difftime(rawtime, gmt));
} }
void Utils::restartDtu()
{
LedSingle.turnAllOff();
Display.setStatus(false);
yield();
delay(1000);
yield();
ESP.restart();
}

View File

@ -4,6 +4,7 @@
*/ */
#include "WebApi_config.h" #include "WebApi_config.h"
#include "Configuration.h" #include "Configuration.h"
#include "Utils.h"
#include "WebApi.h" #include "WebApi.h"
#include "WebApi_errors.h" #include "WebApi_errors.h"
#include <AsyncJson.h> #include <AsyncJson.h>
@ -114,7 +115,7 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request)
request->send(response); request->send(response);
LittleFS.remove(CONFIG_FILENAME); LittleFS.remove(CONFIG_FILENAME);
ESP.restart(); Utils::restartDtu();
} }
void WebApiConfigClass::onConfigListGet(AsyncWebServerRequest* request) void WebApiConfigClass::onConfigListGet(AsyncWebServerRequest* request)
@ -157,10 +158,7 @@ void WebApiConfigClass::onConfigUploadFinish(AsyncWebServerRequest* request)
response->addHeader("Connection", "close"); response->addHeader("Connection", "close");
response->addHeader("Access-Control-Allow-Origin", "*"); response->addHeader("Access-Control-Allow-Origin", "*");
request->send(response); request->send(response);
yield(); Utils::restartDtu();
delay(1000);
yield();
ESP.restart();
} }
void WebApiConfigClass::onConfigUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final) void WebApiConfigClass::onConfigUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final)

View File

@ -6,6 +6,7 @@
#include "Configuration.h" #include "Configuration.h"
#include "Display_Graphic.h" #include "Display_Graphic.h"
#include "PinMapping.h" #include "PinMapping.h"
#include "Utils.h"
#include "WebApi.h" #include "WebApi.h"
#include "WebApi_errors.h" #include "WebApi_errors.h"
#include "helper.h" #include "helper.h"
@ -185,9 +186,6 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request)
request->send(response); request->send(response);
if (performRestart) { if (performRestart) {
yield(); Utils::restartDtu();
delay(1000);
yield();
ESP.restart();
} }
} }

View File

@ -5,6 +5,7 @@
#include "WebApi_firmware.h" #include "WebApi_firmware.h"
#include "Configuration.h" #include "Configuration.h"
#include "Update.h" #include "Update.h"
#include "Utils.h"
#include "WebApi.h" #include "WebApi.h"
#include "helper.h" #include "helper.h"
#include <AsyncJson.h> #include <AsyncJson.h>
@ -42,10 +43,7 @@ void WebApiFirmwareClass::onFirmwareUpdateFinish(AsyncWebServerRequest* request)
response->addHeader("Connection", "close"); response->addHeader("Connection", "close");
response->addHeader("Access-Control-Allow-Origin", "*"); response->addHeader("Access-Control-Allow-Origin", "*");
request->send(response); request->send(response);
yield(); Utils::restartDtu();
delay(1000);
yield();
ESP.restart();
} }
void WebApiFirmwareClass::onFirmwareUpdateUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final) void WebApiFirmwareClass::onFirmwareUpdateUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final)

View File

@ -34,7 +34,7 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request)
return; return;
} }
AsyncJsonResponse* response = new AsyncJsonResponse(false, 4096U); AsyncJsonResponse* response = new AsyncJsonResponse(false, 768 * INV_MAX_COUNT);
JsonObject root = response->getRoot(); JsonObject root = response->getRoot();
JsonArray data = root.createNestedArray("inverter"); JsonArray data = root.createNestedArray("inverter");

View File

@ -4,6 +4,7 @@
*/ */
#include "WebApi_maintenance.h" #include "WebApi_maintenance.h"
#include "Utils.h"
#include "WebApi.h" #include "WebApi.h"
#include "WebApi_errors.h" #include "WebApi_errors.h"
#include <AsyncJson.h> #include <AsyncJson.h>
@ -75,10 +76,7 @@ void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request)
response->setLength(); response->setLength();
request->send(response); request->send(response);
yield(); Utils::restartDtu();
delay(1000);
yield();
ESP.restart();
} else { } else {
retMsg["message"] = "Reboot cancled!"; retMsg["message"] = "Reboot cancled!";
retMsg["code"] = WebApiError::MaintenanceRebootCancled; retMsg["code"] = WebApiError::MaintenanceRebootCancled;

View File

@ -125,7 +125,7 @@ void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request)
CONFIG_T& config = Configuration.get(); CONFIG_T& config = Configuration.get();
config.PowerLimiter_Enabled = root[F("enabled")].as<bool>(); config.PowerLimiter_Enabled = root[F("enabled")].as<bool>();
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<bool>(); config.PowerLimiter_VerboseLogging = root[F("verbose_logging")].as<bool>();
config.PowerLimiter_SolarPassThroughEnabled = root[F("solar_passthrough_enabled")].as<bool>(); config.PowerLimiter_SolarPassThroughEnabled = root[F("solar_passthrough_enabled")].as<bool>();
config.PowerLimiter_SolarPassThroughLosses = root[F("solar_passthrough_losses")].as<uint8_t>(); config.PowerLimiter_SolarPassThroughLosses = root[F("solar_passthrough_losses")].as<uint8_t>();

View File

@ -75,24 +75,13 @@ void WebApiPrometheusClass::onPrometheusMetricsGet(AsyncWebServerRequest* reques
for (auto& t : inv->Statistics()->getChannelTypes()) { for (auto& t : inv->Statistics()->getChannelTypes()) {
for (auto& c : inv->Statistics()->getChannelsByType(t)) { for (auto& c : inv->Statistics()->getChannelsByType(t)) {
addPanelInfo(stream, serial, i, inv, t, c); addPanelInfo(stream, serial, i, inv, t, c);
addField(stream, serial, i, inv, t, c, FLD_PAC); for (uint8_t f = 0; f < sizeof(_publishFields) / sizeof(_publishFields[0]); f++) {
addField(stream, serial, i, inv, t, c, FLD_UAC); if (t == TYPE_AC && _publishFields[f].field == FLD_PDC) {
addField(stream, serial, i, inv, t, c, FLD_IAC); addField(stream, serial, i, inv, t, c, _publishFields[f].field, _metricTypes[_publishFields[f].type], "PowerDC");
if (t == TYPE_AC) {
addField(stream, serial, i, inv, t, c, FLD_PDC, "PowerDC");
} else { } else {
addField(stream, serial, i, inv, t, c, FLD_PDC); 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<InverterAbstract> 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<InverterAbstract> inv, ChannelType_t type, ChannelNum_t channel, FieldId_t fieldId, const char* metricName, const char* channelName)
{ {
if (inv->Statistics()->hasChannelFieldValue(type, channel, fieldId)) { if (inv->Statistics()->hasChannelFieldValue(type, channel, fieldId)) {
const char* chanName = (channelName == NULL) ? inv->Statistics()->getChannelFieldName(type, channel, fieldId) : channelName; const char* chanName = (channelName == NULL) ? inv->Statistics()->getChannelFieldName(type, channel, fieldId) : channelName;
if (idx == 0 && type == TYPE_AC && channel == 0) { 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("# 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, chanName,
serial.c_str(), serial.c_str(),
idx, idx,
inv->name(), inv->name(),
inv->Statistics()->getChannelTypeName(type), inv->Statistics()->getChannelTypeName(type),
channel, channel,
inv->Statistics()->getChannelFieldValue(type, channel, fieldId)); inv->Statistics()->getChannelFieldValueString(type, channel, fieldId).c_str());
} }
} }

1
webapp/.gitignore vendored
View File

@ -13,6 +13,7 @@ dist
dist-ssr dist-ssr
coverage coverage
*.local *.local
vite.user.ts
/cypress/videos/ /cypress/videos/
/cypress/screenshots/ /cypress/screenshots/

View File

@ -22,22 +22,22 @@
"vue-router": "^4.2.4" "vue-router": "^4.2.4"
}, },
"devDependencies": { "devDependencies": {
"@intlify/unplugin-vue-i18n": "^0.12.2", "@intlify/unplugin-vue-i18n": "^0.12.3",
"@rushstack/eslint-patch": "^1.3.3", "@rushstack/eslint-patch": "^1.3.3",
"@tsconfig/node18": "^18.2.0", "@tsconfig/node18": "^18.2.1",
"@types/bootstrap": "^5.2.6", "@types/bootstrap": "^5.2.6",
"@types/node": "^20.4.8", "@types/node": "^20.5.7",
"@types/sortablejs": "^1.15.1", "@types/sortablejs": "^1.15.2",
"@types/spark-md5": "^3.0.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/eslint-config-typescript": "^11.0.3",
"@vue/tsconfig": "^0.4.0", "@vue/tsconfig": "^0.4.0",
"eslint": "^8.46.0", "eslint": "^8.48.0",
"eslint-plugin-vue": "^9.16.1", "eslint-plugin-vue": "^9.17.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"sass": "^1.64.2", "sass": "^1.64.2",
"terser": "^5.19.2", "terser": "^5.19.2",
"typescript": "^5.1.6", "typescript": "^5.2.2",
"vite": "^4.4.9", "vite": "^4.4.9",
"vite-plugin-compression": "^0.5.1", "vite-plugin-compression": "^0.5.1",
"vite-plugin-css-injected-by-js": "^3.3.0", "vite-plugin-css-injected-by-js": "^3.3.0",

View File

@ -690,9 +690,9 @@
"DefaultProfile": "(Standardeinstellungen)", "DefaultProfile": "(Standardeinstellungen)",
"ProfileHint": "Ihr Gerät reagiert möglicherweise nicht mehr, wenn Sie ein inkompatibles Profil wählen. In diesem Fall müssen Sie eine Löschung über das serielle Interface durchführen.", "ProfileHint": "Ihr Gerät reagiert möglicherweise nicht mehr, wenn Sie ein inkompatibles Profil wählen. In diesem Fall müssen Sie eine Löschung über das serielle Interface durchführen.",
"Display": "Display", "Display": "Display",
"PowerSafe": "Power Safe aktivieren:", "PowerSafe": "Stromsparen aktivieren:",
"PowerSafeHint": "Schaltet das Display aus, wenn kein Wechselrichter Strom erzeugt", "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)", "ScreensaverHint": "Bewegt die Ausgabe bei jeder Aktualisierung um ein Einbrennen zu verhindern (v. a. für OLED-Displays nützlich)",
"Contrast": "Kontrast ({contrast}):", "Contrast": "Kontrast ({contrast}):",
"Rotation": "Rotation:", "Rotation": "Rotation:",

View File

@ -699,7 +699,7 @@
"DefaultProfile": "(Default settings)", "DefaultProfile": "(Default settings)",
"ProfileHint": "Your device may stop responding if you select an incompatible profile. In this case, you must perform a deletion via the serial interface.", "ProfileHint": "Your device may stop responding if you select an incompatible profile. In this case, you must perform a deletion via the serial interface.",
"Display": "Display", "Display": "Display",
"PowerSafe": "Enable Power Safe:", "PowerSafe": "Enable Power Save:",
"PowerSafeHint": "Turn off the display if no inverter is producing.", "PowerSafeHint": "Turn off the display if no inverter is producing.",
"Screensaver": "Enable Screensaver:", "Screensaver": "Enable Screensaver:",
"ScreensaverHint": "Move the display a little bit on each update to prevent burn-in. (Useful especially for OLED displays)", "ScreensaverHint": "Move the display a little bit on each update to prevent burn-in. (Useful especially for OLED displays)",

View File

@ -9,6 +9,14 @@ import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'
const path = require('path') 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/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
@ -52,20 +60,25 @@ export default defineConfig({
server: { server: {
proxy: { proxy: {
'^/api': { '^/api': {
target: 'http://192.168.178.87/' target: 'http://' + proxy_target
}, },
'^/livedata': { '^/livedata': {
target: 'ws://192.168.178.87/', target: 'ws://' + proxy_target,
ws: true, ws: true,
changeOrigin: true changeOrigin: true
}, },
'^/vedirectlivedata': { '^/vedirectlivedata': {
target: 'ws://192.168.178.87/', target: 'ws://' + proxy_target,
ws: true,
changeOrigin: true
},
'^/batterylivedata': {
target: 'ws://' + proxy_target,
ws: true, ws: true,
changeOrigin: true changeOrigin: true
}, },
'^/console': { '^/console': {
target: 'ws://192.168.178.87/', target: 'ws://' + proxy_target,
ws: true, ws: true,
changeOrigin: true changeOrigin: true
} }

View File

@ -151,10 +151,10 @@
resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.6.2.tgz#1816b5f6948029c5eaacb0703b850ee0cb37d8f8" resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.6.2.tgz#1816b5f6948029c5eaacb0703b850ee0cb37d8f8"
integrity sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw== integrity sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==
"@eslint/eslintrc@^2.1.1": "@eslint/eslintrc@^2.1.2":
version "2.1.1" version "2.1.2"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.1.tgz#18d635e24ad35f7276e8a49d135c7d3ca6a46f93" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.2.tgz#c6936b4b328c64496692f76944e755738be62396"
integrity sha512-9t7ZA7NGGK8ckelF0PQCfcxIUzs1Md5rrO6U/c+FIQNanea5UZC0wqKXH4vHBccmu4ZJgZ2idtPeW7+Q2npOEA== integrity sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==
dependencies: dependencies:
ajv "^6.12.4" ajv "^6.12.4"
debug "^4.3.2" debug "^4.3.2"
@ -166,10 +166,10 @@
minimatch "^3.1.2" minimatch "^3.1.2"
strip-json-comments "^3.1.1" strip-json-comments "^3.1.1"
"@eslint/js@^8.46.0": "@eslint/js@8.48.0":
version "8.46.0" version "8.48.0"
resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.46.0.tgz#3f7802972e8b6fe3f88ed1aabc74ec596c456db6" resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.48.0.tgz#642633964e217905436033a2bd08bf322849b7fb"
integrity sha512-a8TLtmPi8xzPkCbp/OGFUo5yhRkHM2Ko9kOWP4znJr0WAhWyThaw3PnwX4vOTWOAMsV2uRt32PPDcEz63esSaA== integrity sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw==
"@humanwhocodes/config-array@^0.11.10": "@humanwhocodes/config-array@^0.11.10":
version "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" resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-9.3.0-beta.24.tgz#23e08af9fc904fe3ef896786f9e659da6bb567b5"
integrity sha512-AKxJ8s7eKIQWkNaf4wyyoLRwf4puCuQgjSChlDJm5JBEt6T8HGgnYTJLRXu6LD/JACn3Qwu6hM/XRX1c9yvjmQ== integrity sha512-AKxJ8s7eKIQWkNaf4wyyoLRwf4puCuQgjSChlDJm5JBEt6T8HGgnYTJLRXu6LD/JACn3Qwu6hM/XRX1c9yvjmQ==
"@intlify/unplugin-vue-i18n@^0.12.2": "@intlify/unplugin-vue-i18n@^0.12.3":
version "0.12.2" version "0.12.3"
resolved "https://registry.yarnpkg.com/@intlify/unplugin-vue-i18n/-/unplugin-vue-i18n-0.12.2.tgz#64f7aad79cff1c4e8ff199cc059ea2bb9c36b2bb" resolved "https://registry.yarnpkg.com/@intlify/unplugin-vue-i18n/-/unplugin-vue-i18n-0.12.3.tgz#fae7d92d3e7bfe9e710fb332b28d22e0f8d999f2"
integrity sha512-IIgzLRSPUKZM1FBdUAZ9NwVPiLUr4ea5g/HLWe2lB7gNtPDz4FOfUNUllIT504hT+3pDoJmjaYJ6pyqT7F4Wuw== integrity sha512-0riPtSfTM58JmGNMmJho/aHD2z3K24BESYAmkLvKlo61/LbaPvnjYU1DbSbJEm6bSjE2oEjUj+di3QaYxXei/w==
dependencies: dependencies:
"@intlify/bundle-utils" "^7.0.2" "@intlify/bundle-utils" "^7.0.2"
"@intlify/shared" "9.3.0-beta.24" "@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" resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.3.3.tgz#16ab6c727d8c2020a5b6e4a176a243ecd88d8d69"
integrity sha512-0xd7qez0AQ+MbHatZTlI1gu5vkG8r7MYRUJAHPAHJBmGLs16zpkrpAVLvjQKQOqaXPDUBwOiJzNc00znHSCVBw== integrity sha512-0xd7qez0AQ+MbHatZTlI1gu5vkG8r7MYRUJAHPAHJBmGLs16zpkrpAVLvjQKQOqaXPDUBwOiJzNc00znHSCVBw==
"@tsconfig/node18@^18.2.0": "@tsconfig/node18@^18.2.1":
version "18.2.0" version "18.2.1"
resolved "https://registry.yarnpkg.com/@tsconfig/node18/-/node18-18.2.0.tgz#d6b5358b3fa85fe89b13b46cb1e996e4d79d6a07" resolved "https://registry.yarnpkg.com/@tsconfig/node18/-/node18-18.2.1.tgz#ebf5e6b8d94e9de072e712bc197d6441a325ed61"
integrity sha512-yhxwIlFVSVcMym3O31HoMnRXpoenmpIxcj4Yoes2DUpe+xCJnA7ECQP1Vw889V0jTt/2nzvpLQ/UuMYCd3JPIg== integrity sha512-RDDZFuofwkcKpl8Vpj5wFbY+H53xOtqK7ckEL1sXsbPwvKwDdjQf3LkHbtt9sxIHn9nWIEwkmCwBRZ6z5TKU2A==
"@types/bootstrap@^5.2.6": "@types/bootstrap@^5.2.6":
version "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" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
"@types/node@^20.4.8": "@types/node@^20.5.7":
version "20.4.8" version "20.5.7"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.8.tgz#b5dda19adaa473a9bf0ab5cbd8f30ec7d43f5c85" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.7.tgz#4b8ecac87fbefbc92f431d09c30e176fc0a7c377"
integrity sha512-0mHckf6D2DiIAzh8fM8f3HQCvMKDpK94YQ0DSVkfWTG9BZleYIWudw9cJxX8oCk9bM+vAkDyujDV6dmKHbvQpg== integrity sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==
"@types/semver@^7.3.12": "@types/semver@^7.3.12":
version "7.3.13" version "7.3.13"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91"
integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw== integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==
"@types/sortablejs@^1.15.1": "@types/sortablejs@^1.15.2":
version "1.15.1" version "1.15.2"
resolved "https://registry.yarnpkg.com/@types/sortablejs/-/sortablejs-1.15.1.tgz#123abafbe936f754fee5eb5b49009ce1f1075aa5" resolved "https://registry.yarnpkg.com/@types/sortablejs/-/sortablejs-1.15.2.tgz#d51e5ecac00a9782aa256c1401309ce1c4031ba2"
integrity sha512-g/JwBNToh6oCTAwNS8UGVmjO7NLDKsejVhvE4x1eWiPTC3uCuNsa/TD4ssvX3du+MLiM+SHPNDuijp8y76JzLQ== integrity sha512-mOIv/EnPMzAZAVbuh9uGjOZ1BBdimP9Y6IPGntsvQJtko5yapSDKB7GwB3AOlF5N3bkpk4sBwQRpS3aEkiUbaA==
"@types/spark-md5@^3.0.2": "@types/spark-md5@^3.0.2":
version "3.0.2" version "3.0.2"
@ -486,10 +486,10 @@
"@typescript-eslint/types" "5.59.1" "@typescript-eslint/types" "5.59.1"
eslint-visitor-keys "^3.3.0" eslint-visitor-keys "^3.3.0"
"@vitejs/plugin-vue@^4.2.3": "@vitejs/plugin-vue@^4.3.3":
version "4.2.3" version "4.3.3"
resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-4.2.3.tgz#ee0b6dfcc62fe65364e6395bf38fa2ba10bb44b6" resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-4.3.3.tgz#3b2337f64495f95cfea5b1497d2d3f4a0b3382b2"
integrity sha512-R6JDUfiZbJA9cMiguQ7jxALsgiprjBeHL5ikpXfJCH62pPHtI+JdJ5xWj6Ev73yXSlYl86+blXn1kZHQ7uElxw== integrity sha512-ssxyhIAZqB0TrpUg6R0cBpCuMk9jTIlO1GNSKKQD6S8VjnXi6JXKfUXjSsxey9IwQiaRGsO1WnW9Rkl1L6AJVw==
"@volar/language-core@1.10.0", "@volar/language-core@~1.10.0": "@volar/language-core@1.10.0", "@volar/language-core@~1.10.0":
version "1.10.0" version "1.10.0"
@ -1104,10 +1104,10 @@ escodegen@^2.0.0:
optionalDependencies: optionalDependencies:
source-map "~0.6.1" source-map "~0.6.1"
eslint-plugin-vue@^9.16.1: eslint-plugin-vue@^9.17.0:
version "9.16.1" version "9.17.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-9.16.1.tgz#3508d9279d797b40889db76da2fd26524e9144e6" resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-9.17.0.tgz#4501547373f246547083482838b4c8f4b28e5932"
integrity sha512-2FtnTqazA6aYONfDuOZTk0QzwhAwi7Z4+uJ7+GHeGxcKapjqWlDsRWDenvyG/utyOfAS5bVRmAG3cEWiYEz2bA== integrity sha512-r7Bp79pxQk9I5XDP0k2dpUC7Ots3OSWgvGZNu3BxmKK6Zg7NgVtcOB6OCna5Kb9oQwJPl5hq183WD0SY5tZtIQ==
dependencies: dependencies:
"@eslint-community/eslint-utils" "^4.4.0" "@eslint-community/eslint-utils" "^4.4.0"
natural-compare "^1.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" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994"
integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA== integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==
eslint-visitor-keys@^3.4.2: eslint-visitor-keys@^3.4.3:
version "3.4.2" version "3.4.3"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.2.tgz#8c2095440eca8c933bedcadf16fefa44dbe9ba5f" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800"
integrity sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw== integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
eslint@^8.46.0: eslint@^8.48.0:
version "8.46.0" version "8.48.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.46.0.tgz#a06a0ff6974e53e643acc42d1dcf2e7f797b3552" resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.48.0.tgz#bf9998ba520063907ba7bfe4c480dc8be03c2155"
integrity sha512-cIO74PvbW0qU8e0mIvk5IV3ToWdCq5FYG6gWPHHkx6gNdjlbAYvtfHmlCMXxjcoVaIdwy/IAt3+mDkZkfvb2Dg== integrity sha512-sb6DLeIuRXxeM1YljSe1KEx9/YYeZFQWcV8Rq9HfigmdDEugjLEVEa1ozDjL6YDjBpQHPJxJzze+alxi4T3OLg==
dependencies: dependencies:
"@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/eslint-utils" "^4.2.0"
"@eslint-community/regexpp" "^4.6.1" "@eslint-community/regexpp" "^4.6.1"
"@eslint/eslintrc" "^2.1.1" "@eslint/eslintrc" "^2.1.2"
"@eslint/js" "^8.46.0" "@eslint/js" "8.48.0"
"@humanwhocodes/config-array" "^0.11.10" "@humanwhocodes/config-array" "^0.11.10"
"@humanwhocodes/module-importer" "^1.0.1" "@humanwhocodes/module-importer" "^1.0.1"
"@nodelib/fs.walk" "^1.2.8" "@nodelib/fs.walk" "^1.2.8"
@ -1187,7 +1187,7 @@ eslint@^8.46.0:
doctrine "^3.0.0" doctrine "^3.0.0"
escape-string-regexp "^4.0.0" escape-string-regexp "^4.0.0"
eslint-scope "^7.2.2" eslint-scope "^7.2.2"
eslint-visitor-keys "^3.4.2" eslint-visitor-keys "^3.4.3"
espree "^9.6.1" espree "^9.6.1"
esquery "^1.4.2" esquery "^1.4.2"
esutils "^2.0.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" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
typescript@^5.1.6: typescript@^5.2.2:
version "5.1.6" version "5.2.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78"
integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==
ufo@^1.1.2: ufo@^1.1.2:
version "1.1.2" version "1.1.2"

Binary file not shown.