inital version of full solar passthrough

Webinterface change to set full solar passthrough values

Adding webapi and config changes to enable full solar passthrough over certain battery Soc

inital version of full solar passthrough in power limiter

Passthrough mode can be enabled via MQTT

translations

re-enable comment

remove unused variable
This commit is contained in:
MalteSchm 2023-04-23 11:26:02 +02:00 committed by helgeerbe
commit e7c8a89bd3
36 changed files with 694 additions and 170 deletions

View File

@ -18,5 +18,30 @@
"type": 0,
"clk_mode": 0
}
},
{
"name": "WT32-ETH01 with SSD1306",
"nrf24": {
"miso": 4,
"mosi": 2,
"clk": 32,
"irq": 33,
"en": 14,
"cs": 15
},
"eth": {
"enabled": true,
"phy_addr": 1,
"power": 16,
"mdc": 23,
"mdio": 18,
"type": 0,
"clk_mode": 0
},
"display": {
"type": 2,
"data": 5,
"clk": 17
}
}
]

View File

@ -47,6 +47,7 @@ struct CHANNEL_CONFIG_T {
struct INVERTER_CONFIG_T {
uint64_t Serial;
char Name[INV_MAX_NAME_STRLEN + 1];
uint8_t Order;
bool Poll_Enable;
bool Poll_Enable_Night;
bool Command_Enable;
@ -82,6 +83,7 @@ struct CONFIG_T {
char Ntp_TimezoneDescr[NTP_MAX_TIMEZONEDESCR_STRLEN + 1];
double Ntp_Longitude;
double Ntp_Latitude;
uint8_t Ntp_SunsetType;
bool Mqtt_Enabled;
uint Mqtt_Port;

View File

@ -2,6 +2,8 @@
#pragma once
#include <TimeoutHelper.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
class DatastoreClass {
public:
@ -10,52 +12,69 @@ public:
void loop();
// Sum of yield total of all enabled inverters, a inverter which is just disabled at night is also included
float totalAcYieldTotalEnabled = 0;
float getTotalAcYieldTotalEnabled();
// Sum of yield day of all enabled inverters, a inverter which is just disabled at night is also included
float totalAcYieldDayEnabled = 0;
float getTotalAcYieldDayEnabled();
// Sum of total AC power of all enabled inverters
float totalAcPowerEnabled = 0;
float getTotalAcPowerEnabled();
// Sum of total DC power of all enabled inverters
float totalDcPowerEnabled = 0;
float getTotalDcPowerEnabled();
// Sum of total DC power of all enabled inverters with maxStringPower set
float totalDcPowerIrradiation = 0;
float getTotalDcPowerIrradiation();
// Sum of total installed irradiation of all enabled inverters
float totalDcIrradiationInstalled = 0;
float getTotalDcIrradiationInstalled();
// Percentage (1-100) of total irradiation
float totalDcIrradiation = 0;
float getTotalDcIrradiation();
// Amount of relevant digits for yield total
unsigned int totalAcYieldTotalDigits = 0;
unsigned int getTotalAcYieldTotalDigits();
// Amount of relevant digits for yield total
unsigned int totalAcYieldDayDigits = 0;
unsigned int getTotalAcYieldDayDigits();
// Amount of relevant digits for AC power
unsigned int totalAcPowerDigits = 0;
unsigned int getTotalAcPowerDigits();
// Amount of relevant digits for DC power
unsigned int totalDcPowerDigits = 0;
unsigned int getTotalDcPowerDigits();
// True, if at least one inverter is reachable
bool isAtLeastOneReachable = false;
bool getIsAtLeastOneReachable();
// True if at least one inverter is producing
bool isAtLeastOneProducing = false;
bool getIsAtLeastOneProducing();
// True if all enabled inverters are producing
bool isAllEnabledProducing = false;
bool getIsAllEnabledProducing();
// True if all enabled inverters are reachable
bool isAllEnabledReachable = false;
bool getIsAllEnabledReachable();
private:
TimeoutHelper _updateTimeout;
SemaphoreHandle_t _xSemaphore;
float _totalAcYieldTotalEnabled = 0;
float _totalAcYieldDayEnabled = 0;
float _totalAcPowerEnabled = 0;
float _totalDcPowerEnabled = 0;
float _totalDcPowerIrradiation = 0;
float _totalDcIrradiationInstalled = 0;
float _totalDcIrradiation = 0;
unsigned int _totalAcYieldTotalDigits = 0;
unsigned int _totalAcYieldDayDigits = 0;
unsigned int _totalAcPowerDigits = 0;
unsigned int _totalDcPowerDigits = 0;
bool _isAtLeastOneReachable = false;
bool _isAtLeastOneProducing = false;
bool _isAllEnabledProducing = false;
bool _isAllEnabledReachable = false;
};
extern DatastoreClass Datastore;

View File

@ -12,6 +12,7 @@ public:
void loop();
bool isDayPeriod();
bool isSunsetAvailable();
bool sunsetTime(struct tm* info);
bool sunriseTime(struct tm* info);
@ -20,6 +21,7 @@ private:
SunSet _sun;
bool _isDayPeriod = true;
bool _isSunsetAvailable = true;
uint _sunriseMinutes = 0;
uint _sunsetMinutes = 0;

View File

@ -28,6 +28,7 @@ enum WebApiError {
InverterInvalidMaxChannel,
InverterChanged,
InverterDeleted,
InverterOrdered,
LimitBase = 5000,
LimitSerialZero,

View File

@ -13,6 +13,7 @@ private:
void onInverterAdd(AsyncWebServerRequest* request);
void onInverterEdit(AsyncWebServerRequest* request);
void onInverterDelete(AsyncWebServerRequest* request);
void onInverterOrder(AsyncWebServerRequest* request);
AsyncWebServer* _server;
};

View File

@ -25,6 +25,7 @@
#define NTP_TIMEZONEDESCR "Europe/Berlin"
#define NTP_LONGITUDE 10.4515f
#define NTP_LATITUDE 51.1657f
#define NTP_SUNSETTYPE 1
#define MQTT_ENABLED false
#define MQTT_HOST ""

View File

@ -121,17 +121,25 @@ void HoymilesRadio_CMT::loop()
if (!_rxBuffer.empty()) {
fragment_t f = _rxBuffer.back();
if (checkFragmentCrc(&f)) {
std::shared_ptr<InverterAbstract> inv = Hoymiles.getInverterByFragment(&f);
if (nullptr != inv) {
// Save packet in inverter rx buffer
Hoymiles.getMessageOutput()->printf("RX %.2f MHz --> ", getFrequencyFromChannel(f.channel));
dumpBuf(f.fragment, f.len, false);
Hoymiles.getMessageOutput()->printf("| %d dBm\r\n", f.rssi);
serial_u dtuId = convertSerialToRadioId(_dtuSerial);
inv->addRxFragment(f.fragment, f.len);
} else {
Hoymiles.getMessageOutput()->println("Inverter Not found!");
// The CMT RF module does not filter foreign packages by itself.
// Has to be done manually here.
if (memcmp(&f.fragment[5], &dtuId.b[1], 4) == 0) {
std::shared_ptr<InverterAbstract> inv = Hoymiles.getInverterByFragment(&f);
if (nullptr != inv) {
// Save packet in inverter rx buffer
Hoymiles.getMessageOutput()->printf("RX %.2f MHz --> ", getFrequencyFromChannel(f.channel));
dumpBuf(f.fragment, f.len, false);
Hoymiles.getMessageOutput()->printf("| %d dBm\r\n", f.rssi);
inv->addRxFragment(f.fragment, f.len);
} else {
Hoymiles.getMessageOutput()->println("Inverter Not found!");
}
}
} else {

View File

@ -68,6 +68,11 @@ bool MultiDataCommand::handleResponse(InverterAbstract* inverter, fragment_t fra
uint16_t crc = 0xffff, crcRcv = 0;
for (uint8_t i = 0; i < max_fragment_id; i++) {
// Doublecheck if correct answer package
if (fragment[i].mainCmd != (_payload[0] | 0x80)) {
return false;
}
if (i == max_fragment_id - 1) {
// Last packet
crc = crc16(fragment[i].fragment, fragment[i].len - 2, crc);

View File

@ -6,9 +6,10 @@
#include "../Hoymiles.h"
#include <cstring>
const std::array<const AlarmMessage_t, 76> AlarmLogParser::_alarmMessages = {{
const std::array<const AlarmMessage_t, ALARM_MSG_COUNT> AlarmLogParser::_alarmMessages = {{
{ AlarmMessageType_t::ALL, 1, "Inverter start" },
{ AlarmMessageType_t::ALL, 2, "DTU command failed" },
{ AlarmMessageType_t::ALL, 73, "Temperature >80°C" }, // https://github.com/tbnobody/OpenDTU/discussions/590#discussioncomment-6049750
{ AlarmMessageType_t::ALL, 121, "Over temperature protection" },
{ AlarmMessageType_t::ALL, 124, "Shut down by remote control" },
{ AlarmMessageType_t::ALL, 125, "Grid configuration parameter error" },

View File

@ -9,6 +9,8 @@
#define ALARM_LOG_ENTRY_SIZE 12
#define ALARM_LOG_PAYLOAD_SIZE (ALARM_LOG_ENTRY_COUNT * ALARM_LOG_ENTRY_SIZE + 4)
#define ALARM_MSG_COUNT 77
struct AlarmLogEntry_t {
uint16_t MessageId;
String Message;
@ -50,5 +52,5 @@ private:
AlarmMessageType_t _messageType = AlarmMessageType_t::ALL;
static const std::array<const AlarmMessage_t, 76> _alarmMessages;
static const std::array<const AlarmMessage_t, ALARM_MSG_COUNT> _alarmMessages;
};

View File

@ -32,7 +32,8 @@ const devInfo_t devInfo[] = {
{ { 0x10, 0x10, 0x51, ALL }, 450, "HMS-450" }, // 01
{ { 0x10, 0x10, 0x71, ALL }, 500, "HMS-500" }, // 02
{ { 0x10, 0x21, 0x11, ALL }, 600, "HMS-600" }, // 01
{ { 0x10, 0x21, 0x41, ALL }, 800, "HMS-800" }, // 00
{ { 0x10, 0x21, 0x41, ALL }, 800, "HMS-800" }, // 00
{ { 0x10, 0x11, 0x51, ALL }, 900, "HMS-900" }, // 01
{ { 0x10, 0x21, 0x71, ALL }, 1000, "HMS-1000" }, // 05
{ { 0x10, 0x22, 0x41, ALL }, 1600, "HMS-1600" }, // 4
{ { 0x10, 0x12, 0x51, ALL }, 1800, "HMS-1800" }, // 01

View File

@ -0,0 +1,77 @@
# Part of ESPEasy build toolchain.
#
# Combines separate bin files with their respective offsets into a single file
# This single file must then be flashed to an ESP32 node with 0 offset.
#
# Original implementation: Bartłomiej Zimoń (@uzi18)
# Maintainer: Gijs Noorlander (@TD-er)
#
# Special thanks to @Jason2866 (Tasmota) for helping debug flashing to >4MB flash
# Thanks @jesserockz (esphome) for adapting to use esptool.py with merge_bin
#
# Typical layout of the generated file:
# Offset | File
# - 0x1000 | ~\.platformio\packages\framework-arduinoespressif32\tools\sdk\esp32\bin\bootloader_dout_40m.bin
# - 0x8000 | ~\ESPEasy\.pio\build\<env name>\partitions.bin
# - 0xe000 | ~\.platformio\packages\framework-arduinoespressif32\tools\partitions\boot_app0.bin
# - 0x10000 | ~\ESPEasy\.pio\build\<env name>/<built binary>.bin
Import("env")
platform = env.PioPlatform()
import sys
from os.path import join
sys.path.append(join(platform.get_package_dir("tool-esptoolpy")))
import esptool
def esp32_create_combined_bin(source, target, env):
print("Generating combined binary for serial flashing")
# The offset from begin of the file where the app0 partition starts
# This is defined in the partition .csv file
app_offset = 0x10000
new_file_name = env.subst("$BUILD_DIR/${PROGNAME}.factory.bin")
sections = env.subst(env.get("FLASH_EXTRA_IMAGES"))
firmware_name = env.subst("$BUILD_DIR/${PROGNAME}.bin")
chip = env.get("BOARD_MCU")
flash_size = env.BoardConfig().get("upload.flash_size")
flash_freq = env.BoardConfig().get("build.f_flash", '40m')
flash_freq = flash_freq.replace('000000L', 'm')
flash_mode = env.BoardConfig().get("build.flash_mode", "dio")
memory_type = env.BoardConfig().get("build.arduino.memory_type", "qio_qspi")
if flash_mode == "qio" or flash_mode == "qout":
flash_mode = "dio"
if memory_type == "opi_opi" or memory_type == "opi_qspi":
flash_mode = "dout"
cmd = [
"--chip",
chip,
"merge_bin",
"-o",
new_file_name,
"--flash_mode",
flash_mode,
"--flash_freq",
flash_freq,
"--flash_size",
flash_size,
]
print(" Offset | File")
for section in sections:
sect_adr, sect_file = section.split(" ", 1)
print(f" - {sect_adr} | {sect_file}")
cmd += [sect_adr, sect_file]
print(f" - {hex(app_offset)} | {firmware_name}")
cmd += [hex(app_offset), firmware_name]
print('Using esptool.py arguments: %s' % ' '.join(cmd))
esptool.main(cmd)
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_create_combined_bin)

View File

@ -29,7 +29,7 @@ build_unflags =
lib_deps =
https://github.com/yubox-node-org/ESPAsyncWebServer
bblanchon/ArduinoJson @ ^6.21.2
https://github.com/bertmelis/espMqttClient.git#v1.4.2
https://github.com/bertmelis/espMqttClient.git#v1.4.3
nrf24/RF24 @ ^1.4.5
olikraus/U8g2 @ ^2.34.17
buelowp/sunset @ ^1.1.7
@ -40,6 +40,7 @@ lib_deps =
extra_scripts =
pre:pio-scripts/auto_firmware_version.py
pre:pio-scripts/patch_apply.py
post:pio-scripts/create_factory_bin.py
board_build.partitions = partitions_custom.csv
board_build.filesystem = littlefs

View File

@ -46,6 +46,7 @@ bool ConfigurationClass::write()
ntp["timezone_descr"] = config.Ntp_TimezoneDescr;
ntp["latitude"] = config.Ntp_Latitude;
ntp["longitude"] = config.Ntp_Longitude;
ntp["sunsettype"] = config.Ntp_SunsetType;
JsonObject mqtt = doc.createNestedObject("mqtt");
mqtt["enabled"] = config.Mqtt_Enabled;
@ -102,6 +103,7 @@ bool ConfigurationClass::write()
JsonObject inv = inverters.createNestedObject();
inv["serial"] = config.Inverter[i].Serial;
inv["name"] = config.Inverter[i].Name;
inv["order"] = config.Inverter[i].Order;
inv["poll_enable"] = config.Inverter[i].Poll_Enable;
inv["poll_enable_night"] = config.Inverter[i].Poll_Enable_Night;
inv["command_enable"] = config.Inverter[i].Command_Enable;
@ -241,6 +243,7 @@ bool ConfigurationClass::read()
strlcpy(config.Ntp_TimezoneDescr, ntp["timezone_descr"] | NTP_TIMEZONEDESCR, sizeof(config.Ntp_TimezoneDescr));
config.Ntp_Latitude = ntp["latitude"] | NTP_LATITUDE;
config.Ntp_Longitude = ntp["longitude"] | NTP_LONGITUDE;
config.Ntp_SunsetType = ntp["sunsettype"] | NTP_SUNSETTYPE;
JsonObject mqtt = doc["mqtt"];
config.Mqtt_Enabled = mqtt["enabled"] | MQTT_ENABLED;
@ -297,6 +300,7 @@ bool ConfigurationClass::read()
JsonObject inv = inverters[i].as<JsonObject>();
config.Inverter[i].Serial = inv["serial"] | 0ULL;
strlcpy(config.Inverter[i].Name, inv["name"] | "", sizeof(config.Inverter[i].Name));
config.Inverter[i].Order = inv["order"] | 0;
config.Inverter[i].Poll_Enable = inv["poll_enable"] | true;
config.Inverter[i].Poll_Enable_Night = inv["poll_enable_night"] | true;

View File

@ -6,10 +6,17 @@
#include "Configuration.h"
#include <Hoymiles.h>
#define DAT_SEMAPHORE_TAKE() \
do { \
} while (xSemaphoreTake(_xSemaphore, portMAX_DELAY) != pdPASS)
#define DAT_SEMAPHORE_GIVE() xSemaphoreGive(_xSemaphore)
DatastoreClass Datastore;
DatastoreClass::DatastoreClass()
{
_xSemaphore = xSemaphoreCreateMutex();
DAT_SEMAPHORE_GIVE(); // release before first use
}
void DatastoreClass::init()
@ -24,23 +31,25 @@ void DatastoreClass::loop()
uint8_t isProducing = 0;
uint8_t isReachable = 0;
totalAcYieldTotalEnabled = 0;
totalAcYieldTotalDigits = 0;
DAT_SEMAPHORE_TAKE();
totalAcYieldDayEnabled = 0;
totalAcYieldDayDigits = 0;
_totalAcYieldTotalEnabled = 0;
_totalAcYieldTotalDigits = 0;
totalAcPowerEnabled = 0;
totalAcPowerDigits = 0;
_totalAcYieldDayEnabled = 0;
_totalAcYieldDayDigits = 0;
totalDcPowerEnabled = 0;
totalDcPowerDigits = 0;
_totalAcPowerEnabled = 0;
_totalAcPowerDigits = 0;
totalDcPowerIrradiation = 0;
totalDcIrradiationInstalled = 0;
_totalDcPowerEnabled = 0;
_totalDcPowerDigits = 0;
isAllEnabledProducing = true;
isAllEnabledReachable = true;
_totalDcPowerIrradiation = 0;
_totalDcIrradiationInstalled = 0;
_isAllEnabledProducing = true;
_isAllEnabledReachable = true;
for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) {
auto inv = Hoymiles.getInverterByPos(i);
@ -57,7 +66,7 @@ void DatastoreClass::loop()
isProducing++;
} else {
if (inv->getEnablePolling()) {
isAllEnabledProducing = false;
_isAllEnabledProducing = false;
}
}
@ -65,42 +74,164 @@ void DatastoreClass::loop()
isReachable++;
} else {
if (inv->getEnablePolling()) {
isAllEnabledReachable = false;
_isAllEnabledReachable = false;
}
}
for (auto& c : inv->Statistics()->getChannelsByType(TYPE_AC)) {
if (cfg->Poll_Enable) {
totalAcYieldTotalEnabled += inv->Statistics()->getChannelFieldValue(TYPE_AC, c, FLD_YT);
totalAcYieldDayEnabled += inv->Statistics()->getChannelFieldValue(TYPE_AC, c, FLD_YD);
_totalAcYieldTotalEnabled += inv->Statistics()->getChannelFieldValue(TYPE_AC, c, FLD_YT);
_totalAcYieldDayEnabled += inv->Statistics()->getChannelFieldValue(TYPE_AC, c, FLD_YD);
totalAcYieldTotalDigits = max<unsigned int>(totalAcYieldTotalDigits, inv->Statistics()->getChannelFieldDigits(TYPE_AC, c, FLD_YT));
totalAcYieldDayDigits = max<unsigned int>(totalAcYieldDayDigits, inv->Statistics()->getChannelFieldDigits(TYPE_AC, c, FLD_YD));
_totalAcYieldTotalDigits = max<unsigned int>(_totalAcYieldTotalDigits, inv->Statistics()->getChannelFieldDigits(TYPE_AC, c, FLD_YT));
_totalAcYieldDayDigits = max<unsigned int>(_totalAcYieldDayDigits, inv->Statistics()->getChannelFieldDigits(TYPE_AC, c, FLD_YD));
}
if (inv->getEnablePolling()) {
totalAcPowerEnabled += inv->Statistics()->getChannelFieldValue(TYPE_AC, c, FLD_PAC);
totalAcPowerDigits = max<unsigned int>(totalAcPowerDigits, inv->Statistics()->getChannelFieldDigits(TYPE_AC, c, FLD_PAC));
_totalAcPowerEnabled += inv->Statistics()->getChannelFieldValue(TYPE_AC, c, FLD_PAC);
_totalAcPowerDigits = max<unsigned int>(_totalAcPowerDigits, inv->Statistics()->getChannelFieldDigits(TYPE_AC, c, FLD_PAC));
}
}
for (auto& c : inv->Statistics()->getChannelsByType(TYPE_DC)) {
if (inv->getEnablePolling()) {
totalDcPowerEnabled += inv->Statistics()->getChannelFieldValue(TYPE_DC, c, FLD_PDC);
totalDcPowerDigits = max<unsigned int>(totalDcPowerDigits, inv->Statistics()->getChannelFieldDigits(TYPE_DC, c, FLD_PDC));
_totalDcPowerEnabled += inv->Statistics()->getChannelFieldValue(TYPE_DC, c, FLD_PDC);
_totalDcPowerDigits = max<unsigned int>(_totalDcPowerDigits, inv->Statistics()->getChannelFieldDigits(TYPE_DC, c, FLD_PDC));
if (inv->Statistics()->getStringMaxPower(c) > 0) {
totalDcPowerIrradiation += inv->Statistics()->getChannelFieldValue(TYPE_DC, c, FLD_PDC);
totalDcIrradiationInstalled += inv->Statistics()->getStringMaxPower(c);
_totalDcPowerIrradiation += inv->Statistics()->getChannelFieldValue(TYPE_DC, c, FLD_PDC);
_totalDcIrradiationInstalled += inv->Statistics()->getStringMaxPower(c);
}
}
}
}
isAtLeastOneProducing = isProducing > 0;
isAtLeastOneReachable = isReachable > 0;
_isAtLeastOneProducing = isProducing > 0;
_isAtLeastOneReachable = isReachable > 0;
totalDcIrradiation = totalDcIrradiationInstalled > 0 ? totalDcPowerIrradiation / totalDcIrradiationInstalled * 100.0f : 0;
_totalDcIrradiation = _totalDcIrradiationInstalled > 0 ? _totalDcPowerIrradiation / _totalDcIrradiationInstalled * 100.0f : 0;
DAT_SEMAPHORE_GIVE();
_updateTimeout.reset();
}
}
}
float DatastoreClass::getTotalAcYieldTotalEnabled()
{
DAT_SEMAPHORE_TAKE();
float retval = _totalAcYieldTotalEnabled;
DAT_SEMAPHORE_GIVE();
return retval;
}
float DatastoreClass::getTotalAcYieldDayEnabled()
{
DAT_SEMAPHORE_TAKE();
float retval = _totalAcYieldDayEnabled;
DAT_SEMAPHORE_GIVE();
return retval;
}
float DatastoreClass::getTotalAcPowerEnabled()
{
DAT_SEMAPHORE_TAKE();
float retval = _totalAcPowerEnabled;
DAT_SEMAPHORE_GIVE();
return retval;
}
float DatastoreClass::getTotalDcPowerEnabled()
{
DAT_SEMAPHORE_TAKE();
float retval = _totalDcPowerEnabled;
DAT_SEMAPHORE_GIVE();
return retval;
}
float DatastoreClass::getTotalDcPowerIrradiation()
{
DAT_SEMAPHORE_TAKE();
float retval = _totalDcPowerIrradiation;
DAT_SEMAPHORE_GIVE();
return retval;
}
float DatastoreClass::getTotalDcIrradiationInstalled()
{
DAT_SEMAPHORE_TAKE();
float retval = _totalDcIrradiationInstalled;
DAT_SEMAPHORE_GIVE();
return retval;
}
float DatastoreClass::getTotalDcIrradiation()
{
DAT_SEMAPHORE_TAKE();
float retval = _totalDcIrradiation;
DAT_SEMAPHORE_GIVE();
return retval;
}
unsigned int DatastoreClass::getTotalAcYieldTotalDigits()
{
DAT_SEMAPHORE_TAKE();
unsigned int retval = _totalAcYieldTotalDigits;
DAT_SEMAPHORE_GIVE();
return retval;
}
unsigned int DatastoreClass::getTotalAcYieldDayDigits()
{
DAT_SEMAPHORE_TAKE();
unsigned int retval = _totalAcYieldDayDigits;
DAT_SEMAPHORE_GIVE();
return retval;
}
unsigned int DatastoreClass::getTotalAcPowerDigits()
{
DAT_SEMAPHORE_TAKE();
unsigned int retval = _totalAcPowerDigits;
DAT_SEMAPHORE_GIVE();
return retval;
}
unsigned int DatastoreClass::getTotalDcPowerDigits()
{
DAT_SEMAPHORE_TAKE();
unsigned int retval = _totalDcPowerDigits;
DAT_SEMAPHORE_GIVE();
return retval;
}
bool DatastoreClass::getIsAtLeastOneReachable()
{
DAT_SEMAPHORE_TAKE();
bool retval = _isAtLeastOneReachable;
DAT_SEMAPHORE_GIVE();
return retval;
}
bool DatastoreClass::getIsAtLeastOneProducing()
{
DAT_SEMAPHORE_TAKE();
bool retval = _isAtLeastOneProducing;
DAT_SEMAPHORE_GIVE();
return retval;
}
bool DatastoreClass::getIsAllEnabledProducing()
{
DAT_SEMAPHORE_TAKE();
bool retval = _isAllEnabledProducing;
DAT_SEMAPHORE_GIVE();
return retval;
}
bool DatastoreClass::getIsAllEnabledReachable()
{
DAT_SEMAPHORE_TAKE();
bool retval = _isAllEnabledReachable;
DAT_SEMAPHORE_GIVE();
return retval;
}

View File

@ -141,12 +141,12 @@ void DisplayGraphicClass::loop()
_display->clearBuffer();
//=====> Actual Production ==========
if (Datastore.isAtLeastOneReachable) {
if (Datastore.getIsAtLeastOneReachable()) {
_display->setPowerSave(false);
if (Datastore.totalAcPowerEnabled > 999) {
snprintf(_fmtText, sizeof(_fmtText), i18n_current_power_kw[_display_language], (Datastore.totalAcPowerEnabled / 1000));
if (Datastore.getTotalAcPowerEnabled() > 999) {
snprintf(_fmtText, sizeof(_fmtText), i18n_current_power_kw[_display_language], (Datastore.getTotalAcPowerEnabled() / 1000));
} else {
snprintf(_fmtText, sizeof(_fmtText), i18n_current_power_w[_display_language], Datastore.totalAcPowerEnabled);
snprintf(_fmtText, sizeof(_fmtText), i18n_current_power_w[_display_language], Datastore.getTotalAcPowerEnabled());
}
printText(_fmtText, 0);
_previousMillis = millis();
@ -164,10 +164,10 @@ void DisplayGraphicClass::loop()
//<=======================
//=====> Today & Total Production =======
snprintf(_fmtText, sizeof(_fmtText), i18n_yield_today_wh[_display_language], Datastore.totalAcYieldDayEnabled);
snprintf(_fmtText, sizeof(_fmtText), i18n_yield_today_wh[_display_language], Datastore.getTotalAcYieldDayEnabled());
printText(_fmtText, 1);
snprintf(_fmtText, sizeof(_fmtText), i18n_yield_total_kwh[_display_language], Datastore.totalAcYieldTotalEnabled);
snprintf(_fmtText, sizeof(_fmtText), i18n_yield_total_kwh[_display_language], Datastore.getTotalAcYieldTotalEnabled());
printText(_fmtText, 2);
//<=======================

View File

@ -59,10 +59,10 @@ void LedSingleClass::loop()
_ledState[1] = LedState_t::Off;
if (Hoymiles.getNumInverters()) {
// set LED status
if (Datastore.isAllEnabledReachable && Datastore.isAllEnabledProducing) {
if (Datastore.getIsAllEnabledReachable() && Datastore.getIsAllEnabledProducing()) {
_ledState[1] = LedState_t::On;
}
if (Datastore.isAllEnabledReachable && !Datastore.isAllEnabledProducing) {
if (Datastore.getIsAllEnabledReachable() && !Datastore.getIsAllEnabledProducing()) {
_ledState[1] = LedState_t::Blink;
}
}

View File

@ -22,13 +22,13 @@ void MqttHandleInverterTotalClass::loop()
}
if (_lastPublish.occured()) {
MqttSettings.publish("ac/power", String(Datastore.totalAcPowerEnabled, Datastore.totalAcPowerDigits));
MqttSettings.publish("ac/yieldtotal", String(Datastore.totalAcYieldTotalEnabled, Datastore.totalAcYieldTotalDigits));
MqttSettings.publish("ac/yieldday", String(Datastore.totalAcYieldDayEnabled, Datastore.totalAcYieldDayDigits));
MqttSettings.publish("ac/is_valid", String(Datastore.isAllEnabledReachable));
MqttSettings.publish("dc/power", String(Datastore.totalDcPowerEnabled, Datastore.totalDcPowerDigits));
MqttSettings.publish("dc/irradiation", String(Datastore.totalDcIrradiation, 3));
MqttSettings.publish("dc/is_valid", String(Datastore.isAllEnabledReachable));
MqttSettings.publish("ac/power", String(Datastore.getTotalAcPowerEnabled(), Datastore.getTotalAcPowerDigits()));
MqttSettings.publish("ac/yieldtotal", String(Datastore.getTotalAcYieldTotalEnabled(), Datastore.getTotalAcYieldTotalDigits()));
MqttSettings.publish("ac/yieldday", String(Datastore.getTotalAcYieldDayEnabled(), Datastore.getTotalAcYieldDayDigits()));
MqttSettings.publish("ac/is_valid", String(Datastore.getIsAllEnabledReachable()));
MqttSettings.publish("dc/power", String(Datastore.getTotalDcPowerEnabled(), Datastore.getTotalDcPowerDigits()));
MqttSettings.publish("dc/irradiation", String(Datastore.getTotalDcIrradiation(), 3));
MqttSettings.publish("dc/is_valid", String(Datastore.getIsAllEnabledReachable()));
_lastPublish.set(Configuration.get().Mqtt_PublishInterval * 1000);
}

View File

@ -29,6 +29,11 @@ bool SunPositionClass::isDayPeriod()
return _isDayPeriod;
}
bool SunPositionClass::isSunsetAvailable()
{
return _isSunsetAvailable;
}
void SunPositionClass::updateSunData()
{
CONFIG_T const& config = Configuration.get();
@ -37,7 +42,7 @@ void SunPositionClass::updateSunData()
struct tm timeinfo;
if (!getLocalTime(&timeinfo, 5)) {
_isDayPeriod = false;
_isDayPeriod = true;
_sunriseMinutes = 0;
_sunsetMinutes = 0;
_isValidInfo = false;
@ -45,11 +50,43 @@ void SunPositionClass::updateSunData()
}
_sun.setCurrentDate(1900 + timeinfo.tm_year, timeinfo.tm_mon + 1, timeinfo.tm_mday);
_sunriseMinutes = static_cast<int>(_sun.calcCustomSunrise(SunSet::SUNSET_NAUTICAL));
_sunsetMinutes = static_cast<int>(_sun.calcCustomSunset(SunSet::SUNSET_NAUTICAL));
double sunset_type;
switch (config.Ntp_SunsetType) {
case 0:
sunset_type = SunSet::SUNSET_OFFICIAL;
break;
case 2:
sunset_type = SunSet::SUNSET_CIVIL;
break;
case 3:
sunset_type = SunSet::SUNSET_ASTONOMICAL;
break;
default:
sunset_type = SunSet::SUNSET_NAUTICAL;
break;
}
double sunriseRaw = _sun.calcCustomSunrise(sunset_type);
double sunsetRaw = _sun.calcCustomSunset(sunset_type);
// If no sunset/sunrise exists (e.g. astronomical calculation in summer)
// assume it's day period
if (std::isnan(sunriseRaw) || std::isnan(sunsetRaw)) {
_isDayPeriod = true;
_isSunsetAvailable = false;
_sunriseMinutes = 0;
_sunsetMinutes = 0;
_isValidInfo = false;
return;
}
_sunriseMinutes = static_cast<int>(sunriseRaw);
_sunsetMinutes = static_cast<int>(sunsetRaw);
uint minutesPastMidnight = timeinfo.tm_hour * 60 + timeinfo.tm_min;
_isDayPeriod = (minutesPastMidnight >= _sunriseMinutes) && (minutesPastMidnight < _sunsetMinutes);
_isSunsetAvailable = true;
_isValidInfo = true;
}

View File

@ -21,6 +21,7 @@ void WebApiInverterClass::init(AsyncWebServer* server)
_server->on("/api/inverter/add", HTTP_POST, std::bind(&WebApiInverterClass::onInverterAdd, this, _1));
_server->on("/api/inverter/edit", HTTP_POST, std::bind(&WebApiInverterClass::onInverterEdit, this, _1));
_server->on("/api/inverter/del", HTTP_POST, std::bind(&WebApiInverterClass::onInverterDelete, this, _1));
_server->on("/api/inverter/order", HTTP_POST, std::bind(&WebApiInverterClass::onInverterOrder, this, _1));
}
void WebApiInverterClass::loop()
@ -44,6 +45,7 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request)
JsonObject obj = data.createNestedObject();
obj["id"] = i;
obj["name"] = String(config.Inverter[i].Name);
obj["order"] = config.Inverter[i].Order;
// Inverter Serial is read as HEX
char buffer[sizeof(uint64_t) * 8 + 1];
@ -389,4 +391,73 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request)
request->send(response);
MqttHandleHass.forceUpdate();
}
void WebApiInverterClass::onInverterOrder(AsyncWebServerRequest* request)
{
if (!WebApi.checkCredentials(request)) {
return;
}
AsyncJsonResponse* response = new AsyncJsonResponse();
JsonObject retMsg = response->getRoot();
retMsg["type"] = "warning";
if (!request->hasParam("data", true)) {
retMsg["message"] = "No values found!";
retMsg["code"] = WebApiError::GenericNoValueFound;
response->setLength();
request->send(response);
return;
}
String json = request->getParam("data", true)->value();
if (json.length() > 1024) {
retMsg["message"] = "Data too large!";
retMsg["code"] = WebApiError::GenericDataTooLarge;
response->setLength();
request->send(response);
return;
}
DynamicJsonDocument root(1024);
DeserializationError error = deserializeJson(root, json);
if (error) {
retMsg["message"] = "Failed to parse data!";
retMsg["code"] = WebApiError::GenericParseError;
response->setLength();
request->send(response);
return;
}
if (!(root.containsKey("order"))) {
retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing;
response->setLength();
request->send(response);
return;
}
// The order array contains list or id in the right order
JsonArray orderArray = root["order"].as<JsonArray>();
uint8_t order = 0;
for(JsonVariant id : orderArray) {
uint8_t inverter_id = id.as<uint8_t>();
if (inverter_id < INV_MAX_COUNT) {
INVERTER_CONFIG_T& inverter = Configuration.get().Inverter[inverter_id];
inverter.Order = order;
}
order++;
}
Configuration.write();
retMsg["type"] = "success";
retMsg["message"] = "Inverter order saved!";
retMsg["code"] = WebApiError::InverterOrdered;
response->setLength();
request->send(response);
}

View File

@ -52,14 +52,21 @@ void WebApiNtpClass::onNtpStatus(AsyncWebServerRequest* request)
strftime(timeStringBuff, sizeof(timeStringBuff), "%A, %B %d %Y %H:%M:%S", &timeinfo);
root["ntp_localtime"] = timeStringBuff;
SunPosition.sunriseTime(&timeinfo);
strftime(timeStringBuff, sizeof(timeStringBuff), "%A, %B %d %Y %H:%M:%S", &timeinfo);
if (SunPosition.sunriseTime(&timeinfo)) {
strftime(timeStringBuff, sizeof(timeStringBuff), "%A, %B %d %Y %H:%M:%S", &timeinfo);
} else {
strcpy(timeStringBuff, "--");
}
root["sun_risetime"] = timeStringBuff;
SunPosition.sunsetTime(&timeinfo);
strftime(timeStringBuff, sizeof(timeStringBuff), "%A, %B %d %Y %H:%M:%S", &timeinfo);
if (SunPosition.sunsetTime(&timeinfo)) {
strftime(timeStringBuff, sizeof(timeStringBuff), "%A, %B %d %Y %H:%M:%S", &timeinfo);
} else {
strcpy(timeStringBuff, "--");
}
root["sun_settime"] = timeStringBuff;
root["sun_isSunsetAvailable"] = SunPosition.isSunsetAvailable();
root["sun_isDayPeriod"] = SunPosition.isDayPeriod();
response->setLength();
@ -81,6 +88,7 @@ void WebApiNtpClass::onNtpAdminGet(AsyncWebServerRequest* request)
root["ntp_timezone_descr"] = config.Ntp_TimezoneDescr;
root["longitude"] = config.Ntp_Longitude;
root["latitude"] = config.Ntp_Latitude;
root["sunsettype"] = config.Ntp_SunsetType;
response->setLength();
request->send(response);
@ -125,7 +133,7 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request)
return;
}
if (!(root.containsKey("ntp_server") && root.containsKey("ntp_timezone") && root.containsKey("longitude") && root.containsKey("latitude"))) {
if (!(root.containsKey("ntp_server") && root.containsKey("ntp_timezone") && root.containsKey("longitude") && root.containsKey("latitude") && root.containsKey("sunsettype"))) {
retMsg["message"] = "Values are missing!";
retMsg["code"] = WebApiError::GenericValueMissing;
response->setLength();
@ -166,6 +174,7 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request)
strlcpy(config.Ntp_TimezoneDescr, root["ntp_timezone_descr"].as<String>().c_str(), sizeof(config.Ntp_TimezoneDescr));
config.Ntp_Latitude = root["latitude"].as<double>();
config.Ntp_Longitude = root["longitude"].as<double>();
config.Ntp_SunsetType = root["sunsettype"].as<uint8_t>();
Configuration.write();
retMsg["type"] = "success";

View File

@ -105,9 +105,14 @@ void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root)
}
JsonObject invObject = invArray.createNestedObject();
INVERTER_CONFIG_T* inv_cfg = Configuration.getInverterConfig(inv->serial());
if (inv_cfg == nullptr) {
continue;
}
invObject["serial"] = inv->serialString();
invObject["name"] = inv->name();
invObject["order"] = inv_cfg->Order;
invObject["data_age"] = (millis() - inv->Statistics()->getLastUpdate()) / 1000;
invObject["poll_enabled"] = inv->getEnablePolling();
invObject["reachable"] = inv->isReachable();
@ -124,10 +129,7 @@ void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root)
JsonObject chanTypeObj = invObject.createNestedObject(inv->Statistics()->getChannelTypeName(t));
for (auto& c : inv->Statistics()->getChannelsByType(t)) {
if (t == TYPE_DC) {
INVERTER_CONFIG_T* inv_cfg = Configuration.getInverterConfig(inv->serial());
if (inv_cfg != nullptr) {
chanTypeObj[String(static_cast<uint8_t>(c))]["name"]["u"] = inv_cfg->channel[c].Name;
}
chanTypeObj[String(static_cast<uint8_t>(c))]["name"]["u"] = inv_cfg->channel[c].Name;
}
addField(chanTypeObj, i, inv, t, c, FLD_PAC);
addField(chanTypeObj, i, inv, t, c, FLD_UAC);
@ -164,9 +166,9 @@ void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root)
}
JsonObject totalObj = root.createNestedObject("total");
addTotalField(totalObj, "Power", Datastore.totalAcPowerEnabled, "W", Datastore.totalAcPowerDigits);
addTotalField(totalObj, "YieldDay", Datastore.totalAcYieldDayEnabled, "Wh", Datastore.totalAcYieldDayDigits);
addTotalField(totalObj, "YieldTotal", Datastore.totalAcYieldTotalEnabled, "kWh", Datastore.totalAcYieldTotalDigits);
addTotalField(totalObj, "Power", Datastore.getTotalAcPowerEnabled(), "W", Datastore.getTotalAcPowerDigits());
addTotalField(totalObj, "YieldDay", Datastore.getTotalAcYieldDayEnabled(), "Wh", Datastore.getTotalAcYieldDayDigits());
addTotalField(totalObj, "YieldTotal", Datastore.getTotalAcYieldTotalEnabled(), "kWh", Datastore.getTotalAcYieldTotalDigits());
JsonObject hintObj = root.createNestedObject("hints");
struct tm timeinfo;

View File

@ -11,32 +11,34 @@
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
},
"dependencies": {
"@popperjs/core": "^2.11.7",
"bootstrap": "^5.3.0-alpha3",
"@popperjs/core": "^2.11.8",
"bootstrap": "^5.3.0",
"bootstrap-icons-vue": "^1.10.3",
"mitt": "^3.0.0",
"sortablejs": "^1.15.0",
"spark-md5": "^3.0.2",
"vue": "^3.3.4",
"vue-i18n": "^9.2.2",
"vue-router": "^4.2.1"
"vue-router": "^4.2.2"
},
"devDependencies": {
"@intlify/unplugin-vue-i18n": "^0.10.0",
"@intlify/unplugin-vue-i18n": "^0.11.0",
"@rushstack/eslint-patch": "^1.3.0",
"@tsconfig/node18": "^2.0.1",
"@types/bootstrap": "^5.2.6",
"@types/node": "^20.2.3",
"@types/node": "^20.2.5",
"@types/sortablejs": "^1.15.1",
"@types/spark-md5": "^3.0.2",
"@vitejs/plugin-vue": "^4.2.3",
"@vue/eslint-config-typescript": "^11.0.3",
"@vue/tsconfig": "^0.4.0",
"eslint": "^8.41.0",
"eslint-plugin-vue": "^9.14.0",
"eslint-plugin-vue": "^9.14.1",
"npm-run-all": "^4.1.5",
"sass": "^1.62.1",
"terser": "^5.17.6",
"typescript": "^5.0.4",
"vite": "^4.3.8",
"terser": "^5.17.7",
"typescript": "^5.1.3",
"vite": "^4.3.9",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-css-injected-by-js": "^3.1.1",
"vue-tsc": "^1.6.5"

View File

@ -56,6 +56,7 @@
"4006": "Ungültige Anzahl an Kanalwerten übergeben!",
"4007": "Wechselrichter geändert!",
"4008": "Wechselrichter gelöscht!",
"4009": "Wechselrichter Reihenfolge gespeichert!",
"5001": "@:apiresponse.2001",
"5002": "Das Limit muss zwischen 1 und {max} sein!",
"5003": "Ungültiten Typ angegeben!",
@ -276,8 +277,9 @@
"Synced": "synchronisiert",
"NotSynced": "nicht synchronisiert",
"LocalTime": "Lokale Uhrzeit",
"Sunrise": "Nautische Morgendämmerung",
"Sunset": "Nautische Abenddämmerung",
"Sunrise": "Morgendämmerung",
"Sunset": "Abenddämmerung",
"NotAvailable": "Nicht verfügbar",
"Mode": "Modus",
"Day": "Tag",
"Night": "Nacht"
@ -408,6 +410,12 @@
"LocationConfiguration": "Standortkonfiguration",
"Longitude": "Längengrad:",
"Latitude": "Breitengrad:",
"SunSetType": "Dämmerungstyp:",
"SunSetTypeHint": "Beeinflusst die Tag/Nacht Berechnung. Es kann bis zu einer Minute dauern bis der neue Typ angewendet wurde.",
"OFFICIAL": "Standard Dämmerung (90.8°)",
"NAUTICAL": "Nautische Dämmerung (102°)",
"CIVIL": "Bürgerliche Dämmerung (96°)",
"ASTONOMICAL": "Astronomische Dämmerung (108°)",
"Save": "@:dtuadmin.Save",
"ManualTimeSynchronization": "Manuelle Zeitsynchronization",
"CurrentOpenDtuTime": "Aktuelle OpenDTU-Zeit:",
@ -564,6 +572,7 @@
"StatusHint": "<b>Hinweis:</b> Der Wechselrichter wird über seinen DC-Eingang mit Strom versorgt. Wenn keine Sonne scheint, ist der Wechselrichter aus. Es können trotzdem Anfragen gesendet werden.",
"Type": "Typ",
"Action": "Aktion",
"SaveOrder": "Reihenfolge speichern",
"DeleteInverter": "Wechselrichter löschen",
"EditInverter": "Wechselrichter bearbeiten",
"InverterSerial": "Wechselrichter Seriennummer:",

View File

@ -56,6 +56,7 @@
"4006": "Invalid amount of max channel setting given!",
"4007": "Inverter changed!",
"4008": "Inverter deleted!",
"4009": "Inverter order saved!",
"5001": "@:apiresponse.2001",
"5002": "Limit must between 1 and {max}!",
"5003": "Invalid type specified!",
@ -276,8 +277,9 @@
"Synced": "synced",
"NotSynced": "not synced",
"LocalTime": "Local Time",
"Sunrise": "Nautical Sunrise",
"Sunset": "Nautical Sunset",
"Sunrise": "Sunrise",
"Sunset": "Sunset",
"NotAvailable": "Not Available",
"Mode": "Mode",
"Day": "Day",
"Night": "Night"
@ -408,6 +410,12 @@
"LocationConfiguration": "Location Configuration",
"Longitude": "Longitude",
"Latitude": "Latitude",
"SunSetType": "Sunset type",
"SunSetTypeHint": "Affects the day/night calculation. It can take up to one minute until the new type will be applied.",
"OFFICIAL": "Standard dawn (90.8°)",
"NAUTICAL": "Nautical dawn (102°)",
"CIVIL": "Civil dawn (96°)",
"ASTONOMICAL": "Astronomical dawn (108°)",
"Save": "@:dtuadmin.Save",
"ManualTimeSynchronization": "Manual Time Synchronization",
"CurrentOpenDtuTime": "Current OpenDTU Time:",
@ -568,6 +576,7 @@
"StatusHint": "<b>Hint:</b> The inverter is powered by its DC input. If there is no sun, the inverter is off. Requests can still be sent.",
"Type": "Type",
"Action": "Action",
"SaveOrder": "Save order",
"DeleteInverter": "Delete inverter",
"EditInverter": "Edit inverter",
"InverterSerial": "Inverter Serial:",

View File

@ -56,6 +56,7 @@
"4006": "Réglage du montant maximal de canaux invalide !",
"4007": "Onduleur modifié !",
"4008": "Onduleur supprimé !",
"4009": "Inverter order saved!",
"5001": "@:apiresponse.2001",
"5002": "La limite doit être comprise entre 1 et {max} !",
"5003": "Type spécifié invalide !",
@ -275,8 +276,9 @@
"Synced": "synchronisée",
"NotSynced": "pas synchronisée",
"LocalTime": "Heure locale",
"Sunrise": "Nautical Sunrise",
"Sunset": "Nautical Sunset",
"Sunrise": "Sunrise",
"Sunset": "Sunset",
"NotAvailable": "Not Available",
"Mode": "Mode",
"Day": "Day",
"Night": "Night"
@ -407,6 +409,12 @@
"LocationConfiguration": "Géolocalisation",
"Longitude": "Longitude",
"Latitude": "Latitude",
"SunSetType": "Sunset type",
"SunSetTypeHint": "Affects the day/night calculation. It can take up to one minute until the new type will be applied.",
"OFFICIAL": "Standard dawn (90.8°)",
"NAUTICAL": "Nautical dawn (102°)",
"CIVIL": "Civil dawn (96°)",
"ASTONOMICAL": "Astronomical dawn (108°)",
"Save": "@:dtuadmin.Save",
"ManualTimeSynchronization": "Synchronisation manuelle de l'heure",
"CurrentOpenDtuTime": "Heure actuelle de l'OpenDTU",
@ -491,6 +499,7 @@
"StatusHint": "<b>Astuce :</b> L'onduleur est alimenté par son entrée courant continu. S'il n'y a pas de soleil, l'onduleur est éteint, mais les requêtes peuvent toujours être envoyées.",
"Type": "Type",
"Action": "Action",
"SaveOrder": "Save order",
"DeleteInverter": "Supprimer l'onduleur",
"EditInverter": "Modifier l'onduleur",
"InverterSerial": "Numéro de série de l'onduleur",

View File

@ -23,6 +23,7 @@ export interface InverterStatistics {
export interface Inverter {
serial: number;
name: string;
order: number;
data_age: number;
poll_enabled: boolean;
reachable: boolean;

View File

@ -4,4 +4,5 @@ export interface NtpConfig {
ntp_timezone_descr: string;
latitude: number;
longitude: number;
sunsettype: number;
}

View File

@ -7,4 +7,5 @@ export interface NtpStatus {
sun_risetime: string;
sun_settime: string;
sun_isDayPeriod: boolean;
sun_isSunsetAvailable: boolean;
}

View File

@ -470,7 +470,9 @@ export default defineComponent({
'decimalTwoDigits');
},
inverterData(): Inverter[] {
return this.liveData.inverters;
return this.liveData.inverters.slice().sort((a : Inverter, b: Inverter) => {
return a.order - b.order;
});
}
},
methods: {

View File

@ -28,6 +28,7 @@
<table class="table">
<thead>
<tr>
<th>#</th>
<th scope="col">{{ $t('inverteradmin.Status') }}</th>
<th>{{ $t('inverteradmin.Serial') }}</th>
<th>{{ $t('inverteradmin.Name') }}</th>
@ -35,8 +36,9 @@
<th>{{ $t('inverteradmin.Action') }}</th>
</tr>
</thead>
<tbody>
<tr v-for="inverter in sortedInverters" v-bind:key="inverter.id">
<tbody ref="invList">
<tr v-for="inverter in inverters" v-bind:key="inverter.id" :data-id="inverter.id">
<td><BIconGripHorizontal class="drag-handle" /></td>
<td>
<span class="badge" :title="$t('inverteradmin.Receive')" :class="{
'text-bg-warning': !inverter.poll_enable_night,
@ -63,6 +65,9 @@
</tbody>
</table>
</div>
<div class="ml-auto text-right">
<button class="btn btn-primary my-2" @click="onSaveOrder()">{{ $t('inverteradmin.SaveOrder') }}</button>
</div>
</CardElement>
</BasePage>
@ -197,6 +202,7 @@ import BasePage from '@/components/BasePage.vue';
import BootstrapAlert from "@/components/BootstrapAlert.vue";
import CardElement from '@/components/CardElement.vue';
import InputElement from '@/components/InputElement.vue';
import Sortable from 'sortablejs';
import { authHeader, handleResponse } from '@/utils/authentication';
import * as bootstrap from 'bootstrap';
import {
@ -205,6 +211,7 @@ import {
BIconTrash,
BIconArrowDown,
BIconArrowUp,
BIconGripHorizontal,
} from 'bootstrap-icons-vue';
import { defineComponent } from 'vue';
@ -219,6 +226,7 @@ declare interface Inverter {
serial: number;
name: string;
type: string;
order: number;
poll_enable: boolean;
poll_enable_night: boolean;
command_enable: boolean;
@ -244,6 +252,7 @@ export default defineComponent({
BIconTrash,
BIconArrowDown,
BIconArrowUp,
BIconGripHorizontal,
},
data() {
return {
@ -253,7 +262,8 @@ export default defineComponent({
selectedInverterData: {} as Inverter,
inverters: [] as Inverter[],
dataLoading: true,
alert: {} as AlertResponse
alert: {} as AlertResponse,
sortable: {} as Sortable,
};
},
mounted() {
@ -263,21 +273,27 @@ export default defineComponent({
created() {
this.getInverters();
},
computed: {
sortedInverters(): Inverter[] {
return this.inverters.slice().sort((a, b) => {
return a.serial - b.serial;
});
},
},
methods: {
getInverters() {
this.dataLoading = true;
fetch("/api/inverter/list", { headers: authHeader() })
.then((response) => handleResponse(response, this.$emitter, this.$router))
.then((data) => {
this.inverters = data.inverter;
this.inverters = data.inverter.slice().sort((a : Inverter, b: Inverter) => {
return a.order - b.order;
});
this.dataLoading = false;
this.$nextTick(() => {
const table = this.$refs.invList as HTMLElement;
this.sortable = Sortable.create(table, {
sort: true,
handle: '.drag-handle',
animation: 150,
draggable: 'tr',
});
});
});
},
callInverterApiEndpoint(endpoint: string, jsonData: string) {
@ -316,7 +332,16 @@ export default defineComponent({
},
onCloseModal(modal: bootstrap.Modal) {
modal.hide();
}
},
onSaveOrder() {
this.callInverterApiEndpoint("order", JSON.stringify({ order: this.sortable.toArray() }));
},
},
});
</script>
</script>
<style>
.drag-handle {
cursor: grab;
}
</style>

View File

@ -36,6 +36,21 @@
<InputElement :label="$t('ntpadmin.Longitude')"
v-model="ntpConfigList.longitude"
type="number" min="-180" max="180" step="any"/>
<div class="row mb-3">
<label class="col-sm-2 col-form-label">
{{ $t('ntpadmin.SunSetType') }}
<BIconInfoCircle v-tooltip :title="$t('ntpadmin.SunSetTypeHint')" />
</label>
<div class="col-sm-10">
<select class="form-select" v-model="ntpConfigList.sunsettype">
<option v-for="sunsettype in sunsetTypeList" :key="sunsettype.key" :value="sunsettype.key">
{{ $t(`ntpadmin.` + sunsettype.value) }}
</option>
</select>
</div>
</div>
</CardElement>
<button type="submit" class="btn btn-primary mb-3">{{ $t('ntpadmin.Save') }}</button>
</form>
@ -67,6 +82,7 @@ import InputElement from '@/components/InputElement.vue';
import type { NtpConfig } from "@/types/NtpConfig";
import { authHeader, handleResponse } from '@/utils/authentication';
import { defineComponent } from 'vue';
import { BIconInfoCircle } from 'bootstrap-icons-vue';
export default defineComponent({
components: {
@ -74,6 +90,7 @@ export default defineComponent({
BootstrapAlert,
CardElement,
InputElement,
BIconInfoCircle,
},
data() {
return {
@ -88,6 +105,12 @@ export default defineComponent({
alertMessage: "",
alertType: "info",
showAlert: false,
sunsetTypeList: [
{ key: 0, value: 'OFFICIAL' },
{ key: 1, value: 'NAUTICAL' },
{ key: 2, value: 'CIVIL' },
{ key: 3, value: 'ASTONOMICAL' },
],
};
},
watch: {

View File

@ -38,11 +38,13 @@
<tr>
<th>{{ $t('ntpinfo.Sunrise') }}</th>
<td>{{ ntpDataList.sun_risetime }}</td>
<td v-if="ntpDataList.sun_isSunsetAvailable">{{ ntpDataList.sun_risetime }}</td>
<td v-else>{{ $t('ntpinfo.NotAvailable') }}</td>
</tr>
<tr>
<th>{{ $t('ntpinfo.Sunset') }}</th>
<td>{{ ntpDataList.sun_settime }}</td>
<td v-if="ntpDataList.sun_isSunsetAvailable">{{ ntpDataList.sun_settime }}</td>
<td v-else>{{ $t('ntpinfo.NotAvailable') }}</td>
</tr>
<tr>
<th>{{ $t('ntpinfo.Mode') }}</th>

View File

@ -180,10 +180,10 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
"@intlify/bundle-utils@^5.4.0":
version "5.4.0"
resolved "https://registry.yarnpkg.com/@intlify/bundle-utils/-/bundle-utils-5.4.0.tgz#12d1e2316a52cdf4818f5f183dc2726da35886c0"
integrity sha512-oJbibbP5djdQYTv0cQC4PYRHPpS5nF/KZ7MWM1/yhdsGzjvCekJHWk25MCQIIOrfQ+aw5tKi2t66KpYEUki/tw==
"@intlify/bundle-utils@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/@intlify/bundle-utils/-/bundle-utils-6.0.1.tgz#889ef90a07f54d3285082f1fff99be06db49c65f"
integrity sha512-BkeZNKZiC0B7K3OYMwiPLoAqsZmKH3SxTL75vYAkuQ//XWR8WO0NpfjXhTxgLTVFHxMcNb2agAopC0DP6fqDrg==
dependencies:
"@intlify/message-compiler" "9.3.0-beta.17"
"@intlify/shared" "9.3.0-beta.17"
@ -192,6 +192,7 @@
estree-walker "^2.0.2"
jsonc-eslint-parser "^1.0.1"
magic-string "^0.30.0"
mlly "^1.2.0"
source-map "0.6.1"
yaml-eslint-parser "^0.3.2"
@ -238,12 +239,12 @@
resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-9.3.0-beta.17.tgz#1180dcb0b30741555fad0b62e4621802e8272ee5"
integrity sha512-mscf7RQsUTOil35jTij4KGW1RC9SWQjYScwLxP53Ns6g24iEd5HN7ksbt9O6FvTmlQuX77u+MXpBdfJsGqizLQ==
"@intlify/unplugin-vue-i18n@^0.10.0":
version "0.10.0"
resolved "https://registry.yarnpkg.com/@intlify/unplugin-vue-i18n/-/unplugin-vue-i18n-0.10.0.tgz#28a05a7b9e0a7cc35e91e6762e5e6e57f954a45c"
integrity sha512-Sf8fe26/d8rBNcg+zBSb7RA1uyhrG9zhIM+CRX6lqcznMDjLRr/1tuVaJ9E6xqJkzjfPgRzNcCqwMt6rpNkL7Q==
"@intlify/unplugin-vue-i18n@^0.11.0":
version "0.11.0"
resolved "https://registry.yarnpkg.com/@intlify/unplugin-vue-i18n/-/unplugin-vue-i18n-0.11.0.tgz#43d2730d86ceb214e7e147b0cc8f78f7d7df0707"
integrity sha512-ivcLZo08fvepHWV8o5lcKfhcKFSWqhwrqIAU6pUIbvq2ICo9fnXnIPYIZj7FeuHDLW1G3ADm44ZhQC3nYmvDlg==
dependencies:
"@intlify/bundle-utils" "^5.4.0"
"@intlify/bundle-utils" "^6.0.1"
"@intlify/shared" "9.3.0-beta.17"
"@rollup/pluginutils" "^5.0.2"
"@vue/compiler-sfc" "^3.2.47"
@ -283,10 +284,10 @@
resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==
"@jridgewell/source-map@^0.3.2":
version "0.3.2"
resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb"
integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==
"@jridgewell/source-map@^0.3.3":
version "0.3.3"
resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.3.tgz#8108265659d4c33e72ffe14e33d6cc5eb59f2fda"
integrity sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==
dependencies:
"@jridgewell/gen-mapping" "^0.3.0"
"@jridgewell/trace-mapping" "^0.3.9"
@ -325,10 +326,10 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@popperjs/core@^2.11.7":
version "2.11.7"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.7.tgz#ccab5c8f7dc557a52ca3288c10075c9ccd37fff7"
integrity sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==
"@popperjs/core@^2.11.8":
version "2.11.8"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
"@popperjs/core@^2.9.2":
version "2.11.5"
@ -371,16 +372,21 @@
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.2.3":
version "20.2.3"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.2.3.tgz#b31eb300610c3835ac008d690de6f87e28f9b878"
integrity sha512-pg9d0yC4rVNWQzX8U7xb4olIOFuuVL9za3bzMT2pu2SU0SNEi66i2qrvhE2qt0HvkhuCaWJu7pLNOt/Pj8BIrw==
"@types/node@^20.2.5":
version "20.2.5"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.2.5.tgz#26d295f3570323b2837d322180dfbf1ba156fefb"
integrity sha512-JJulVEQXmiY9Px5axXHeYGLSjhkZEnD+MDPDGbCbIAbMslkKwmygtZFy1X6s/075Yo94sf8GuSlFfPzysQrWZQ==
"@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/spark-md5@^3.0.2":
version "3.0.2"
resolved "https://registry.yarnpkg.com/@types/spark-md5/-/spark-md5-3.0.2.tgz#da2e8a778a20335fc4f40b6471c4b0d86b70da55"
@ -766,16 +772,16 @@ acorn@^7.1.1, acorn@^7.4.1:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
acorn@^8.5.0, acorn@^8.8.2:
version "8.8.2"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a"
integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==
acorn@^8.8.0:
version "8.8.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8"
integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==
acorn@^8.8.2:
version "8.8.2"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a"
integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==
ajv@^6.10.0, ajv@^6.12.4:
version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
@ -843,10 +849,10 @@ bootstrap-icons-vue@^1.10.3:
resolved "https://registry.yarnpkg.com/bootstrap-icons-vue/-/bootstrap-icons-vue-1.10.3.tgz#ae725513c9655ce86effa2a0b09e9e65b02c8f1a"
integrity sha512-BzqmLufgHjFvSReJ1GQqNkl780UFK0rWT4Y1IQC7lZClXyOSsM5Ipw04BnuVmmrqgtSxzak83jcBwLJgCK3scg==
bootstrap@^5.3.0-alpha3:
version "5.3.0-alpha3"
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.3.0-alpha3.tgz#ad64d9a663c53ab7aca99c560e0bd16b5e023441"
integrity sha512-FBhOWMxkCFr74hesJdchLXhqagPTXS+kRNU3gE0FR5Ki/AdPSz32Ik96Z28+yBluCnE/pc9st7l1yPwKgbtfSA==
bootstrap@^5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.3.0.tgz#0718a7cc29040ee8dbf1bd652b896f3436a87c29"
integrity sha512-UnBV3E3v4STVNQdms6jSGO2CvOkjUMdDAVR2V5N4uCMdaIkaQjbcEAMqRimDHIs4uqBYzDAKCQwCB+97tJgHQw==
brace-expansion@^1.1.7:
version "1.1.11"
@ -1119,10 +1125,10 @@ escodegen@^2.0.0:
optionalDependencies:
source-map "~0.6.1"
eslint-plugin-vue@^9.14.0:
version "9.14.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-9.14.0.tgz#73004a62d794e276a60d471114d81ed8887efcb8"
integrity sha512-4O7EuiqPGVQA1wYCzLvCzsBTv9JIPHLHhrf0k55DLzbwtmJbSw2TKS0G/l7pOwi9RWMSkjIT7ftChU5gZpgnJw==
eslint-plugin-vue@^9.14.1:
version "9.14.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-9.14.1.tgz#3b0c9857642dac547c7564031cfb09d283eafdd4"
integrity sha512-LQazDB1qkNEKejLe/b5a9VfEbtbczcOaui5lQ4Qw0tbRBbQYREyxxOV5BQgNDTqGPs9pxqiEpbMi9ywuIaF7vw==
dependencies:
"@eslint-community/eslint-utils" "^4.3.0"
natural-compare "^1.4.0"
@ -1746,6 +1752,11 @@ jsonc-eslint-parser@^1.0.1:
espree "^6.0.0"
semver "^6.3.0"
jsonc-parser@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76"
integrity sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==
jsonfile@^6.0.1:
version "6.1.0"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
@ -1856,6 +1867,16 @@ mitt@^3.0.0:
resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.0.tgz#69ef9bd5c80ff6f57473e8d89326d01c414be0bd"
integrity sha512-7dX2/10ITVyqh4aOSVI9gdape+t9l2/8QxHrFmUXu4EEUpdlxl6RudZUPZoc+zuY2hk1j7XxVroIVIan/pD/SQ==
mlly@^1.2.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.3.0.tgz#3184cb80c6437bda861a9f452ae74e3434ed9cd1"
integrity sha512-HT5mcgIQKkOrZecOjOX3DJorTikWXwsBfpcr/MGBkhfWcjiqvnaL/9ppxvIUXfjT6xt4DVIAsN9fMUz1ev4bIw==
dependencies:
acorn "^8.8.2"
pathe "^1.1.0"
pkg-types "^1.0.3"
ufo "^1.1.2"
ms@2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
@ -2045,7 +2066,7 @@ path-type@^4.0.0:
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
pathe@^1.0.0:
pathe@^1.0.0, pathe@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.0.tgz#e2e13f6c62b31a3289af4ba19886c230f295ec03"
integrity sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w==
@ -2070,6 +2091,15 @@ pify@^3.0.0:
resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
integrity sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==
pkg-types@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.0.3.tgz#988b42ab19254c01614d13f4f65a2cfc7880f868"
integrity sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==
dependencies:
jsonc-parser "^3.2.0"
mlly "^1.2.0"
pathe "^1.1.0"
postcss-selector-parser@^6.0.9:
version "6.0.10"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz#79b61e2c0d1bfc2602d549e11d0876256f8df88d"
@ -2266,6 +2296,11 @@ slash@^3.0.0:
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
sortablejs@^1.15.0:
version "1.15.0"
resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.15.0.tgz#53230b8aa3502bb77a29e2005808ffdb4a5f7e2a"
integrity sha512-bv9qgVMjUMf89wAvM6AxVvS/4MX3sPeN0+agqShejLU5z5GX4C75ow1O2e5k4L6XItUyAK3gH6AxSbXrOM5e8w==
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
@ -2383,13 +2418,13 @@ supports-preserve-symlinks-flag@^1.0.0:
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
terser@^5.17.6:
version "5.17.6"
resolved "https://registry.yarnpkg.com/terser/-/terser-5.17.6.tgz#d810e75e1bb3350c799cd90ebefe19c9412c12de"
integrity sha512-V8QHcs8YuyLkLHsJO5ucyff1ykrLVsR4dNnS//L5Y3NiSXpbK1J+WMVUs67eI0KTxs9JtHhgEQpXQVHlHI92DQ==
terser@^5.17.7:
version "5.17.7"
resolved "https://registry.yarnpkg.com/terser/-/terser-5.17.7.tgz#2a8b134826fe179b711969fd9d9a0c2479b2a8c3"
integrity sha512-/bi0Zm2C6VAexlGgLlVxA0P2lru/sdLyfCVaRMfKVo9nWxbmz7f/sD8VPybPeSUJaJcwmCJis9pBIhcVcG1QcQ==
dependencies:
"@jridgewell/source-map" "^0.3.2"
acorn "^8.5.0"
"@jridgewell/source-map" "^0.3.3"
acorn "^8.8.2"
commander "^2.20.0"
source-map-support "~0.5.20"
@ -2436,10 +2471,15 @@ 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.0.4:
version "5.0.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.0.4.tgz#b217fd20119bd61a94d4011274e0ab369058da3b"
integrity sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==
typescript@^5.1.3:
version "5.1.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.3.tgz#8d84219244a6b40b6fb2b33cc1c062f715b9e826"
integrity sha512-XH627E9vkeqhlZFQuL+UsyAXEnibT0kWR2FWONlr4sTjvxyJYnyefgrkyECLzM5NenmKzRAy2rR/OlYLA1HkZw==
ufo@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.1.2.tgz#d0d9e0fa09dece0c31ffd57bd363f030a35cfe76"
integrity sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ==
unbox-primitive@^1.0.2:
version "1.0.2"
@ -2500,10 +2540,10 @@ vite-plugin-css-injected-by-js@^3.1.1:
resolved "https://registry.yarnpkg.com/vite-plugin-css-injected-by-js/-/vite-plugin-css-injected-by-js-3.1.1.tgz#8324412636cf6fdada1a86f595aa2e78458e5ddb"
integrity sha512-mwrFvEEy0TuH8Ul0cb2HgjmNboQ/JnEFy+kHCWqAJph3ikMOiIuyYVdx0JO4nEIWJyzSnc4TTdmoTulsikvJEg==
vite@^4.3.8:
version "4.3.8"
resolved "https://registry.yarnpkg.com/vite/-/vite-4.3.8.tgz#70cd6a294ab52d7fb8f37f5bc63d117dd19e9918"
integrity sha512-uYB8PwN7hbMrf4j1xzGDk/lqjsZvCDbt/JC5dyfxc19Pg8kRm14LinK/uq+HSLNswZEoKmweGdtpbnxRtrAXiQ==
vite@^4.3.9:
version "4.3.9"
resolved "https://registry.yarnpkg.com/vite/-/vite-4.3.9.tgz#db896200c0b1aa13b37cdc35c9e99ee2fdd5f96d"
integrity sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==
dependencies:
esbuild "^0.17.5"
postcss "^8.4.23"
@ -2547,10 +2587,10 @@ vue-i18n@^9.2.2:
"@intlify/vue-devtools" "9.2.2"
"@vue/devtools-api" "^6.2.1"
vue-router@^4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.2.1.tgz#f8ab85c89e74682cad71519480fdf2b855e8c9e0"
integrity sha512-nW28EeifEp8Abc5AfmAShy5ZKGsGzjcnZ3L1yc2DYUo+MqbBClrRP9yda3dIekM4I50/KnEwo1wkBLf7kHH5Cw==
vue-router@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.2.2.tgz#b0097b66d89ca81c0986be03da244c7b32a4fd81"
integrity sha512-cChBPPmAflgBGmy3tBsjeoe3f3VOSG6naKyY5pjtrqLGbNEXdzCigFUHgBvp9e3ysAtFtEx7OLqcSDh/1Cq2TQ==
dependencies:
"@vue/devtools-api" "^6.5.0"

Binary file not shown.