diff --git a/docs/DeviceProfiles/wt32-eth01.json b/docs/DeviceProfiles/wt32-eth01.json index 971dec91..e8aee231 100644 --- a/docs/DeviceProfiles/wt32-eth01.json +++ b/docs/DeviceProfiles/wt32-eth01.json @@ -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 + } } ] \ No newline at end of file diff --git a/include/Configuration.h b/include/Configuration.h index 8c0aefeb..ae61ba1c 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -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; diff --git a/include/Datastore.h b/include/Datastore.h index b36312af..4f9545a0 100644 --- a/include/Datastore.h +++ b/include/Datastore.h @@ -2,6 +2,8 @@ #pragma once #include +#include +#include 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; \ No newline at end of file diff --git a/include/SunPosition.h b/include/SunPosition.h index 49c5c71f..691d42c0 100644 --- a/include/SunPosition.h +++ b/include/SunPosition.h @@ -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; diff --git a/include/WebApi_errors.h b/include/WebApi_errors.h index e4ec7881..8107840d 100644 --- a/include/WebApi_errors.h +++ b/include/WebApi_errors.h @@ -28,6 +28,7 @@ enum WebApiError { InverterInvalidMaxChannel, InverterChanged, InverterDeleted, + InverterOrdered, LimitBase = 5000, LimitSerialZero, diff --git a/include/WebApi_inverter.h b/include/WebApi_inverter.h index c2298ec9..9f2b0673 100644 --- a/include/WebApi_inverter.h +++ b/include/WebApi_inverter.h @@ -13,6 +13,7 @@ private: void onInverterAdd(AsyncWebServerRequest* request); void onInverterEdit(AsyncWebServerRequest* request); void onInverterDelete(AsyncWebServerRequest* request); + void onInverterOrder(AsyncWebServerRequest* request); AsyncWebServer* _server; }; \ No newline at end of file diff --git a/include/defaults.h b/include/defaults.h index e1d7c70c..7fe0d511 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -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 "" diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp index b083248c..4c3285f5 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp @@ -121,17 +121,25 @@ void HoymilesRadio_CMT::loop() if (!_rxBuffer.empty()) { fragment_t f = _rxBuffer.back(); if (checkFragmentCrc(&f)) { - std::shared_ptr 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 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 { diff --git a/lib/Hoymiles/src/commands/MultiDataCommand.cpp b/lib/Hoymiles/src/commands/MultiDataCommand.cpp index c34ccd00..b25c9027 100644 --- a/lib/Hoymiles/src/commands/MultiDataCommand.cpp +++ b/lib/Hoymiles/src/commands/MultiDataCommand.cpp @@ -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); diff --git a/lib/Hoymiles/src/parser/AlarmLogParser.cpp b/lib/Hoymiles/src/parser/AlarmLogParser.cpp index 1ac701be..3e1dc664 100644 --- a/lib/Hoymiles/src/parser/AlarmLogParser.cpp +++ b/lib/Hoymiles/src/parser/AlarmLogParser.cpp @@ -6,9 +6,10 @@ #include "../Hoymiles.h" #include -const std::array AlarmLogParser::_alarmMessages = {{ +const std::array 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" }, diff --git a/lib/Hoymiles/src/parser/AlarmLogParser.h b/lib/Hoymiles/src/parser/AlarmLogParser.h index b57948be..0286db15 100644 --- a/lib/Hoymiles/src/parser/AlarmLogParser.h +++ b/lib/Hoymiles/src/parser/AlarmLogParser.h @@ -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 _alarmMessages; + static const std::array _alarmMessages; }; \ No newline at end of file diff --git a/lib/Hoymiles/src/parser/DevInfoParser.cpp b/lib/Hoymiles/src/parser/DevInfoParser.cpp index 1c8c4822..0c1a10f5 100644 --- a/lib/Hoymiles/src/parser/DevInfoParser.cpp +++ b/lib/Hoymiles/src/parser/DevInfoParser.cpp @@ -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 diff --git a/pio-scripts/create_factory_bin.py b/pio-scripts/create_factory_bin.py new file mode 100644 index 00000000..56f71c4b --- /dev/null +++ b/pio-scripts/create_factory_bin.py @@ -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\\partitions.bin +# - 0xe000 | ~\.platformio\packages\framework-arduinoespressif32\tools\partitions\boot_app0.bin +# - 0x10000 | ~\ESPEasy\.pio\build\/.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) \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index 4d30348c..307d37b5 100644 --- a/platformio.ini +++ b/platformio.ini @@ -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 diff --git a/src/Configuration.cpp b/src/Configuration.cpp index 9e22a9cc..b6d9c605 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -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(); 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; diff --git a/src/Datastore.cpp b/src/Datastore.cpp index fa5b8c60..880ff174 100644 --- a/src/Datastore.cpp +++ b/src/Datastore.cpp @@ -6,10 +6,17 @@ #include "Configuration.h" #include +#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(totalAcYieldTotalDigits, inv->Statistics()->getChannelFieldDigits(TYPE_AC, c, FLD_YT)); - totalAcYieldDayDigits = max(totalAcYieldDayDigits, inv->Statistics()->getChannelFieldDigits(TYPE_AC, c, FLD_YD)); + _totalAcYieldTotalDigits = max(_totalAcYieldTotalDigits, inv->Statistics()->getChannelFieldDigits(TYPE_AC, c, FLD_YT)); + _totalAcYieldDayDigits = max(_totalAcYieldDayDigits, inv->Statistics()->getChannelFieldDigits(TYPE_AC, c, FLD_YD)); } if (inv->getEnablePolling()) { - totalAcPowerEnabled += inv->Statistics()->getChannelFieldValue(TYPE_AC, c, FLD_PAC); - totalAcPowerDigits = max(totalAcPowerDigits, inv->Statistics()->getChannelFieldDigits(TYPE_AC, c, FLD_PAC)); + _totalAcPowerEnabled += inv->Statistics()->getChannelFieldValue(TYPE_AC, c, FLD_PAC); + _totalAcPowerDigits = max(_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(totalDcPowerDigits, inv->Statistics()->getChannelFieldDigits(TYPE_DC, c, FLD_PDC)); + _totalDcPowerEnabled += inv->Statistics()->getChannelFieldValue(TYPE_DC, c, FLD_PDC); + _totalDcPowerDigits = max(_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(); } -} \ No newline at end of file +} + +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; +} diff --git a/src/Display_Graphic.cpp b/src/Display_Graphic.cpp index d098c506..972b3bc2 100644 --- a/src/Display_Graphic.cpp +++ b/src/Display_Graphic.cpp @@ -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); //<======================= diff --git a/src/Led_Single.cpp b/src/Led_Single.cpp index f4f1cf0b..bd87ba1d 100644 --- a/src/Led_Single.cpp +++ b/src/Led_Single.cpp @@ -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; } } diff --git a/src/MqttHandleInverterTotal.cpp b/src/MqttHandleInverterTotal.cpp index f89436c2..ac8e6a4e 100644 --- a/src/MqttHandleInverterTotal.cpp +++ b/src/MqttHandleInverterTotal.cpp @@ -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); } diff --git a/src/SunPosition.cpp b/src/SunPosition.cpp index 1485eaa4..7ab453f0 100644 --- a/src/SunPosition.cpp +++ b/src/SunPosition.cpp @@ -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(_sun.calcCustomSunrise(SunSet::SUNSET_NAUTICAL)); - _sunsetMinutes = static_cast(_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(sunriseRaw); + _sunsetMinutes = static_cast(sunsetRaw); uint minutesPastMidnight = timeinfo.tm_hour * 60 + timeinfo.tm_min; _isDayPeriod = (minutesPastMidnight >= _sunriseMinutes) && (minutesPastMidnight < _sunsetMinutes); + _isSunsetAvailable = true; _isValidInfo = true; } diff --git a/src/WebApi_inverter.cpp b/src/WebApi_inverter.cpp index 3f42d3c4..495b5fac 100644 --- a/src/WebApi_inverter.cpp +++ b/src/WebApi_inverter.cpp @@ -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(); + uint8_t order = 0; + for(JsonVariant id : orderArray) { + uint8_t inverter_id = id.as(); + 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); } \ No newline at end of file diff --git a/src/WebApi_ntp.cpp b/src/WebApi_ntp.cpp index e5927123..26524c2a 100644 --- a/src/WebApi_ntp.cpp +++ b/src/WebApi_ntp.cpp @@ -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().c_str(), sizeof(config.Ntp_TimezoneDescr)); config.Ntp_Latitude = root["latitude"].as(); config.Ntp_Longitude = root["longitude"].as(); + config.Ntp_SunsetType = root["sunsettype"].as(); Configuration.write(); retMsg["type"] = "success"; diff --git a/src/WebApi_ws_live.cpp b/src/WebApi_ws_live.cpp index 7a359358..66155d41 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -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(c))]["name"]["u"] = inv_cfg->channel[c].Name; - } + chanTypeObj[String(static_cast(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; diff --git a/webapp/package.json b/webapp/package.json index 9c7096e4..7ae39f84 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -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" diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 120e430c..1e4cccc8 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -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": "Hinweis: 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:", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 91c59d59..28aaadcc 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -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": "Hint: 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:", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index fb414e4e..1bf2c09f 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -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": "Astuce : 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", diff --git a/webapp/src/types/LiveDataStatus.ts b/webapp/src/types/LiveDataStatus.ts index a64e681a..0e5458f1 100644 --- a/webapp/src/types/LiveDataStatus.ts +++ b/webapp/src/types/LiveDataStatus.ts @@ -23,6 +23,7 @@ export interface InverterStatistics { export interface Inverter { serial: number; name: string; + order: number; data_age: number; poll_enabled: boolean; reachable: boolean; diff --git a/webapp/src/types/NtpConfig.ts b/webapp/src/types/NtpConfig.ts index 172ffa09..a87c806a 100644 --- a/webapp/src/types/NtpConfig.ts +++ b/webapp/src/types/NtpConfig.ts @@ -4,4 +4,5 @@ export interface NtpConfig { ntp_timezone_descr: string; latitude: number; longitude: number; + sunsettype: number; } \ No newline at end of file diff --git a/webapp/src/types/NtpStatus.ts b/webapp/src/types/NtpStatus.ts index 55975751..f0f48d89 100644 --- a/webapp/src/types/NtpStatus.ts +++ b/webapp/src/types/NtpStatus.ts @@ -7,4 +7,5 @@ export interface NtpStatus { sun_risetime: string; sun_settime: string; sun_isDayPeriod: boolean; + sun_isSunsetAvailable: boolean; } \ No newline at end of file diff --git a/webapp/src/views/HomeView.vue b/webapp/src/views/HomeView.vue index e31e3719..abd7eff7 100644 --- a/webapp/src/views/HomeView.vue +++ b/webapp/src/views/HomeView.vue @@ -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: { diff --git a/webapp/src/views/InverterAdminView.vue b/webapp/src/views/InverterAdminView.vue index eb692ba1..3660a498 100644 --- a/webapp/src/views/InverterAdminView.vue +++ b/webapp/src/views/InverterAdminView.vue @@ -28,6 +28,7 @@ + @@ -35,8 +36,9 @@ - - + + + - + + - + + diff --git a/webapp/yarn.lock b/webapp/yarn.lock index 30b85096..7a5d7d5c 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -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" diff --git a/webapp_dist/js/app.js.gz b/webapp_dist/js/app.js.gz index c8a15fed..d11f6fbd 100644 Binary files a/webapp_dist/js/app.js.gz and b/webapp_dist/js/app.js.gz differ
# {{ $t('inverteradmin.Status') }} {{ $t('inverteradmin.Serial') }} {{ $t('inverteradmin.Name') }}{{ $t('inverteradmin.Action') }}
+ + @@ -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() })); + }, }, }); - \ No newline at end of file + + + \ No newline at end of file diff --git a/webapp/src/views/NtpAdminView.vue b/webapp/src/views/NtpAdminView.vue index 97f9b2a3..69e696c9 100644 --- a/webapp/src/views/NtpAdminView.vue +++ b/webapp/src/views/NtpAdminView.vue @@ -36,6 +36,21 @@ + + +
+ +
+ +
+
@@ -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: { diff --git a/webapp/src/views/NtpInfoView.vue b/webapp/src/views/NtpInfoView.vue index 66d1119e..62da5be3 100644 --- a/webapp/src/views/NtpInfoView.vue +++ b/webapp/src/views/NtpInfoView.vue @@ -38,11 +38,13 @@
{{ $t('ntpinfo.Sunrise') }}{{ ntpDataList.sun_risetime }}{{ ntpDataList.sun_risetime }}{{ $t('ntpinfo.NotAvailable') }}
{{ $t('ntpinfo.Sunset') }}{{ ntpDataList.sun_settime }}{{ ntpDataList.sun_settime }}{{ $t('ntpinfo.NotAvailable') }}
{{ $t('ntpinfo.Mode') }}