diff --git a/.vscode/extensions.json b/.vscode/extensions.json index bd7f35aa6..d18910d31 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -5,7 +5,6 @@ "DavidAnson.vscode-markdownlint", "EditorConfig.EditorConfig", "Vue.volar", - "Vue.vscode-typescript-vue-plugin", "platformio.platformio-ide" ], "unwantedRecommendations": [ diff --git a/README.md b/README.md index c0432b66b..45677b55e 100644 --- a/README.md +++ b/README.md @@ -29,10 +29,6 @@ Summer 2022 I bought my Victron MPPT battery charger, and didn't like the idea t This project is still under development and adds following features: -> **Warning** -> -> In contrast to the original openDTU, with release 2023.05.23.post1 openDTU-onBattery supports only 5 inverters. Otherwise, there is not enough memory for the liveData view. - * Support Victron's Ve.Direct protocol on the same chip (cable based serial interface!). Additional information about Ve.direct can be downloaded directly from [Victron's website](https://www.victronenergy.com/support-and-downloads/technical-information). * Dynamically sets the Hoymiles power limited according to the currently used energy in the household. Needs an HTTP JSON based power meter (e.g. Tasmota), an MQTT based power meter like Shelly 3EM or an SDM power meter. * Battery support: Read the voltage from Victron MPPT charge controller or from the Hoymiles DC inputs and starts/stops the power producing based on configurable voltage thresholds diff --git a/docs/hardware_flash.md b/docs/hardware_flash.md index 8022f436e..e8aa193c2 100644 --- a/docs/hardware_flash.md +++ b/docs/hardware_flash.md @@ -49,9 +49,7 @@ The SN65HVD230 CAN bus transceiver is used to interface with the Pylontech batte ### MCP2515 CAN bus module -The MCP2515 CAN bus module consists of a CAN bus controller and a CAN bus transceiver and is used to interface with the Huawei AC charger. This CAN bus operates at 125kbit/s. The module is connected via SPI and currently requires a separate SPI bus. If you want to use the Huawei AC charger make sure to get an ESP which supports 2 SPI busses. Currently the SPI bus host is hardcoded to number 2. This may change in future. Please note: Using the Huawei AC charger in combination with the CMT2300A radio board is not supported at the moment. - -MCP2515 CAN bus modules that are widely available are designed for 5V supply voltage. To make them work with 3.3V / the ESP32 a modification is required. [This modification is described here.](https://forums.raspberrypi.com/viewtopic.php?t=141052) +See [Wiki](https://github.com/helgeerbe/OpenDTU-OnBattery/wiki/Huawei-AC-PSU) for details. ### Relay module @@ -188,4 +186,4 @@ After the successful upload, the OpenDTU immediately restarts into the new firmw ## Builds Different builds from existing installations can be found here [Builds](builds/README.md) -Like to show your own build? Just send me a Pull Request. \ No newline at end of file +Like to show your own build? Just send me a Pull Request. diff --git a/include/Battery.h b/include/Battery.h index b5f5ace63..700c46c45 100644 --- a/include/Battery.h +++ b/include/Battery.h @@ -1,7 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once -#include #include #include #include @@ -9,28 +8,28 @@ #include "BatteryStats.h" class BatteryProvider { - public: - // returns true if the provider is ready for use, false otherwise - virtual bool init(bool verboseLogging) = 0; - - virtual void deinit() = 0; - virtual void loop() = 0; - virtual std::shared_ptr getStats() const = 0; +public: + // returns true if the provider is ready for use, false otherwise + virtual bool init(bool verboseLogging) = 0; + virtual void deinit() = 0; + virtual void loop() = 0; + virtual std::shared_ptr getStats() const = 0; + virtual bool usesHwPort2() const { return false; } }; class BatteryClass { - public: - void init(Scheduler&); - void updateSettings(); +public: + void init(Scheduler&); + void updateSettings(); - std::shared_ptr getStats() const; - private: - void loop(); + std::shared_ptr getStats() const; - Task _loopTask; +private: + void loop(); - mutable std::mutex _mutex; - std::unique_ptr _upProvider = nullptr; + Task _loopTask; + mutable std::mutex _mutex; + std::unique_ptr _upProvider = nullptr; }; extern BatteryClass Battery; diff --git a/include/BatteryStats.h b/include/BatteryStats.h index c11f45f95..86e1750b2 100644 --- a/include/BatteryStats.h +++ b/include/BatteryStats.h @@ -35,18 +35,22 @@ class BatteryStats { bool isSoCValid() const { return _lastUpdateSoC > 0; } bool isVoltageValid() const { return _lastUpdateVoltage > 0; } + // returns true if the battery reached a critically low voltage/SoC, + // such that it is in need of charging to prevent degredation. + virtual bool needsCharging() const { return false; } + protected: virtual void mqttPublish() const; void setSoC(float soc, uint8_t precision, uint32_t timestamp) { _soc = soc; _socPrecision = precision; - _lastUpdateSoC = timestamp; + _lastUpdateSoC = _lastUpdate = timestamp; } void setVoltage(float voltage, uint32_t timestamp) { _voltage = voltage; - _lastUpdateVoltage = timestamp; + _lastUpdateVoltage = _lastUpdate = timestamp; } String _manufacturer = "unknown"; @@ -67,6 +71,7 @@ class PylontechBatteryStats : public BatteryStats { public: void getLiveViewData(JsonVariant& root) const final; void mqttPublish() const final; + bool needsCharging() const final { return _chargeImmediately; } private: void setManufacturer(String&& m) { _manufacturer = std::move(m); } @@ -147,6 +152,9 @@ class VictronSmartShuntStats : public BatteryStats { float _chargedEnergy; float _dischargedEnergy; String _modelName; + int32_t _instantaneousPower; + float _consumedAmpHours; + int32_t _lastFullCharge; bool _alarmLowVoltage; bool _alarmHighVoltage; diff --git a/include/Configuration.h b/include/Configuration.h index 5fdd21c7f..077ba9414 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -23,7 +23,7 @@ #define MQTT_MAX_CERT_STRLEN 2560 #define INV_MAX_NAME_STRLEN 31 -#define INV_MAX_COUNT 5 +#define INV_MAX_COUNT 10 #define INV_MAX_CHAN_COUNT 6 #define CHAN_MAX_NAME_STRLEN 31 @@ -198,16 +198,17 @@ struct CONFIG_T { bool HttpIndividualRequests; POWERMETER_HTTP_PHASE_CONFIG_T Http_Phase[POWERMETER_MAX_PHASES]; } PowerMeter; - + struct { bool Enabled; bool VerboseLogging; bool SolarPassThroughEnabled; uint8_t SolarPassThroughLosses; - uint8_t BatteryDrainStategy; + bool BatteryAlwaysUseAtNight; uint32_t Interval; bool IsInverterBehindPowerMeter; - uint8_t InverterId; + bool IsInverterSolarPowered; + uint64_t InverterId; uint8_t InverterChannelId; int32_t TargetPowerConsumption; int32_t TargetPowerConsumptionHysteresis; @@ -224,7 +225,7 @@ struct CONFIG_T { float FullSolarPassThroughStartVoltage; float FullSolarPassThroughStopVoltage; } PowerLimiter; - + struct { bool Enabled; bool VerboseLogging; @@ -242,7 +243,7 @@ struct CONFIG_T { float Auto_Power_Voltage_Limit; float Auto_Power_Enable_Voltage_Limit; float Auto_Power_Lower_Power_Limit; - float Auto_Power_Upper_Power_Limit; + float Auto_Power_Upper_Power_Limit; } Huawei; @@ -260,6 +261,7 @@ class ConfigurationClass { INVERTER_CONFIG_T* getFreeInverterSlot(); INVERTER_CONFIG_T* getInverterConfig(const uint64_t serial); + void deleteInverterById(const uint8_t id); }; extern ConfigurationClass Configuration; diff --git a/include/HttpPowerMeter.h b/include/HttpPowerMeter.h index 40eed04dd..7ac225a47 100644 --- a/include/HttpPowerMeter.h +++ b/include/HttpPowerMeter.h @@ -25,7 +25,7 @@ class HttpPowerMeterClass { String extractParam(String& authReq, const String& param, const char delimit); String getcNonce(const int len); String getDigestAuth(String& authReq, const String& username, const String& password, const String& method, const String& uri, unsigned int counter); - bool tryGetFloatValueForPhase(int phase, int httpCode, const char* jsonPath); + bool tryGetFloatValueForPhase(int phase, const char* jsonPath); void prepareRequest(uint32_t timeout, const char* httpHeader, const char* httpValue); String sha256(const String& data); }; diff --git a/include/JkBmsController.h b/include/JkBmsController.h index 5399951d4..b21744d3f 100644 --- a/include/JkBmsController.h +++ b/include/JkBmsController.h @@ -19,6 +19,7 @@ class Controller : public BatteryProvider { void deinit() final; void loop() final; std::shared_ptr getStats() const final { return _stats; } + bool usesHwPort2() const final { return true; } private: enum class Status : unsigned { diff --git a/include/MqttBattery.h b/include/MqttBattery.h index 61df04500..7948019e3 100644 --- a/include/MqttBattery.h +++ b/include/MqttBattery.h @@ -5,23 +5,23 @@ #include class MqttBattery : public BatteryProvider { - public: - MqttBattery() = default; +public: + MqttBattery() = default; - bool init(bool verboseLogging) final; - void deinit() final; - void loop() final { return; } // this class is event-driven - std::shared_ptr getStats() const final { return _stats; } + bool init(bool verboseLogging) final; + void deinit() final; + void loop() final { return; } // this class is event-driven + std::shared_ptr getStats() const final { return _stats; } - private: - bool _verboseLogging = false; - String _socTopic; - String _voltageTopic; - std::shared_ptr _stats = std::make_shared(); +private: + bool _verboseLogging = false; + String _socTopic; + String _voltageTopic; + std::shared_ptr _stats = std::make_shared(); - std::optional getFloat(std::string const& src, char const* topic); - void onMqttMessageSoC(espMqttClientTypes::MessageProperties const& properties, - char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total); - void onMqttMessageVoltage(espMqttClientTypes::MessageProperties const& properties, - char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total); + std::optional getFloat(std::string const& src, char const* topic); + void onMqttMessageSoC(espMqttClientTypes::MessageProperties const& properties, + char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total); + void onMqttMessageVoltage(espMqttClientTypes::MessageProperties const& properties, + char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total); }; diff --git a/include/MqttHandlePowerLimiter.h b/include/MqttHandlePowerLimiter.h index d78c3f19d..fa7ef12cc 100644 --- a/include/MqttHandlePowerLimiter.h +++ b/include/MqttHandlePowerLimiter.h @@ -14,7 +14,19 @@ class MqttHandlePowerLimiterClass { private: void loop(); - void onCmdMode(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total); + + enum class MqttPowerLimiterCommand : unsigned { + Mode, + BatterySoCStartThreshold, + BatterySoCStopThreshold, + FullSolarPassthroughSoC, + VoltageStartThreshold, + VoltageStopThreshold, + FullSolarPassThroughStartVoltage, + FullSolarPassThroughStopVoltage + }; + + void onMqttCmd(MqttPowerLimiterCommand command, const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total); Task _loopTask; @@ -28,4 +40,4 @@ class MqttHandlePowerLimiterClass { std::deque> _mqttCallbacks; }; -extern MqttHandlePowerLimiterClass MqttHandlePowerLimiter; \ No newline at end of file +extern MqttHandlePowerLimiterClass MqttHandlePowerLimiter; diff --git a/include/MqttHandlePowerLimiterHass.h b/include/MqttHandlePowerLimiterHass.h new file mode 100644 index 000000000..f465994c6 --- /dev/null +++ b/include/MqttHandlePowerLimiterHass.h @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include + +class MqttHandlePowerLimiterHassClass { +public: + void init(Scheduler& scheduler); + void publishConfig(); + void forceUpdate(); + +private: + void loop(); + void publish(const String& subtopic, const String& payload); + void publishNumber(const char* caption, const char* icon, const char* category, const char* commandTopic, const char* stateTopic, const char* unitOfMeasure, const int16_t min, const int16_t max); + void publishSelect(const char* caption, const char* icon, const char* category, const char* commandTopic, const char* stateTopic); + void createDeviceInfo(JsonObject& object); + + Task _loopTask; + + bool _wasConnected = false; + bool _updateForced = false; +}; + +extern MqttHandlePowerLimiterHassClass MqttHandlePowerLimiterHass; diff --git a/include/MqttHandleVedirect.h b/include/MqttHandleVedirect.h index 571ee1e6a..c420d0884 100644 --- a/include/MqttHandleVedirect.h +++ b/include/MqttHandleVedirect.h @@ -4,6 +4,7 @@ #include "VeDirectMpptController.h" #include "Configuration.h" #include +#include #include #ifndef VICTRON_PIN_RX @@ -20,7 +21,7 @@ class MqttHandleVedirectClass { void forceUpdate(); private: void loop(); - VeDirectMpptController::veMpptStruct _kvFrame{}; + std::map _kvFrames; Task _loopTask; @@ -31,6 +32,9 @@ class MqttHandleVedirectClass { uint32_t _nextPublishFull = 1; bool _PublishFull; + + void publish_mppt_data(const VeDirectMpptController::spData_t &spMpptData, + VeDirectMpptController::veMpptStruct &frame) const; }; -extern MqttHandleVedirectClass MqttHandleVedirect; \ No newline at end of file +extern MqttHandleVedirectClass MqttHandleVedirect; diff --git a/include/MqttHandleVedirectHass.h b/include/MqttHandleVedirectHass.h index 577f08d6a..86d364cda 100644 --- a/include/MqttHandleVedirectHass.h +++ b/include/MqttHandleVedirectHass.h @@ -14,9 +14,15 @@ class MqttHandleVedirectHassClass { private: void loop(); void publish(const String& subtopic, const String& payload); - void publishBinarySensor(const char* caption, const char* icon, const char* subTopic, const char* payload_on, const char* payload_off); - void publishSensor(const char* caption, const char* icon, const char* subTopic, const char* deviceClass = NULL, const char* stateClass = NULL, const char* unitOfMeasurement = NULL); - void createDeviceInfo(JsonObject& object); + void publishBinarySensor(const char *caption, const char *icon, const char *subTopic, + const char *payload_on, const char *payload_off, + const VeDirectMpptController::spData_t &spMpptData); + void publishSensor(const char *caption, const char *icon, const char *subTopic, + const char *deviceClass, const char *stateClass, + const char *unitOfMeasurement, + const VeDirectMpptController::spData_t &spMpptData); + void createDeviceInfo(JsonObject &object, + const VeDirectMpptController::spData_t &spMpptData); Task _loopTask; @@ -24,4 +30,4 @@ class MqttHandleVedirectHassClass { bool _updateForced = false; }; -extern MqttHandleVedirectHassClass MqttHandleVedirectHass; \ No newline at end of file +extern MqttHandleVedirectHassClass MqttHandleVedirectHass; diff --git a/include/PinMapping.h b/include/PinMapping.h index 4096e8072..f6db7a2da 100644 --- a/include/PinMapping.h +++ b/include/PinMapping.h @@ -38,8 +38,13 @@ struct PinMapping_t { uint8_t display_clk; uint8_t display_cs; uint8_t display_reset; + int8_t led[PINMAPPING_LED_COUNT]; + + // OpenDTU-OnBattery-specific pins below int8_t victron_tx; int8_t victron_rx; + int8_t victron_tx2; + int8_t victron_rx2; int8_t battery_rx; int8_t battery_rxen; int8_t battery_tx; @@ -50,7 +55,9 @@ struct PinMapping_t { int8_t huawei_irq; int8_t huawei_cs; int8_t huawei_power; - int8_t led[PINMAPPING_LED_COUNT]; + int8_t powermeter_rx; + int8_t powermeter_tx; + int8_t powermeter_dere; }; class PinMappingClass { @@ -63,7 +70,7 @@ class PinMappingClass { bool isValidCmt2300Config() const; bool isValidEthConfig() const; bool isValidHuaweiConfig() const; - + private: PinMapping_t _pinMapping; }; diff --git a/include/PowerLimiter.h b/include/PowerLimiter.h index 81e5be381..0a8dfab04 100644 --- a/include/PowerLimiter.h +++ b/include/PowerLimiter.h @@ -40,8 +40,11 @@ class PowerLimiterClass { InverterPowerCmdPending, InverterDevInfoPending, InverterStatsPending, + CalculatedLimitBelowMinLimit, UnconditionalSolarPassthrough, NoVeDirect, + NoEnergy, + HuaweiPsu, Settling, Stable, }; @@ -91,10 +94,10 @@ class PowerLimiterClass { int32_t inverterPowerDcToAc(std::shared_ptr inverter, int32_t dcPower); void unconditionalSolarPassthrough(std::shared_ptr inverter); bool canUseDirectSolarPower(); - int32_t calcPowerLimit(std::shared_ptr inverter, bool solarPowerEnabled, bool batteryDischargeEnabled); + bool calcPowerLimit(std::shared_ptr inverter, int32_t solarPower, bool batteryPower); bool updateInverter(); bool setNewPowerLimit(std::shared_ptr inverter, int32_t newPowerLimit); - int32_t getSolarChargePower(); + int32_t getSolarPower(); float getLoadCorrectedVoltage(); bool testThreshold(float socThreshold, float voltThreshold, std::function compare); diff --git a/include/PowerMeter.h b/include/PowerMeter.h index f6d29f2f0..f2b2042c6 100644 --- a/include/PowerMeter.h +++ b/include/PowerMeter.h @@ -6,21 +6,11 @@ #include #include #include +#include #include "SDM.h" #include "sml.h" #include - -#ifndef SDM_RX_PIN -#define SDM_RX_PIN 13 -#endif - -#ifndef SDM_TX_PIN -#define SDM_TX_PIN 32 -#endif - -#ifndef SML_RX_PIN -#define SML_RX_PIN 35 -#endif +#include typedef struct { const unsigned char OBIS[6]; @@ -30,12 +20,13 @@ typedef struct { class PowerMeterClass { public: - enum SOURCE { - SOURCE_MQTT = 0, - SOURCE_SDM1PH = 1, - SOURCE_SDM3PH = 2, - SOURCE_HTTP = 3, - SOURCE_SML = 4 + enum class Source : unsigned { + MQTT = 0, + SDM1PH = 1, + SDM3PH = 2, + HTTP = 3, + SML = 4, + SMAHM2 = 5 }; void init(Scheduler& scheduler); float getPowerTotal(bool forceUpdate = true); @@ -48,7 +39,7 @@ class PowerMeterClass { void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total); - Task _loopTask; + Task _loopTask; bool _verboseLogging = true; uint32_t _lastPowerMeterCheck; @@ -66,6 +57,11 @@ class PowerMeterClass { std::map _mqttSubscriptions; + mutable std::mutex _mutex; + + std::unique_ptr _upSdm = nullptr; + std::unique_ptr _upSmlSerial = nullptr; + void readPowerMeter(); bool smlReadLoop(); diff --git a/include/SMA_HM.h b/include/SMA_HM.h new file mode 100644 index 000000000..e5600902b --- /dev/null +++ b/include/SMA_HM.h @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2024 Holger-Steffen Stapf + */ +#pragma once + +#include +#include + +class SMA_HMClass { +public: + void init(Scheduler& scheduler, bool verboseLogging); + void loop(); + void event1(); + float getPowerTotal() const { return _powerMeterPower; } + float getPowerL1() const { return _powerMeterL1; } + float getPowerL2() const { return _powerMeterL2; } + float getPowerL3() const { return _powerMeterL3; } + +private: + void Soutput(int kanal, int index, int art, int tarif, + char const* name, float value, uint32_t timestamp); + + uint8_t* decodeGroup(uint8_t* offset, uint16_t grouplen); + + bool _verboseLogging = false; + float _powerMeterPower = 0.0; + float _powerMeterL1 = 0.0; + float _powerMeterL2 = 0.0; + float _powerMeterL3 = 0.0; + uint32_t _previousMillis = 0; + uint32_t _serial = 0; + Task _loopTask; +}; + +extern SMA_HMClass SMA_HM; diff --git a/include/SerialPortManager.h b/include/SerialPortManager.h new file mode 100644 index 000000000..4d4c2dabb --- /dev/null +++ b/include/SerialPortManager.h @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include + +class SerialPortManagerClass { +public: + bool allocateMpptPort(int port); + bool allocateBatteryPort(int port); + void invalidateBatteryPort(); + void invalidateMpptPorts(); + +private: + enum Owner { + BATTERY, + MPPT + }; + + std::map allocatedPorts; + + bool allocatePort(uint8_t port, Owner owner); + void invalidate(Owner owner); + + static const char* print(Owner owner); +}; + +extern SerialPortManagerClass SerialPortManager; diff --git a/include/Utils.h b/include/Utils.h index fddc2ab97..35d648bc6 100644 --- a/include/Utils.h +++ b/include/Utils.h @@ -11,5 +11,6 @@ class Utils { static int getTimezoneOffset(); static void restartDtu(); static bool checkJsonAlloc(const DynamicJsonDocument& doc, const char* function, const uint16_t line); + static bool checkJsonOverflow(const DynamicJsonDocument& doc, const char* function, const uint16_t line); static void removeAllFiles(); }; diff --git a/include/VictronMppt.h b/include/VictronMppt.h index 12d6bdf75..39e85aad7 100644 --- a/include/VictronMppt.h +++ b/include/VictronMppt.h @@ -5,6 +5,7 @@ #include #include "VeDirectMpptController.h" +#include "Configuration.h" #include class VictronMpptClass { @@ -16,12 +17,15 @@ class VictronMpptClass { void updateSettings(); bool isDataValid() const; + bool isDataValid(size_t idx) const; // returns the data age of all controllers, // i.e, the youngest data's age is returned. uint32_t getDataAgeMillis() const; + uint32_t getDataAgeMillis(size_t idx) const; - VeDirectMpptController::spData_t getData(size_t idx = 0) const; + size_t controllerAmount() const { return _controllers.size(); } + std::optional getData(size_t idx = 0) const; // total output of all MPPT charge controllers in Watts int32_t getPowerOutputWatts() const; @@ -50,6 +54,8 @@ class VictronMpptClass { mutable std::mutex _mutex; using controller_t = std::unique_ptr; std::vector _controllers; + + bool initController(int8_t rx, int8_t tx, bool logging, int hwSerialPort); }; extern VictronMpptClass VictronMppt; diff --git a/include/VictronSmartShunt.h b/include/VictronSmartShunt.h index ffb91ee5b..42b65774e 100644 --- a/include/VictronSmartShunt.h +++ b/include/VictronSmartShunt.h @@ -9,6 +9,7 @@ class VictronSmartShunt : public BatteryProvider { void deinit() final { } void loop() final; std::shared_ptr getStats() const final { return _stats; } + bool usesHwPort2() const final { return true; } private: uint32_t _lastUpdate = 0; diff --git a/include/WebApi_powerlimiter.h b/include/WebApi_powerlimiter.h index 7e076e0bb..c846e6d17 100644 --- a/include/WebApi_powerlimiter.h +++ b/include/WebApi_powerlimiter.h @@ -11,6 +11,7 @@ class WebApiPowerLimiterClass { private: void onStatus(AsyncWebServerRequest* request); + void onMetaData(AsyncWebServerRequest* request); void onAdminGet(AsyncWebServerRequest* request); void onAdminPost(AsyncWebServerRequest* request); diff --git a/include/WebApi_ws_vedirect_live.h b/include/WebApi_ws_vedirect_live.h index 3e0b81aba..d46de4cc2 100644 --- a/include/WebApi_ws_vedirect_live.h +++ b/include/WebApi_ws_vedirect_live.h @@ -2,6 +2,7 @@ #pragma once #include "ArduinoJson.h" +#include "Configuration.h" #include #include #include @@ -13,16 +14,18 @@ class WebApiWsVedirectLiveClass { void init(AsyncWebServer& server, Scheduler& scheduler); private: - void generateJsonResponse(JsonVariant& root); + void generateJsonResponse(JsonVariant& root, bool fullUpdate); + static void populateJson(const JsonObject &root, const VeDirectMpptController::spData_t &spMpptData); void onLivedataStatus(AsyncWebServerRequest* request); void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len); + bool hasUpdate(size_t idx); AsyncWebServer* _server; AsyncWebSocket _ws; - uint32_t _lastWsPublish = 0; - uint32_t _dataAgeMillis = 0; - static constexpr uint16_t _responseSize = 1024 + 128; + uint32_t _lastFullPublish = 0; + uint32_t _lastPublish = 0; + uint16_t responseSize() const; std::mutex _mutex; @@ -31,4 +34,4 @@ class WebApiWsVedirectLiveClass { Task _sendDataTask; void sendDataTaskCb(); -}; \ No newline at end of file +}; diff --git a/include/defaults.h b/include/defaults.h index 56030d9b3..940080c5f 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -104,58 +104,11 @@ #define REACHABLE_THRESHOLD 2U -#define MAX_INVERTER_LIMIT 2250 -#define VEDIRECT_ENABLED false -#define VEDIRECT_VERBOSE_LOGGING false -#define VEDIRECT_UPDATESONLY true - -#define POWERMETER_ENABLED false -#define POWERMETER_INTERVAL 10 -#define POWERMETER_SOURCE 2 -#define POWERMETER_SDMBAUDRATE 9600 -#define POWERMETER_SDMADDRESS 1 - - -#define POWERLIMITER_ENABLED false -#define POWERLIMITER_SOLAR_PASSTHROUGH_ENABLED true -#define POWERLIMITER_SOLAR_PASSTHROUGH_LOSSES 3 -#define POWERLIMITER_BATTERY_DRAIN_STRATEGY 0 -#define POWERLIMITER_INTERVAL 10 -#define POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER true -#define POWERLIMITER_INVERTER_ID 0 -#define POWERLIMITER_INVERTER_CHANNEL_ID 0 -#define POWERLIMITER_TARGET_POWER_CONSUMPTION 0 -#define POWERLIMITER_TARGET_POWER_CONSUMPTION_HYSTERESIS 0 -#define POWERLIMITER_LOWER_POWER_LIMIT 10 -#define POWERLIMITER_UPPER_POWER_LIMIT 800 -#define POWERLIMITER_IGNORE_SOC false -#define POWERLIMITER_BATTERY_SOC_START_THRESHOLD 80 -#define POWERLIMITER_BATTERY_SOC_STOP_THRESHOLD 20 -#define POWERLIMITER_VOLTAGE_START_THRESHOLD 50.0 -#define POWERLIMITER_VOLTAGE_STOP_THRESHOLD 49.0 -#define POWERLIMITER_VOLTAGE_LOAD_CORRECTION_FACTOR 0.001 -#define POWERLIMITER_RESTART_HOUR -1 -#define POWERLIMITER_FULL_SOLAR_PASSTHROUGH_SOC 100 -#define POWERLIMITER_FULL_SOLAR_PASSTHROUGH_START_VOLTAGE 100.0 -#define POWERLIMITER_FULL_SOLAR_PASSTHROUGH_STOP_VOLTAGE 100.0 - -#define BATTERY_ENABLED false -#define BATTERY_PROVIDER 0 // Pylontech CAN receiver -#define BATTERY_JKBMS_INTERFACE 0 -#define BATTERY_JKBMS_POLLING_INTERVAL 5 - -#define HUAWEI_ENABLED false -#define HUAWEI_CAN_CONTROLLER_FREQUENCY 8000000UL -#define HUAWEI_AUTO_POWER_VOLTAGE_LIMIT 42.0 -#define HUAWEI_AUTO_POWER_ENABLE_VOLTAGE_LIMIT 42.0 -#define HUAWEI_AUTO_POWER_LOWER_POWER_LIMIT 150 -#define HUAWEI_AUTO_POWER_UPPER_POWER_LIMIT 2000 - -#define VERBOSE_LOGGING true - #define LED_BRIGHTNESS 100U #define MAX_INVERTER_LIMIT 2250 + +// values specific to downstream project OpenDTU-OnBattery start here: #define VEDIRECT_ENABLED false #define VEDIRECT_VERBOSE_LOGGING false #define VEDIRECT_UPDATESONLY true @@ -166,19 +119,20 @@ #define POWERMETER_SDMBAUDRATE 9600 #define POWERMETER_SDMADDRESS 1 - #define POWERLIMITER_ENABLED false #define POWERLIMITER_SOLAR_PASSTHROUGH_ENABLED true #define POWERLIMITER_SOLAR_PASSTHROUGH_LOSSES 3 -#define POWERLIMITER_BATTERY_DRAIN_STRATEGY 0 +#define POWERLIMITER_BATTERY_ALWAYS_USE_AT_NIGHT false #define POWERLIMITER_INTERVAL 10 #define POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER true -#define POWERLIMITER_INVERTER_ID 0 +#define POWERLIMITER_IS_INVERTER_SOLAR_POWERED false +#define POWERLIMITER_INVERTER_ID 0ULL #define POWERLIMITER_INVERTER_CHANNEL_ID 0 #define POWERLIMITER_TARGET_POWER_CONSUMPTION 0 #define POWERLIMITER_TARGET_POWER_CONSUMPTION_HYSTERESIS 0 #define POWERLIMITER_LOWER_POWER_LIMIT 10 #define POWERLIMITER_UPPER_POWER_LIMIT 800 +#define POWERLIMITER_IGNORE_SOC false #define POWERLIMITER_BATTERY_SOC_START_THRESHOLD 80 #define POWERLIMITER_BATTERY_SOC_STOP_THRESHOLD 20 #define POWERLIMITER_VOLTAGE_START_THRESHOLD 50.0 @@ -202,7 +156,3 @@ #define HUAWEI_AUTO_POWER_UPPER_POWER_LIMIT 2000 #define VERBOSE_LOGGING true - -#define LED_BRIGHTNESS 100U - -#define MAX_INVERTER_LIMIT 2250 diff --git a/lib/Hoymiles/src/Hoymiles.cpp b/lib/Hoymiles/src/Hoymiles.cpp index 38791f584..b759cd6cf 100644 --- a/lib/Hoymiles/src/Hoymiles.cpp +++ b/lib/Hoymiles/src/Hoymiles.cpp @@ -1,9 +1,11 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022-2023 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ #include "Hoymiles.h" #include "Utils.h" +#include "inverters/HERF_2CH.h" +#include "inverters/HERF_4CH.h" #include "inverters/HMS_1CH.h" #include "inverters/HMS_1CHv2.h" #include "inverters/HMS_2CH.h" @@ -168,6 +170,10 @@ std::shared_ptr HoymilesClass::addInverter(const char* name, c i = std::make_shared(_radioNrf.get(), serial); } else if (HM_1CH::isValidSerial(serial)) { i = std::make_shared(_radioNrf.get(), serial); + } else if (HERF_2CH::isValidSerial(serial)) { + i = std::make_shared(_radioNrf.get(), serial); + } else if (HERF_4CH::isValidSerial(serial)) { + i = std::make_shared(_radioNrf.get(), serial); } if (i) { diff --git a/lib/Hoymiles/src/inverters/HERF_2CH.cpp b/lib/Hoymiles/src/inverters/HERF_2CH.cpp new file mode 100644 index 000000000..f0216a643 --- /dev/null +++ b/lib/Hoymiles/src/inverters/HERF_2CH.cpp @@ -0,0 +1,62 @@ + +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022-2024 Thomas Basler and others + */ +#include "HERF_2CH.h" + +static const byteAssign_t byteAssignment[] = { + { TYPE_DC, CH0, FLD_UDC, UNIT_V, 2, 2, 10, false, 1 }, + { TYPE_DC, CH0, FLD_IDC, UNIT_A, 6, 2, 100, false, 2 }, + { TYPE_DC, CH0, FLD_PDC, UNIT_W, 10, 2, 10, false, 1 }, + { TYPE_DC, CH0, FLD_YD, UNIT_WH, 22, 2, 1, false, 0 }, + { TYPE_DC, CH0, FLD_YT, UNIT_KWH, 14, 4, 1000, false, 3 }, + { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH0, CMD_CALC, false, 3 }, + + { TYPE_DC, CH1, FLD_UDC, UNIT_V, 4, 2, 10, false, 1 }, + { TYPE_DC, CH1, FLD_IDC, UNIT_A, 8, 2, 100, false, 2 }, + { TYPE_DC, CH1, FLD_PDC, UNIT_W, 12, 2, 10, false, 1 }, + { TYPE_DC, CH1, FLD_YD, UNIT_WH, 24, 2, 1, false, 0 }, + { TYPE_DC, CH1, FLD_YT, UNIT_KWH, 18, 4, 1000, false, 3 }, + { TYPE_DC, CH1, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH1, CMD_CALC, false, 3 }, + + { TYPE_AC, CH0, FLD_UAC, UNIT_V, 26, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_IAC, UNIT_A, 34, 2, 100, false, 2 }, + { TYPE_AC, CH0, FLD_PAC, UNIT_W, 30, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_Q, UNIT_VAR, 32, 2, 10, false, 1 }, + { TYPE_AC, CH0, FLD_F, UNIT_HZ, 28, 2, 100, false, 2 }, + { TYPE_AC, CH0, FLD_PF, UNIT_NONE, 36, 2, 1000, false, 3 }, + + { TYPE_INV, CH0, FLD_T, UNIT_C, 38, 2, 10, true, 1 }, + { TYPE_INV, CH0, FLD_EVT_LOG, UNIT_NONE, 40, 2, 1, false, 0 }, + + { TYPE_INV, CH0, FLD_YD, UNIT_WH, CALC_TOTAL_YD, 0, CMD_CALC, false, 0 }, + { TYPE_INV, CH0, FLD_YT, UNIT_KWH, CALC_TOTAL_YT, 0, CMD_CALC, false, 3 }, + { TYPE_INV, CH0, FLD_PDC, UNIT_W, CALC_TOTAL_PDC, 0, CMD_CALC, false, 1 }, + { TYPE_INV, CH0, FLD_EFF, UNIT_PCT, CALC_TOTAL_EFF, 0, CMD_CALC, false, 3 } +}; + +HERF_2CH::HERF_2CH(HoymilesRadio* radio, const uint64_t serial) + : HM_Abstract(radio, serial) {}; + +bool HERF_2CH::isValidSerial(const uint64_t serial) +{ + // serial >= 0x282100000000 && serial <= 0x2821ffffffff + uint16_t preSerial = (serial >> 32) & 0xffff; + return preSerial == 0x2821; +} + +String HERF_2CH::typeName() const +{ + return "HERF-800-2T"; +} + +const byteAssign_t* HERF_2CH::getByteAssignment() const +{ + return byteAssignment; +} + +uint8_t HERF_2CH::getByteAssignmentSize() const +{ + return sizeof(byteAssignment) / sizeof(byteAssignment[0]); +} diff --git a/lib/Hoymiles/src/inverters/HERF_2CH.h b/lib/Hoymiles/src/inverters/HERF_2CH.h new file mode 100644 index 000000000..048ccb618 --- /dev/null +++ b/lib/Hoymiles/src/inverters/HERF_2CH.h @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "HM_Abstract.h" + +class HERF_2CH : public HM_Abstract { +public: + explicit HERF_2CH(HoymilesRadio* radio, const uint64_t serial); + static bool isValidSerial(const uint64_t serial); + String typeName() const; + const byteAssign_t* getByteAssignment() const; + uint8_t getByteAssignmentSize() const; +}; diff --git a/lib/Hoymiles/src/inverters/HERF_4CH.cpp b/lib/Hoymiles/src/inverters/HERF_4CH.cpp new file mode 100644 index 000000000..dcd01b6d5 --- /dev/null +++ b/lib/Hoymiles/src/inverters/HERF_4CH.cpp @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022-2024 Thomas Basler and others + */ +#include "HERF_4CH.h" + +HERF_4CH::HERF_4CH(HoymilesRadio* radio, const uint64_t serial) + : HM_4CH(radio, serial) {}; + +bool HERF_4CH::isValidSerial(const uint64_t serial) +{ + // serial >= 0x280100000000 && serial <= 0x2801ffffffff + uint16_t preSerial = (serial >> 32) & 0xffff; + return preSerial == 0x2801; +} + +String HERF_4CH::typeName() const +{ + return "HERF-1600/1800-4T"; +} diff --git a/lib/Hoymiles/src/inverters/HERF_4CH.h b/lib/Hoymiles/src/inverters/HERF_4CH.h new file mode 100644 index 000000000..70c1ad216 --- /dev/null +++ b/lib/Hoymiles/src/inverters/HERF_4CH.h @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "HM_4CH.h" + +class HERF_4CH : public HM_4CH { +public: + explicit HERF_4CH(HoymilesRadio* radio, const uint64_t serial); + static bool isValidSerial(const uint64_t serial); + String typeName() const; +}; diff --git a/lib/Hoymiles/src/inverters/HMS_1CH.cpp b/lib/Hoymiles/src/inverters/HMS_1CH.cpp index 5d906e58f..2c7e3857b 100644 --- a/lib/Hoymiles/src/inverters/HMS_1CH.cpp +++ b/lib/Hoymiles/src/inverters/HMS_1CH.cpp @@ -33,7 +33,7 @@ HMS_1CH::HMS_1CH(HoymilesRadio* radio, const uint64_t serial) bool HMS_1CH::isValidSerial(const uint64_t serial) { - // serial >= 0x112400000000 && serial <= 0x112499999999 + // serial >= 0x112400000000 && serial <= 0x1124ffffffff uint16_t preSerial = (serial >> 32) & 0xffff; return preSerial == 0x1124; } diff --git a/lib/Hoymiles/src/inverters/HMS_1CHv2.cpp b/lib/Hoymiles/src/inverters/HMS_1CHv2.cpp index 2cfaa28b0..d79d2c1d2 100644 --- a/lib/Hoymiles/src/inverters/HMS_1CHv2.cpp +++ b/lib/Hoymiles/src/inverters/HMS_1CHv2.cpp @@ -33,7 +33,7 @@ HMS_1CHv2::HMS_1CHv2(HoymilesRadio* radio, const uint64_t serial) bool HMS_1CHv2::isValidSerial(const uint64_t serial) { - // serial >= 0x112500000000 && serial <= 0x112599999999 + // serial >= 0x112500000000 && serial <= 0x1125ffffffff uint16_t preSerial = (serial >> 32) & 0xffff; return preSerial == 0x1125; } diff --git a/lib/Hoymiles/src/inverters/HMS_2CH.cpp b/lib/Hoymiles/src/inverters/HMS_2CH.cpp index 56c7fc69b..4a700a9a9 100644 --- a/lib/Hoymiles/src/inverters/HMS_2CH.cpp +++ b/lib/Hoymiles/src/inverters/HMS_2CH.cpp @@ -40,7 +40,7 @@ HMS_2CH::HMS_2CH(HoymilesRadio* radio, const uint64_t serial) bool HMS_2CH::isValidSerial(const uint64_t serial) { - // serial >= 0x114400000000 && serial <= 0x114499999999 + // serial >= 0x114400000000 && serial <= 0x1144ffffffff uint16_t preSerial = (serial >> 32) & 0xffff; return preSerial == 0x1144; } diff --git a/lib/Hoymiles/src/inverters/HMS_4CH.cpp b/lib/Hoymiles/src/inverters/HMS_4CH.cpp index 9aeaf1065..b3cf1f380 100644 --- a/lib/Hoymiles/src/inverters/HMS_4CH.cpp +++ b/lib/Hoymiles/src/inverters/HMS_4CH.cpp @@ -54,7 +54,7 @@ HMS_4CH::HMS_4CH(HoymilesRadio* radio, const uint64_t serial) bool HMS_4CH::isValidSerial(const uint64_t serial) { - // serial >= 0x116400000000 && serial <= 0x116499999999 + // serial >= 0x116400000000 && serial <= 0x1164ffffffff uint16_t preSerial = (serial >> 32) & 0xffff; return preSerial == 0x1164; } diff --git a/lib/Hoymiles/src/inverters/HMT_4CH.cpp b/lib/Hoymiles/src/inverters/HMT_4CH.cpp index d92a510f4..609e3350f 100644 --- a/lib/Hoymiles/src/inverters/HMT_4CH.cpp +++ b/lib/Hoymiles/src/inverters/HMT_4CH.cpp @@ -63,7 +63,7 @@ HMT_4CH::HMT_4CH(HoymilesRadio* radio, const uint64_t serial) bool HMT_4CH::isValidSerial(const uint64_t serial) { - // serial >= 0x136100000000 && serial <= 0x136199999999 + // serial >= 0x136100000000 && serial <= 0x1361ffffffff uint16_t preSerial = (serial >> 32) & 0xffff; return preSerial == 0x1361; } diff --git a/lib/Hoymiles/src/inverters/HMT_6CH.cpp b/lib/Hoymiles/src/inverters/HMT_6CH.cpp index 757cf91de..f8b9f4075 100644 --- a/lib/Hoymiles/src/inverters/HMT_6CH.cpp +++ b/lib/Hoymiles/src/inverters/HMT_6CH.cpp @@ -77,7 +77,7 @@ HMT_6CH::HMT_6CH(HoymilesRadio* radio, const uint64_t serial) bool HMT_6CH::isValidSerial(const uint64_t serial) { - // serial >= 0x138200000000 && serial <= 0x138299999999 + // serial >= 0x138200000000 && serial <= 0x1382ffffffff uint16_t preSerial = (serial >> 32) & 0xffff; return preSerial == 0x1382; } diff --git a/lib/Hoymiles/src/inverters/HM_1CH.cpp b/lib/Hoymiles/src/inverters/HM_1CH.cpp index 670b7dbe2..0f0c64c23 100644 --- a/lib/Hoymiles/src/inverters/HM_1CH.cpp +++ b/lib/Hoymiles/src/inverters/HM_1CH.cpp @@ -33,7 +33,7 @@ HM_1CH::HM_1CH(HoymilesRadio* radio, const uint64_t serial) bool HM_1CH::isValidSerial(const uint64_t serial) { - // serial >= 0x112100000000 && serial <= 0x112199999999 + // serial >= 0x112100000000 && serial <= 0x1121ffffffff uint8_t preId[2]; preId[0] = (uint8_t)(serial >> 40); diff --git a/lib/Hoymiles/src/inverters/HM_2CH.cpp b/lib/Hoymiles/src/inverters/HM_2CH.cpp index 6d9b7ca91..02dd8ae4f 100644 --- a/lib/Hoymiles/src/inverters/HM_2CH.cpp +++ b/lib/Hoymiles/src/inverters/HM_2CH.cpp @@ -41,7 +41,7 @@ HM_2CH::HM_2CH(HoymilesRadio* radio, const uint64_t serial) bool HM_2CH::isValidSerial(const uint64_t serial) { - // serial >= 0x114100000000 && serial <= 0x114199999999 + // serial >= 0x114100000000 && serial <= 0x1141ffffffff uint8_t preId[2]; preId[0] = (uint8_t)(serial >> 40); diff --git a/lib/Hoymiles/src/inverters/HM_4CH.cpp b/lib/Hoymiles/src/inverters/HM_4CH.cpp index 13ca061a9..586248b59 100644 --- a/lib/Hoymiles/src/inverters/HM_4CH.cpp +++ b/lib/Hoymiles/src/inverters/HM_4CH.cpp @@ -54,7 +54,7 @@ HM_4CH::HM_4CH(HoymilesRadio* radio, const uint64_t serial) bool HM_4CH::isValidSerial(const uint64_t serial) { - // serial >= 0x116100000000 && serial <= 0x116199999999 + // serial >= 0x116100000000 && serial <= 0x1161ffffffff uint8_t preId[2]; preId[0] = (uint8_t)(serial >> 40); diff --git a/lib/Hoymiles/src/inverters/README.md b/lib/Hoymiles/src/inverters/README.md index c080a7351..6d6104a20 100644 --- a/lib/Hoymiles/src/inverters/README.md +++ b/lib/Hoymiles/src/inverters/README.md @@ -11,3 +11,5 @@ | HMS_4CH | HMS-1600/1800/2000-4T | 1164 | | HMT_4CH | HMT-1600/1800/2000-4T | 1361 | | HMT_6CH | HMT-1800/2250-6T | 1382 | +| HERF_2CH | HERF 800 | 2821 | +| HERF_4CH | HERF 1800 | 2801 | diff --git a/lib/Hoymiles/src/parser/AlarmLogParser.cpp b/lib/Hoymiles/src/parser/AlarmLogParser.cpp index 4086f8e38..652159002 100644 --- a/lib/Hoymiles/src/parser/AlarmLogParser.cpp +++ b/lib/Hoymiles/src/parser/AlarmLogParser.cpp @@ -55,11 +55,12 @@ const std::array AlarmLogParser::_alarmMe { AlarmMessageType_t::ALL, 144, "Grid: Grid overfrequency", "Netz: Netzüberfrequenz", "Réseau: Surfréquence du réseau" }, { AlarmMessageType_t::ALL, 145, "Grid: Grid underfrequency", "Netz: Netzunterfrequenz", "Réseau: Sous-fréquence du réseau" }, { AlarmMessageType_t::ALL, 146, "Grid: Rapid grid frequency change rate", "Netz: Schnelle Wechselrate der Netzfrequenz", "Réseau: Taux de fluctuation rapide de la fréquence du réseau" }, - { AlarmMessageType_t::ALL, 147, "Grid: Power grid outage", "Netz: Eletrizitätsnetzausfall", "Réseau: Panne du réseau électrique" }, + { AlarmMessageType_t::ALL, 147, "Grid: Power grid outage", "Netz: Elektrizitätsnetzausfall", "Réseau: Panne du réseau électrique" }, { AlarmMessageType_t::ALL, 148, "Grid: Grid disconnection", "Netz: Netztrennung", "Réseau: Déconnexion du réseau" }, { AlarmMessageType_t::ALL, 149, "Grid: Island detected", "Netz: Inselbetrieb festgestellt", "Réseau: Détection d’îlots" }, { AlarmMessageType_t::ALL, 150, "DCI exceeded", "", "" }, + { AlarmMessageType_t::ALL, 152, "Grid: Phase angle difference between two phases exceeded 5° >10 times", "", "" }, { AlarmMessageType_t::HMT, 171, "Grid: Abnormal phase difference between phase to phase", "", "" }, { AlarmMessageType_t::ALL, 181, "Abnormal insulation impedance", "", "" }, { AlarmMessageType_t::ALL, 182, "Abnormal grounding", "", "" }, @@ -294,4 +295,4 @@ int AlarmLogParser::getTimezoneOffset() gmt = mktime(ptm); return static_cast(difftime(rawtime, gmt)); -} \ No newline at end of file +} diff --git a/lib/Hoymiles/src/parser/AlarmLogParser.h b/lib/Hoymiles/src/parser/AlarmLogParser.h index a6f0c10c6..87413ce7a 100644 --- a/lib/Hoymiles/src/parser/AlarmLogParser.h +++ b/lib/Hoymiles/src/parser/AlarmLogParser.h @@ -8,7 +8,7 @@ #define ALARM_LOG_ENTRY_SIZE 12 #define ALARM_LOG_PAYLOAD_SIZE (ALARM_LOG_ENTRY_COUNT * ALARM_LOG_ENTRY_SIZE + 4) -#define ALARM_MSG_COUNT 130 +#define ALARM_MSG_COUNT 131 struct AlarmLogEntry_t { uint16_t MessageId; @@ -62,4 +62,4 @@ class AlarmLogParser : public Parser { AlarmMessageType_t _messageType = AlarmMessageType_t::ALL; 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 b2b30a2e8..26e3c9d4f 100644 --- a/lib/Hoymiles/src/parser/DevInfoParser.cpp +++ b/lib/Hoymiles/src/parser/DevInfoParser.cpp @@ -52,7 +52,11 @@ const devInfo_t devInfo[] = { { { 0x10, 0x32, 0x71, ALL }, 2000, "HMT-2000-4T" }, // 0 { { 0x10, 0x33, 0x11, ALL }, 1800, "HMT-1800-6T" }, // 01 - { { 0x10, 0x33, 0x31, ALL }, 2250, "HMT-2250-6T" } // 01 + { { 0x10, 0x33, 0x31, ALL }, 2250, "HMT-2250-6T" }, // 01 + + { { 0xF1, 0x01, 0x14, ALL }, 800, "HERF-800" }, // 00 + { { 0xF1, 0x01, 0x24, ALL }, 1600, "HERF-1600" }, // 00 + { { 0xF1, 0x01, 0x22, ALL }, 1800, "HERF-1800" }, // 00 }; DevInfoParser::DevInfoParser() @@ -200,7 +204,7 @@ bool DevInfoParser::containsValidData() const struct tm info; localtime_r(&t, &info); - return info.tm_year > (2016 - 1900); + return info.tm_year > (2016 - 1900) && getHwPartNumber() != 124097; } uint8_t DevInfoParser::getDevIdx() const diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp index 8047fd125..64c1e5df0 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp +++ b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp @@ -281,13 +281,7 @@ int VeDirectFrameHandler::hexRxEvent(uint8_t inbyte) { } bool VeDirectFrameHandler::isDataValid(veStruct const& frame) const { - if (_lastUpdate == 0) { - return false; - } - if (strlen(frame.SER) == 0) { - return false; - } - return true; + return strlen(frame.SER) > 0 && _lastUpdate > 0 && (millis() - _lastUpdate) < (10 * 1000); } uint32_t VeDirectFrameHandler::getLastUpdate() const diff --git a/lib/VeDirectFrameHandler/VeDirectMpptController.cpp b/lib/VeDirectFrameHandler/VeDirectMpptController.cpp index 5b8d6afd7..4112510f4 100644 --- a/lib/VeDirectFrameHandler/VeDirectMpptController.cpp +++ b/lib/VeDirectFrameHandler/VeDirectMpptController.cpp @@ -1,9 +1,9 @@ #include #include "VeDirectMpptController.h" -void VeDirectMpptController::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging) +void VeDirectMpptController::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort) { - VeDirectFrameHandler::init(rx, tx, msgOut, verboseLogging, 1); + VeDirectFrameHandler::init(rx, tx, msgOut, verboseLogging, hwSerialPort); _spData = std::make_shared(); if (_verboseLogging) { _msgOut->println("Finished init MPPTController"); } } diff --git a/lib/VeDirectFrameHandler/VeDirectMpptController.h b/lib/VeDirectFrameHandler/VeDirectMpptController.h index 158772373..08574252d 100644 --- a/lib/VeDirectFrameHandler/VeDirectMpptController.h +++ b/lib/VeDirectFrameHandler/VeDirectMpptController.h @@ -39,7 +39,7 @@ class VeDirectMpptController : public VeDirectFrameHandler { public: VeDirectMpptController() = default; - void init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging); + void init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort); bool isDataValid() const; // return true if data valid and not outdated struct veMpptStruct : veStruct { @@ -49,7 +49,7 @@ class VeDirectMpptController : public VeDirectFrameHandler { double VPV; // panel voltage in V double IPV; // panel current in A (calculated) bool LOAD; // virtual load output state (on if battery voltage reaches upper limit, off if battery reaches lower limit) - uint8_t CS; // current state of operation e. g. OFF or Bulk + uint8_t CS; // current state of operation e.g. OFF or Bulk uint8_t ERR; // error code uint32_t OR; // off reason uint32_t HSDS; // day sequence number 1...365 diff --git a/platformio.ini b/platformio.ini index adf7eb879..ce9998ae4 100644 --- a/platformio.ini +++ b/platformio.ini @@ -1,263 +1,263 @@ -; PlatformIO Project Configuration File -; -; Build options: build flags, source filter -; Upload options: custom upload port, speed and extra flags -; Library options: dependencies, extra library storages -; Advanced options: extra scripting -; -; Please visit documentation for the other options and examples -; https://docs.platformio.org/page/projectconf.html - -[platformio] -default_envs = generic_esp32 -extra_configs = - platformio_override.ini - -[env] -; Make sure to NOT add any spaces in the custom_ci_action property -; (also the position in the file is important) -custom_ci_action = generic,generic_esp32,generic_esp32s3,generic_esp32s3_usb - -framework = arduino -platform = espressif32@6.5.0 - -build_flags = - -DPIOENV=\"$PIOENV\" - -D_TASK_STD_FUNCTION=1 - -D_TASK_THREAD_SAFE=1 - -Wall -Wextra -Wunused -Wmisleading-indentation -Wduplicated-cond -Wlogical-op -Wnull-dereference -; Have to remove -Werror because of -; https://github.com/espressif/arduino-esp32/issues/9044 and -; https://github.com/espressif/arduino-esp32/issues/9045 -; -Werror - -std=c++17 - -std=gnu++17 -build_unflags = - -std=gnu++11 - -lib_deps = - mathieucarbou/ESP Async WebServer @ 2.7.0 - bblanchon/ArduinoJson @ ^6.21.5 - https://github.com/bertmelis/espMqttClient.git#v1.6.0 - nrf24/RF24 @ ^1.4.8 - olikraus/U8g2 @ ^2.35.9 - buelowp/sunset @ ^1.1.7 - https://github.com/arkhipenko/TaskScheduler#testing - https://github.com/coryjfowler/MCP_CAN_lib - plerup/EspSoftwareSerial @ ^8.0.1 - https://github.com/dok-net/ghostl @ ^1.0.1 - mobizt/FirebaseJson @ ^3.0.6 - rweather/Crypto@^0.4.0 - -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_4mb.csv -board_build.filesystem = littlefs -board_build.embed_files = - webapp_dist/index.html.gz - webapp_dist/zones.json.gz - webapp_dist/favicon.ico - webapp_dist/favicon.png - webapp_dist/js/app.js.gz - webapp_dist/site.webmanifest - -custom_patches = - -monitor_filters = esp32_exception_decoder, time, log2file, colorize -monitor_speed = 115200 -upload_protocol = esptool - -; Specify port in platformio_override.ini. Comment out (add ; in front of line) to use auto detection. -; monitor_port = COM4 -; upload_port = COM4 - - -[env:generic_esp32] -board = esp32dev -build_flags = ${env.build_flags} - - -[env:generic_esp32_16mb_psram] -board = esp32dev -board_build.flash_mode = qio -board_build.partitions = partitions_custom_16mb.csv -board_upload.flash_size = 16MB -build_flags = ${env.build_flags} - -DBOARD_HAS_PSRAM - -mfix-esp32-psram-cache-issue - - -[env:generic_esp32c3] -board = esp32-c3-devkitc-02 -custom_patches = ${env.custom_patches},esp32c3 -build_flags = ${env.build_flags} - - -[env:generic_esp32c3_usb] -board = esp32-c3-devkitc-02 -custom_patches = ${env.custom_patches},esp32c3 -build_flags = ${env.build_flags} - -DARDUINO_USB_MODE=1 - -DARDUINO_USB_CDC_ON_BOOT=1 - - -[env:generic_esp32s3] -board = esp32-s3-devkitc-1 -build_flags = ${env.build_flags} - - -[env:generic_esp32s3_usb] -board = esp32-s3-devkitc-1 -upload_protocol = esp-builtin -build_flags = ${env.build_flags} - -DARDUINO_USB_MODE=1 - -DARDUINO_USB_CDC_ON_BOOT=1 - - -[env:generic] -board = esp32dev -build_flags = ${env.build_flags} - -DHOYMILES_PIN_MISO=19 - -DHOYMILES_PIN_MOSI=23 - -DHOYMILES_PIN_SCLK=18 - -DHOYMILES_PIN_IRQ=16 - -DHOYMILES_PIN_CE=4 - -DHOYMILES_PIN_CS=5 - - -[env:olimex_esp32_poe] -; https://www.olimex.com/Products/IoT/ESP32/ESP32-POE/open-source-hardware -board = esp32-poe -build_flags = ${env.build_flags} - -DHOYMILES_PIN_MISO=15 - -DHOYMILES_PIN_MOSI=2 - -DHOYMILES_PIN_SCLK=14 - -DHOYMILES_PIN_IRQ=13 - -DHOYMILES_PIN_CE=16 - -DHOYMILES_PIN_CS=5 - -DOPENDTU_ETHERNET - - -[env:olimex_esp32_evb] -; https://www.olimex.com/Products/IoT/ESP32/ESP32-EVB/open-source-hardware -board = esp32-evb -build_flags = ${env.build_flags} - -DHOYMILES_PIN_MISO=15 - -DHOYMILES_PIN_MOSI=2 - -DHOYMILES_PIN_SCLK=14 - -DHOYMILES_PIN_IRQ=13 - -DHOYMILES_PIN_CE=16 - -DHOYMILES_PIN_CS=17 - -DOPENDTU_ETHERNET - - -[env:d1_mini_esp32] -board = wemos_d1_mini32 -build_flags = - ${env.build_flags} - -DHOYMILES_PIN_MISO=19 - -DHOYMILES_PIN_MOSI=23 - -DHOYMILES_PIN_SCLK=18 - -DHOYMILES_PIN_IRQ=16 - -DHOYMILES_PIN_CE=17 - -DHOYMILES_PIN_CS=5 - -DVICTRON_PIN_TX=21 - -DVICTRON_PIN_RX=22 - -DPYLONTECH_PIN_RX=27 - -DPYLONTECH_PIN_TX=14 - -DHUAWEI_PIN_MISO=12 - -DHUAWEI_PIN_MOSI=13 - -DHUAWEI_PIN_SCLK=26 - -DHUAWEI_PIN_IRQ=25 - -DHUAWEI_PIN_CS=15 - -DHUAWEI_PIN_POWER=33 - -[env:wt32_eth01] -; http://www.wireless-tag.com/portfolio/wt32-eth01/ -board = wt32-eth01 -build_flags = ${env.build_flags} - -DHOYMILES_PIN_MISO=4 - -DHOYMILES_PIN_MOSI=2 - -DHOYMILES_PIN_SCLK=32 - -DHOYMILES_PIN_IRQ=33 - -DHOYMILES_PIN_CE=14 - -DHOYMILES_PIN_CS=15 - -DOPENDTU_ETHERNET - - -[env:esp_s3_12k_kit] -; https://www.waveshare.com/wiki/NodeMCU-ESP-S3-12K-Kit -board = esp32-s3-devkitc-1 -build_flags = ${env.build_flags} - -DHOYMILES_PIN_MISO=16 - -DHOYMILES_PIN_MOSI=17 - -DHOYMILES_PIN_SCLK=18 - -DHOYMILES_PIN_IRQ=3 - -DHOYMILES_PIN_CE=4 - -DHOYMILES_PIN_CS=5 - - -[env:lolin32_lite] -; https://www.makershop.de/plattformen/esp8266/wemos-lolin32/ -; https://www.az-delivery.de/products/esp32-lolin-lolin32 -board = lolin32_lite -build_flags = ${env.build_flags} - -DHOYMILES_PIN_MISO=19 - -DHOYMILES_PIN_MOSI=23 - -DHOYMILES_PIN_SCLK=18 - -DHOYMILES_PIN_IRQ=16 - -DHOYMILES_PIN_CE=17 - -DHOYMILES_PIN_CS=5 - -[env:lolin_s2_mini] -board = lolin_s2_mini -build_flags = ${env.build_flags} - -DHOYMILES_PIN_MISO=13 - -DHOYMILES_PIN_MOSI=11 - -DHOYMILES_PIN_SCLK=12 - -DHOYMILES_PIN_CS=10 - -DHOYMILES_PIN_IRQ=4 - -DHOYMILES_PIN_CE=5 - - -[env:opendtufusionv1] -board = esp32-s3-devkitc-1 -upload_protocol = esp-builtin -debug_tool = esp-builtin -debug_speed = 12000 -build_flags = ${env.build_flags} - -DHOYMILES_PIN_MISO=48 - -DHOYMILES_PIN_MOSI=35 - -DHOYMILES_PIN_SCLK=36 - -DHOYMILES_PIN_IRQ=47 - -DHOYMILES_PIN_CE=38 - -DHOYMILES_PIN_CS=37 - -DLED0=17 - -DLED1=18 - -DARDUINO_USB_MODE=1 - -[env:opendtufusionv2] -board = esp32-s3-devkitc-1 -upload_protocol = esp-builtin -debug_tool = esp-builtin -debug_speed = 12000 -build_flags = ${env.build_flags} - -DHOYMILES_PIN_MISO=48 - -DHOYMILES_PIN_MOSI=35 - -DHOYMILES_PIN_SCLK=36 - -DHOYMILES_PIN_IRQ=47 - -DHOYMILES_PIN_CE=38 - -DHOYMILES_PIN_CS=37 - -DLED0=17 - -DLED1=18 - -DCMT_CLK=6 - -DCMT_CS=4 - -DCMT_FCS=21 - -DCMT_GPIO2=3 - -DCMT_GPIO3=8 - -DCMT_SDIO=5 - -DARDUINO_USB_MODE=1 - -DARDUINO_USB_CDC_ON_BOOT=1 +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[platformio] +default_envs = generic_esp32 +extra_configs = + platformio_override.ini + +[env] +; Make sure to NOT add any spaces in the custom_ci_action property +; (also the position in the file is important) +custom_ci_action = generic,generic_esp32,generic_esp32s3,generic_esp32s3_usb + +framework = arduino +platform = espressif32@6.5.0 + +build_flags = + -DPIOENV=\"$PIOENV\" + -D_TASK_STD_FUNCTION=1 + -D_TASK_THREAD_SAFE=1 + -Wall -Wextra -Wunused -Wmisleading-indentation -Wduplicated-cond -Wlogical-op -Wnull-dereference +; Have to remove -Werror because of +; https://github.com/espressif/arduino-esp32/issues/9044 and +; https://github.com/espressif/arduino-esp32/issues/9045 +; -Werror + -std=c++17 + -std=gnu++17 +build_unflags = + -std=gnu++11 + +lib_deps = + mathieucarbou/ESP Async WebServer @ 2.8.1 + bblanchon/ArduinoJson @ ^6.21.5 + https://github.com/bertmelis/espMqttClient.git#v1.6.0 + nrf24/RF24 @ ^1.4.8 + olikraus/U8g2 @ ^2.35.15 + buelowp/sunset @ ^1.1.7 + https://github.com/arkhipenko/TaskScheduler#testing + https://github.com/coryjfowler/MCP_CAN_lib + plerup/EspSoftwareSerial @ ^8.0.1 + https://github.com/dok-net/ghostl @ ^1.0.1 + mobizt/FirebaseJson @ ^3.0.6 + rweather/Crypto@^0.4.0 + +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_4mb.csv +board_build.filesystem = littlefs +board_build.embed_files = + webapp_dist/index.html.gz + webapp_dist/zones.json.gz + webapp_dist/favicon.ico + webapp_dist/favicon.png + webapp_dist/js/app.js.gz + webapp_dist/site.webmanifest + +custom_patches = + +monitor_filters = esp32_exception_decoder, time, log2file, colorize +monitor_speed = 115200 +upload_protocol = esptool + +; Specify port in platformio_override.ini. Comment out (add ; in front of line) to use auto detection. +; monitor_port = COM4 +; upload_port = COM4 + + +[env:generic_esp32] +board = esp32dev +build_flags = ${env.build_flags} + + +[env:generic_esp32_16mb_psram] +board = esp32dev +board_build.flash_mode = qio +board_build.partitions = partitions_custom_16mb.csv +board_upload.flash_size = 16MB +build_flags = ${env.build_flags} + -DBOARD_HAS_PSRAM + -mfix-esp32-psram-cache-issue + + +[env:generic_esp32c3] +board = esp32-c3-devkitc-02 +custom_patches = ${env.custom_patches},esp32c3 +build_flags = ${env.build_flags} + + +[env:generic_esp32c3_usb] +board = esp32-c3-devkitc-02 +custom_patches = ${env.custom_patches},esp32c3 +build_flags = ${env.build_flags} + -DARDUINO_USB_MODE=1 + -DARDUINO_USB_CDC_ON_BOOT=1 + + +[env:generic_esp32s3] +board = esp32-s3-devkitc-1 +build_flags = ${env.build_flags} + + +[env:generic_esp32s3_usb] +board = esp32-s3-devkitc-1 +upload_protocol = esp-builtin +build_flags = ${env.build_flags} + -DARDUINO_USB_MODE=1 + -DARDUINO_USB_CDC_ON_BOOT=1 + + +[env:generic] +board = esp32dev +build_flags = ${env.build_flags} + -DHOYMILES_PIN_MISO=19 + -DHOYMILES_PIN_MOSI=23 + -DHOYMILES_PIN_SCLK=18 + -DHOYMILES_PIN_IRQ=16 + -DHOYMILES_PIN_CE=4 + -DHOYMILES_PIN_CS=5 + + +[env:olimex_esp32_poe] +; https://www.olimex.com/Products/IoT/ESP32/ESP32-POE/open-source-hardware +board = esp32-poe +build_flags = ${env.build_flags} + -DHOYMILES_PIN_MISO=15 + -DHOYMILES_PIN_MOSI=2 + -DHOYMILES_PIN_SCLK=14 + -DHOYMILES_PIN_IRQ=13 + -DHOYMILES_PIN_CE=16 + -DHOYMILES_PIN_CS=5 + -DOPENDTU_ETHERNET + + +[env:olimex_esp32_evb] +; https://www.olimex.com/Products/IoT/ESP32/ESP32-EVB/open-source-hardware +board = esp32-evb +build_flags = ${env.build_flags} + -DHOYMILES_PIN_MISO=15 + -DHOYMILES_PIN_MOSI=2 + -DHOYMILES_PIN_SCLK=14 + -DHOYMILES_PIN_IRQ=13 + -DHOYMILES_PIN_CE=16 + -DHOYMILES_PIN_CS=17 + -DOPENDTU_ETHERNET + + +[env:d1_mini_esp32] +board = wemos_d1_mini32 +build_flags = + ${env.build_flags} + -DHOYMILES_PIN_MISO=19 + -DHOYMILES_PIN_MOSI=23 + -DHOYMILES_PIN_SCLK=18 + -DHOYMILES_PIN_IRQ=16 + -DHOYMILES_PIN_CE=17 + -DHOYMILES_PIN_CS=5 + -DVICTRON_PIN_TX=21 + -DVICTRON_PIN_RX=22 + -DPYLONTECH_PIN_RX=27 + -DPYLONTECH_PIN_TX=14 + -DHUAWEI_PIN_MISO=12 + -DHUAWEI_PIN_MOSI=13 + -DHUAWEI_PIN_SCLK=26 + -DHUAWEI_PIN_IRQ=25 + -DHUAWEI_PIN_CS=15 + -DHUAWEI_PIN_POWER=33 + +[env:wt32_eth01] +; http://www.wireless-tag.com/portfolio/wt32-eth01/ +board = wt32-eth01 +build_flags = ${env.build_flags} + -DHOYMILES_PIN_MISO=4 + -DHOYMILES_PIN_MOSI=2 + -DHOYMILES_PIN_SCLK=32 + -DHOYMILES_PIN_IRQ=33 + -DHOYMILES_PIN_CE=14 + -DHOYMILES_PIN_CS=15 + -DOPENDTU_ETHERNET + + +[env:esp_s3_12k_kit] +; https://www.waveshare.com/wiki/NodeMCU-ESP-S3-12K-Kit +board = esp32-s3-devkitc-1 +build_flags = ${env.build_flags} + -DHOYMILES_PIN_MISO=16 + -DHOYMILES_PIN_MOSI=17 + -DHOYMILES_PIN_SCLK=18 + -DHOYMILES_PIN_IRQ=3 + -DHOYMILES_PIN_CE=4 + -DHOYMILES_PIN_CS=5 + + +[env:lolin32_lite] +; https://www.makershop.de/plattformen/esp8266/wemos-lolin32/ +; https://www.az-delivery.de/products/esp32-lolin-lolin32 +board = lolin32_lite +build_flags = ${env.build_flags} + -DHOYMILES_PIN_MISO=19 + -DHOYMILES_PIN_MOSI=23 + -DHOYMILES_PIN_SCLK=18 + -DHOYMILES_PIN_IRQ=16 + -DHOYMILES_PIN_CE=17 + -DHOYMILES_PIN_CS=5 + +[env:lolin_s2_mini] +board = lolin_s2_mini +build_flags = ${env.build_flags} + -DHOYMILES_PIN_MISO=13 + -DHOYMILES_PIN_MOSI=11 + -DHOYMILES_PIN_SCLK=12 + -DHOYMILES_PIN_CS=10 + -DHOYMILES_PIN_IRQ=4 + -DHOYMILES_PIN_CE=5 + + +[env:opendtufusionv1] +board = esp32-s3-devkitc-1 +upload_protocol = esp-builtin +debug_tool = esp-builtin +debug_speed = 12000 +build_flags = ${env.build_flags} + -DHOYMILES_PIN_MISO=48 + -DHOYMILES_PIN_MOSI=35 + -DHOYMILES_PIN_SCLK=36 + -DHOYMILES_PIN_IRQ=47 + -DHOYMILES_PIN_CE=38 + -DHOYMILES_PIN_CS=37 + -DLED0=17 + -DLED1=18 + -DARDUINO_USB_MODE=1 + +[env:opendtufusionv2] +board = esp32-s3-devkitc-1 +upload_protocol = esp-builtin +debug_tool = esp-builtin +debug_speed = 12000 +build_flags = ${env.build_flags} + -DHOYMILES_PIN_MISO=48 + -DHOYMILES_PIN_MOSI=35 + -DHOYMILES_PIN_SCLK=36 + -DHOYMILES_PIN_IRQ=47 + -DHOYMILES_PIN_CE=38 + -DHOYMILES_PIN_CS=37 + -DLED0=17 + -DLED1=18 + -DCMT_CLK=6 + -DCMT_CS=4 + -DCMT_FCS=21 + -DCMT_GPIO2=3 + -DCMT_GPIO3=8 + -DCMT_SDIO=5 + -DARDUINO_USB_MODE=1 + -DARDUINO_USB_CDC_ON_BOOT=1 diff --git a/src/Battery.cpp b/src/Battery.cpp index 381fdc952..de05b03db 100644 --- a/src/Battery.cpp +++ b/src/Battery.cpp @@ -5,6 +5,7 @@ #include "JkBmsController.h" #include "VictronSmartShunt.h" #include "MqttBattery.h" +#include "SerialPortManager.h" BatteryClass Battery; @@ -38,6 +39,7 @@ void BatteryClass::updateSettings() _upProvider->deinit(); _upProvider = nullptr; } + SerialPortManager.invalidateBatteryPort(); CONFIG_T& config = Configuration.get(); if (!config.Battery.Enabled) { return; } @@ -47,23 +49,32 @@ void BatteryClass::updateSettings() switch (config.Battery.Provider) { case 0: _upProvider = std::make_unique(); - if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; } break; case 1: _upProvider = std::make_unique(); - if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; } break; case 2: _upProvider = std::make_unique(); - if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; } break; case 3: _upProvider = std::make_unique(); - if (!_upProvider->init(verboseLogging)) { _upProvider = nullptr; } break; default: MessageOutput.printf("Unknown battery provider: %d\r\n", config.Battery.Provider); - break; + return; + } + + if(_upProvider->usesHwPort2()) { + if (!SerialPortManager.allocateBatteryPort(2)) { + MessageOutput.printf("[Battery] Serial port %d already in use. Initialization aborted!\r\n", 2); + _upProvider = nullptr; + return; + } + } + + if (!_upProvider->init(verboseLogging)) { + SerialPortManager.invalidateBatteryPort(); + _upProvider = nullptr; } } diff --git a/src/BatteryStats.cpp b/src/BatteryStats.cpp index 48d089165..563562c82 100644 --- a/src/BatteryStats.cpp +++ b/src/BatteryStats.cpp @@ -53,6 +53,8 @@ static void addLiveViewAlarm(JsonVariant& root, std::string const& name, bool BatteryStats::updateAvailable(uint32_t since) const { + if (_lastUpdate == 0) { return false; } // no data at all processed yet + auto constexpr halfOfAllMillis = std::numeric_limits::max() / 2; return (_lastUpdate - since) < halfOfAllMillis; } @@ -379,12 +381,14 @@ void VictronSmartShuntStats::updateFrom(VeDirectShuntController::veShuntStruct c _modelName = shuntData.getPidAsString().data(); _chargeCycles = shuntData.H4; _timeToGo = shuntData.TTG / 60; - _chargedEnergy = shuntData.H18 / 100; - _dischargedEnergy = shuntData.H17 / 100; + _chargedEnergy = static_cast(shuntData.H18) / 100; + _dischargedEnergy = static_cast(shuntData.H17) / 100; _manufacturer = "Victron " + _modelName; _temperature = shuntData.T; _tempPresent = shuntData.tempPresent; - + _instantaneousPower = shuntData.P; + _consumedAmpHours = static_cast(shuntData.CE) / 1000; + _lastFullCharge = shuntData.H9 / 60; // shuntData.AR is a bitfield, so we need to check each bit individually _alarmLowVoltage = shuntData.AR & 1; _alarmHighVoltage = shuntData.AR & 2; @@ -401,8 +405,11 @@ void VictronSmartShuntStats::getLiveViewData(JsonVariant& root) const { // values go into the "Status" card of the web application addLiveViewValue(root, "current", _current, "A", 1); addLiveViewValue(root, "chargeCycles", _chargeCycles, "", 0); - addLiveViewValue(root, "chargedEnergy", _chargedEnergy, "KWh", 1); - addLiveViewValue(root, "dischargedEnergy", _dischargedEnergy, "KWh", 1); + addLiveViewValue(root, "chargedEnergy", _chargedEnergy, "kWh", 2); + addLiveViewValue(root, "dischargedEnergy", _dischargedEnergy, "kWh", 2); + addLiveViewValue(root, "instantaneousPower", _instantaneousPower, "W", 0); + addLiveViewValue(root, "consumedAmpHours", _consumedAmpHours, "Ah", 3); + addLiveViewValue(root, "lastFullCharge", _lastFullCharge, "min", 0); if (_tempPresent) { addLiveViewValue(root, "temperature", _temperature, "°C", 0); } @@ -421,4 +428,7 @@ void VictronSmartShuntStats::mqttPublish() const { MqttSettings.publish(F("battery/chargeCycles"), String(_chargeCycles)); MqttSettings.publish(F("battery/chargedEnergy"), String(_chargedEnergy)); MqttSettings.publish(F("battery/dischargedEnergy"), String(_dischargedEnergy)); + MqttSettings.publish(F("battery/instantaneousPower"), String(_instantaneousPower)); + MqttSettings.publish(F("battery/consumedAmpHours"), String(_consumedAmpHours)); + MqttSettings.publish(F("battery/lastFullCharge"), String(_lastFullCharge)); } diff --git a/src/Configuration.cpp b/src/Configuration.cpp index 441d2dac9..c00617cf7 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -182,9 +182,10 @@ bool ConfigurationClass::write() powerlimiter["verbose_logging"] = config.PowerLimiter.VerboseLogging; powerlimiter["solar_passtrough_enabled"] = config.PowerLimiter.SolarPassThroughEnabled; powerlimiter["solar_passtrough_losses"] = config.PowerLimiter.SolarPassThroughLosses; - powerlimiter["battery_drain_strategy"] = config.PowerLimiter.BatteryDrainStategy; + powerlimiter["battery_always_use_at_night"] = config.PowerLimiter.BatteryAlwaysUseAtNight; powerlimiter["interval"] = config.PowerLimiter.Interval; powerlimiter["is_inverter_behind_powermeter"] = config.PowerLimiter.IsInverterBehindPowerMeter; + powerlimiter["is_inverter_solar_powered"] = config.PowerLimiter.IsInverterSolarPowered; powerlimiter["inverter_id"] = config.PowerLimiter.InverterId; powerlimiter["inverter_channel_id"] = config.PowerLimiter.InverterChannelId; powerlimiter["target_power_consumption"] = config.PowerLimiter.TargetPowerConsumption; @@ -428,9 +429,11 @@ bool ConfigurationClass::read() config.PowerLimiter.VerboseLogging = powerlimiter["verbose_logging"] | VERBOSE_LOGGING; config.PowerLimiter.SolarPassThroughEnabled = powerlimiter["solar_passtrough_enabled"] | POWERLIMITER_SOLAR_PASSTHROUGH_ENABLED; config.PowerLimiter.SolarPassThroughLosses = powerlimiter["solar_passthrough_losses"] | POWERLIMITER_SOLAR_PASSTHROUGH_LOSSES; - config.PowerLimiter.BatteryDrainStategy = powerlimiter["battery_drain_strategy"] | POWERLIMITER_BATTERY_DRAIN_STRATEGY; + config.PowerLimiter.BatteryAlwaysUseAtNight = powerlimiter["battery_always_use_at_night"] | POWERLIMITER_BATTERY_ALWAYS_USE_AT_NIGHT; + if (powerlimiter["battery_drain_strategy"].as() == 1) { config.PowerLimiter.BatteryAlwaysUseAtNight = true; } // convert legacy setting config.PowerLimiter.Interval = powerlimiter["interval"] | POWERLIMITER_INTERVAL; config.PowerLimiter.IsInverterBehindPowerMeter = powerlimiter["is_inverter_behind_powermeter"] | POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER; + config.PowerLimiter.IsInverterSolarPowered = powerlimiter["is_inverter_solar_powered"] | POWERLIMITER_IS_INVERTER_SOLAR_POWERED; config.PowerLimiter.InverterId = powerlimiter["inverter_id"] | POWERLIMITER_INVERTER_ID; config.PowerLimiter.InverterChannelId = powerlimiter["inverter_channel_id"] | POWERLIMITER_INVERTER_CHANNEL_ID; config.PowerLimiter.TargetPowerConsumption = powerlimiter["target_power_consumption"] | POWERLIMITER_TARGET_POWER_CONSUMPTION; @@ -560,4 +563,26 @@ INVERTER_CONFIG_T* ConfigurationClass::getInverterConfig(const uint64_t serial) return nullptr; } +void ConfigurationClass::deleteInverterById(const uint8_t id) +{ + config.Inverter[id].Serial = 0ULL; + strlcpy(config.Inverter[id].Name, "", sizeof(config.Inverter[id].Name)); + config.Inverter[id].Order = 0; + + config.Inverter[id].Poll_Enable = true; + config.Inverter[id].Poll_Enable_Night = true; + config.Inverter[id].Command_Enable = true; + config.Inverter[id].Command_Enable_Night = true; + config.Inverter[id].ReachableThreshold = REACHABLE_THRESHOLD; + config.Inverter[id].ZeroRuntimeDataIfUnrechable = false; + config.Inverter[id].ZeroYieldDayOnMidnight = false; + config.Inverter[id].YieldDayCorrection = false; + + for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { + config.Inverter[id].channel[c].MaxChannelPower = 0; + config.Inverter[id].channel[c].YieldTotalOffset = 0.0f; + strlcpy(config.Inverter[id].channel[c].Name, "", sizeof(config.Inverter[id].channel[c].Name)); + } +} + ConfigurationClass Configuration; diff --git a/src/Display_Graphic.cpp b/src/Display_Graphic.cpp index 98d46eea1..1b08aff82 100644 --- a/src/Display_Graphic.cpp +++ b/src/Display_Graphic.cpp @@ -31,13 +31,19 @@ const uint8_t languages[] = { }; static const char* const i18n_offline[] = { "Offline", "Offline", "Offline" }; + static const char* const i18n_current_power_w[] = { "%.0f W", "%.0f W", "%.0f W" }; static const char* const i18n_current_power_kw[] = { "%.1f kW", "%.1f kW", "%.1f kW" }; + static const char* const i18n_meter_power_w[] = { "grid: %.0f W", "Netz: %.0f W", "reseau: %.0f W" }; static const char* const i18n_meter_power_kw[] = { "grid: %.1f kW", "Netz: %.1f kW", "reseau: %.1f kW" }; + static const char* const i18n_yield_today_wh[] = { "today: %4.0f Wh", "Heute: %4.0f Wh", "auj.: %4.0f Wh" }; +static const char* const i18n_yield_today_kwh[] = { "today: %.1f kWh", "Heute: %.1f kWh", "auj.: %.1f kWh" }; + static const char* const i18n_yield_total_kwh[] = { "total: %.1f kWh", "Ges.: %.1f kWh", "total: %.1f kWh" }; static const char* const i18n_yield_total_mwh[] = { "total: %.0f kWh", "Ges.: %.0f kWh", "total: %.0f kWh" }; + static const char* const i18n_date_format[] = { "%m/%d/%Y %H:%M", "%d.%m.%Y %H:%M", "%d/%m/%Y %H:%M" }; DisplayGraphicClass::DisplayGraphicClass() @@ -133,6 +139,10 @@ void DisplayGraphicClass::printText(const char* text, const uint8_t line) offset -= (_isLarge ? 5 : 0); // oscillate around center on large screens dispX += offset; } + + if (dispX > _display->getDisplayWidth()) { + dispX = 0; + } _display->drawStr(dispX, _lineOffsets[line], text); } @@ -241,15 +251,20 @@ void DisplayGraphicClass::loop() //<======================= if (showText) { - //=====> Today & Total Production ======= - snprintf(_fmtText, sizeof(_fmtText), i18n_yield_today_wh[_display_language], Datastore.getTotalAcYieldDayEnabled()); + // Daily production + float wattsToday = Datastore.getTotalAcYieldDayEnabled(); + if (wattsToday >= 10000) { + snprintf(_fmtText, sizeof(_fmtText), i18n_yield_today_kwh[_display_language], wattsToday / 1000); + } else { + snprintf(_fmtText, sizeof(_fmtText), i18n_yield_today_wh[_display_language], wattsToday); + } printText(_fmtText, 1); - const float watts = Datastore.getTotalAcYieldTotalEnabled(); - auto const format = (watts >= 1000) ? i18n_yield_total_mwh : i18n_yield_total_kwh; - snprintf(_fmtText, sizeof(_fmtText), format[_display_language], watts); + // Total production + const float wattsTotal = Datastore.getTotalAcYieldTotalEnabled(); + auto const format = (wattsTotal >= 1000) ? i18n_yield_total_mwh : i18n_yield_total_kwh; + snprintf(_fmtText, sizeof(_fmtText), format[_display_language], wattsTotal); printText(_fmtText, 2); - //<======================= //=====> IP or Date-Time ======== // Change every 3 seconds diff --git a/src/HttpPowerMeter.cpp b/src/HttpPowerMeter.cpp index 4d6c12f4d..cb829e1d7 100644 --- a/src/HttpPowerMeter.cpp +++ b/src/HttpPowerMeter.cpp @@ -21,32 +21,39 @@ float HttpPowerMeterClass::getPower(int8_t phase) bool HttpPowerMeterClass::updateValues() { - const CONFIG_T& config = Configuration.get(); + const CONFIG_T& config = Configuration.get(); for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) { POWERMETER_HTTP_PHASE_CONFIG_T phaseConfig = config.PowerMeter.Http_Phase[i]; if (!phaseConfig.Enabled) { power[i] = 0.0; - continue; - } + continue; + } if (i == 0 || config.PowerMeter.HttpIndividualRequests) { - if (!queryPhase(i, phaseConfig.Url, phaseConfig.AuthType, phaseConfig.Username, phaseConfig.Password, phaseConfig.HeaderKey, phaseConfig.HeaderValue, phaseConfig.Timeout, + if (!queryPhase(i, phaseConfig.Url, phaseConfig.AuthType, phaseConfig.Username, phaseConfig.Password, phaseConfig.HeaderKey, phaseConfig.HeaderValue, phaseConfig.Timeout, phaseConfig.JsonPath)) { MessageOutput.printf("[HttpPowerMeter] Getting the power of phase %d failed.\r\n", i + 1); MessageOutput.printf("%s\r\n", httpPowerMeterError); return false; } + continue; + } + + if(!tryGetFloatValueForPhase(i, phaseConfig.JsonPath)) { + MessageOutput.printf("[HttpPowerMeter] Getting the power of phase %d (from JSON fetched with Phase 1 config) failed.\r\n", i + 1); + MessageOutput.printf("%s\r\n", httpPowerMeterError); + return false; } } return true; } -bool HttpPowerMeterClass::queryPhase(int phase, const String& url, Auth authType, const char* username, const char* password, +bool HttpPowerMeterClass::queryPhase(int phase, const String& url, Auth authType, const char* username, const char* password, const char* httpHeader, const char* httpValue, uint32_t timeout, const char* jsonPath) { - //hostByName in WiFiGeneric fails to resolve local names. issue described in + //hostByName in WiFiGeneric fails to resolve local names. issue described in //https://github.com/espressif/arduino-esp32/issues/3822 //and in depth analyzed in https://github.com/espressif/esp-idf/issues/2507#issuecomment-761836300 //in conclusion: we cannot rely on httpClient.begin(*wifiClient, url) to resolve IP adresses. @@ -59,7 +66,7 @@ bool HttpPowerMeterClass::queryPhase(int phase, const String& url, Auth authType extractUrlComponents(url, protocol, host, uri, port, base64Authorization); IPAddress ipaddr((uint32_t)0); - //first check if "host" is already an IP adress + //first check if "host" is already an IP adress if (!ipaddr.fromString(host)) { //"host"" is not an IP address so try to resolve the IP adress @@ -69,20 +76,20 @@ bool HttpPowerMeterClass::queryPhase(int phase, const String& url, Auth authType snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Error resolving host %s via DNS, try to enable mDNS in Network Settings"), host.c_str()); //ensure we try resolving via DNS even if mDNS is disabled if(!WiFiGenericClass::hostByName(host.c_str(), ipaddr)){ - snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Error resolving host %s via DNS"), host.c_str()); - } + snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Error resolving host %s via DNS"), host.c_str()); + } } else { - ipaddr = MDNS.queryHost(host); + ipaddr = MDNS.queryHost(host); if (ipaddr == INADDR_NONE){ snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Error resolving host %s via mDNS"), host.c_str()); //when we cannot find local server via mDNS, try resolving via DNS if(!WiFiGenericClass::hostByName(host.c_str(), ipaddr)){ - snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Error resolving host %s via DNS"), host.c_str()); + snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Error resolving host %s via DNS"), host.c_str()); } } - } + } } // secureWifiClient MUST be created before HTTPClient @@ -97,19 +104,19 @@ bool HttpPowerMeterClass::queryPhase(int phase, const String& url, Auth authType } else { wifiClient = std::make_unique(); } - + return httpRequest(phase, *wifiClient, ipaddr.toString(), port, uri, https, authType, username, password, httpHeader, httpValue, timeout, jsonPath); } bool HttpPowerMeterClass::httpRequest(int phase, WiFiClient &wifiClient, const String& host, uint16_t port, const String& uri, bool https, Auth authType, const char* username, const char* password, const char* httpHeader, const char* httpValue, uint32_t timeout, const char* jsonPath) { - if(!httpClient.begin(wifiClient, host, port, uri, https)){ - snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("httpClient.begin() failed for %s://%s"), (https ? "https" : "http"), host.c_str()); + if(!httpClient.begin(wifiClient, host, port, uri, https)){ + snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("httpClient.begin() failed for %s://%s"), (https ? "https" : "http"), host.c_str()); return false; } - prepareRequest(timeout, httpHeader, httpValue); + prepareRequest(timeout, httpHeader, httpValue); if (authType == Auth::digest) { const char *headers[1] = {"WWW-Authenticate"}; httpClient.collectHeaders(headers, 1); @@ -129,8 +136,8 @@ bool HttpPowerMeterClass::httpRequest(int phase, WiFiClient &wifiClient, const S String authReq = httpClient.header("WWW-Authenticate"); String authorization = getDigestAuth(authReq, String(username), String(password), "GET", String(uri), 1); httpClient.end(); - if(!httpClient.begin(wifiClient, host, port, uri, https)){ - snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("httpClient.begin() failed for %s://%s using digest auth"), (https ? "https" : "http"), host.c_str()); + if(!httpClient.begin(wifiClient, host, port, uri, https)){ + snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("httpClient.begin() failed for %s://%s using digest auth"), (https ? "https" : "http"), host.c_str()); return false; } @@ -139,9 +146,21 @@ bool HttpPowerMeterClass::httpRequest(int phase, WiFiClient &wifiClient, const S httpCode = httpClient.GET(); } } - bool result = tryGetFloatValueForPhase(phase, httpCode, jsonPath); + + if (httpCode <= 0) { + snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("HTTP Error %s"), httpClient.errorToString(httpCode).c_str()); + return false; + } + + if (httpCode != HTTP_CODE_OK) { + snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Bad HTTP code: %d"), httpCode); + return false; + } + + httpResponse = httpClient.getString(); // very unfortunate that we cannot parse WifiClient stream directly httpClient.end(); - return result; + + return tryGetFloatValueForPhase(phase, jsonPath); } String HttpPowerMeterClass::extractParam(String& authReq, const String& param, const char delimit) { @@ -176,11 +195,11 @@ String HttpPowerMeterClass::getDigestAuth(String& authReq, const String& usernam //sha256 of method:uri String ha2 = sha256(method + ":" + uri); - //sha256 of h1:nonce:nc:cNonce:auth:h2 + //sha256 of h1:nonce:nc:cNonce:auth:h2 String response = sha256(ha1 + ":" + nonce + ":" + String(nc) + ":" + cNonce + ":" + "auth" + ":" + ha2); //Final authorization String; - String authorization = "Digest username=\""; + String authorization = "Digest username=\""; authorization += username; authorization += "\", realm=\""; authorization += realm; @@ -194,32 +213,23 @@ String HttpPowerMeterClass::getDigestAuth(String& authReq, const String& usernam authorization += String(nc); authorization += ", qop=auth, response=\""; authorization += response; - authorization += "\", algorithm=SHA-256"; + authorization += "\", algorithm=SHA-256"; return authorization; } -bool HttpPowerMeterClass::tryGetFloatValueForPhase(int phase, int httpCode, const char* jsonPath) +bool HttpPowerMeterClass::tryGetFloatValueForPhase(int phase, const char* jsonPath) { - bool success = false; - if (httpCode == HTTP_CODE_OK) { - httpResponse = httpClient.getString(); //very unfortunate that we cannot parse WifiClient stream directly - FirebaseJson json; - json.setJsonData(httpResponse); - FirebaseJsonData value; - if (!json.get(value, jsonPath)) { - snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("[HttpPowerMeter] Couldn't find a value for phase %i with Json query \"%s\""), phase, jsonPath); - }else { - power[phase] = value.to(); - //MessageOutput.printf("Power for Phase %i: %5.2fW\r\n", phase, power[phase]); - success = true; - } - } else if (httpCode <= 0) { - snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("HTTP Error %s"), httpClient.errorToString(httpCode).c_str()); - } else if (httpCode != HTTP_CODE_OK) { - snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Bad HTTP code: %d"), httpCode); - } - return success; + FirebaseJson json; + json.setJsonData(httpResponse); + FirebaseJsonData value; + if (!json.get(value, jsonPath)) { + snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("[HttpPowerMeter] Couldn't find a value for phase %i with Json query \"%s\""), phase, jsonPath); + return false; + } + + power[phase] = value.to(); + return true; } //extract url component as done by httpClient::begin(String url, const char* expectedProtocol) https://github.com/espressif/arduino-esp32/blob/da6325dd7e8e152094b19fe63190907f38ef1ff0/libraries/HTTPClient/src/HTTPClient.cpp#L250 @@ -234,7 +244,7 @@ bool HttpPowerMeterClass::extractUrlComponents(String url, String& _protocol, St _protocol = url.substring(0, index); - //initialize port to default values for http or https. + //initialize port to default values for http or https. //port will be overwritten below in case port is explicitly defined _port = (_protocol == "https" ? 443 : 80); diff --git a/src/Huawei_can.cpp b/src/Huawei_can.cpp index 87b141aad..df1d5eb24 100644 --- a/src/Huawei_can.cpp +++ b/src/Huawei_can.cpp @@ -325,7 +325,13 @@ void HuaweiCanClass::loop() // Check if inverter used by the power limiter is active std::shared_ptr inverter = - Hoymiles.getInverterByPos(config.PowerLimiter.InverterId); + Hoymiles.getInverterBySerial(config.PowerLimiter.InverterId); + + if (inverter == nullptr && config.PowerLimiter.InverterId < INV_MAX_COUNT) { + // we previously had an index saved as InverterId. fall back to the + // respective positional lookup if InverterId is not a known serial. + inverter = Hoymiles.getInverterByPos(config.PowerLimiter.InverterId); + } if (inverter != nullptr) { if(inverter->isProducing()) { diff --git a/src/MqttHandlVedirectHass.cpp b/src/MqttHandlVedirectHass.cpp index b176e9410..b839af3c2 100644 --- a/src/MqttHandlVedirectHass.cpp +++ b/src/MqttHandlVedirectHass.cpp @@ -7,7 +7,7 @@ #include "MqttSettings.h" #include "NetworkSettings.h" #include "MessageOutput.h" -#include "VictronMppt.h" +#include "VictronMppt.h" #include "Utils.h" MqttHandleVedirectHassClass MqttHandleVedirectHass; @@ -15,7 +15,7 @@ MqttHandleVedirectHassClass MqttHandleVedirectHass; void MqttHandleVedirectHassClass::init(Scheduler& scheduler) { scheduler.addTask(_loopTask); - _loopTask.setCallback(std::bind(&MqttHandleVedirectHassClass::loop, this)); + _loopTask.setCallback([this] { loop(); }); _loopTask.setIterations(TASK_FOREVER); _loopTask.enable(); } @@ -55,43 +55,56 @@ void MqttHandleVedirectHassClass::publishConfig() if (!MqttSettings.getConnected()) { return; } - // ensure data is revieved from victron - if (!VictronMppt.isDataValid()) { - return; - } // device info - publishBinarySensor("MPPT load output state", "mdi:export", "LOAD", "ON", "OFF"); - publishSensor("MPPT serial number", "mdi:counter", "SER"); - publishSensor("MPPT firmware number", "mdi:counter", "FW"); - publishSensor("MPPT state of operation", "mdi:wrench", "CS"); - publishSensor("MPPT error code", "mdi:bell", "ERR"); - publishSensor("MPPT off reason", "mdi:wrench", "OR"); - publishSensor("MPPT tracker operation mode", "mdi:wrench", "MPPT"); - publishSensor("MPPT Day sequence number (0...364)", "mdi:calendar-month-outline", "HSDS", NULL, "total", "d"); - - // battery info - publishSensor("Battery voltage", NULL, "V", "voltage", "measurement", "V"); - publishSensor("Battery current", NULL, "I", "current", "measurement", "A"); - publishSensor("Battery power (calculated)", NULL, "P", "power", "measurement", "W"); - publishSensor("Battery efficiency (calculated)", NULL, "E", NULL, "measurement", "%"); - - // panel info - publishSensor("Panel voltage", NULL, "VPV", "voltage", "measurement", "V"); - publishSensor("Panel current (calculated)", NULL, "IPV", "current", "measurement", "A"); - publishSensor("Panel power", NULL, "PPV", "power", "measurement", "W"); - publishSensor("Panel yield total", NULL, "H19", "energy", "total_increasing", "kWh"); - publishSensor("Panel yield today", NULL, "H20", "energy", "total", "kWh"); - publishSensor("Panel maximum power today", NULL, "H21", "power", "measurement", "W"); - publishSensor("Panel yield yesterday", NULL, "H22", "energy", "total", "kWh"); - publishSensor("Panel maximum power yesterday", NULL, "H23", "power", "measurement", "W"); + for (int idx = 0; idx < VictronMppt.controllerAmount(); ++idx) { + // ensure data is received from victron + if (!VictronMppt.isDataValid(idx)) { + continue; + } + + std::optional spOptMpptData = VictronMppt.getData(idx); + if (!spOptMpptData.has_value()) { + continue; + } + + VeDirectMpptController::spData_t &spMpptData = spOptMpptData.value(); + + publishBinarySensor("MPPT load output state", "mdi:export", "LOAD", "ON", "OFF", spMpptData); + publishSensor("MPPT serial number", "mdi:counter", "SER", nullptr, nullptr, nullptr, spMpptData); + publishSensor("MPPT firmware number", "mdi:counter", "FW", nullptr, nullptr, nullptr, spMpptData); + publishSensor("MPPT state of operation", "mdi:wrench", "CS", nullptr, nullptr, nullptr, spMpptData); + publishSensor("MPPT error code", "mdi:bell", "ERR", nullptr, nullptr, nullptr, spMpptData); + publishSensor("MPPT off reason", "mdi:wrench", "OR", nullptr, nullptr, nullptr, spMpptData); + publishSensor("MPPT tracker operation mode", "mdi:wrench", "MPPT", nullptr, nullptr, nullptr, spMpptData); + publishSensor("MPPT Day sequence number (0...364)", "mdi:calendar-month-outline", "HSDS", NULL, "total", "d", spMpptData); + + // battery info + publishSensor("Battery voltage", NULL, "V", "voltage", "measurement", "V", spMpptData); + publishSensor("Battery current", NULL, "I", "current", "measurement", "A", spMpptData); + publishSensor("Battery power (calculated)", NULL, "P", "power", "measurement", "W", spMpptData); + publishSensor("Battery efficiency (calculated)", NULL, "E", NULL, "measurement", "%", spMpptData); + + // panel info + publishSensor("Panel voltage", NULL, "VPV", "voltage", "measurement", "V", spMpptData); + publishSensor("Panel current (calculated)", NULL, "IPV", "current", "measurement", "A", spMpptData); + publishSensor("Panel power", NULL, "PPV", "power", "measurement", "W", spMpptData); + publishSensor("Panel yield total", NULL, "H19", "energy", "total_increasing", "kWh", spMpptData); + publishSensor("Panel yield today", NULL, "H20", "energy", "total", "kWh", spMpptData); + publishSensor("Panel maximum power today", NULL, "H21", "power", "measurement", "W", spMpptData); + publishSensor("Panel yield yesterday", NULL, "H22", "energy", "total", "kWh", spMpptData); + publishSensor("Panel maximum power yesterday", NULL, "H23", "power", "measurement", "W", spMpptData); + } yield(); } -void MqttHandleVedirectHassClass::publishSensor(const char* caption, const char* icon, const char* subTopic, const char* deviceClass, const char* stateClass, const char* unitOfMeasurement ) +void MqttHandleVedirectHassClass::publishSensor(const char *caption, const char *icon, const char *subTopic, + const char *deviceClass, const char *stateClass, + const char *unitOfMeasurement, + const VeDirectMpptController::spData_t &spMpptData) { - String serial = VictronMppt.getData()->SER; + String serial = spMpptData->SER; String sensorId = caption; sensorId.replace(" ", "_"); @@ -126,7 +139,7 @@ void MqttHandleVedirectHassClass::publishSensor(const char* caption, const char* } JsonObject deviceObj = root.createNestedObject("dev"); - createDeviceInfo(deviceObj); + createDeviceInfo(deviceObj, spMpptData); if (Configuration.get().Mqtt.Hass.Expire) { root["exp_aft"] = Configuration.get().Mqtt.PublishInterval * 3; @@ -138,14 +151,18 @@ void MqttHandleVedirectHassClass::publishSensor(const char* caption, const char* root["stat_cla"] = stateClass; } + if (Utils::checkJsonOverflow(root, __FUNCTION__, __LINE__)) { return; } + char buffer[512]; serializeJson(root, buffer); publish(configTopic, buffer); } -void MqttHandleVedirectHassClass::publishBinarySensor(const char* caption, const char* icon, const char* subTopic, const char* payload_on, const char* payload_off) +void MqttHandleVedirectHassClass::publishBinarySensor(const char *caption, const char *icon, const char *subTopic, + const char *payload_on, const char *payload_off, + const VeDirectMpptController::spData_t &spMpptData) { - String serial = VictronMppt.getData()->SER; + String serial = spMpptData->SER; String sensorId = caption; sensorId.replace(" ", "_"); @@ -178,16 +195,18 @@ void MqttHandleVedirectHassClass::publishBinarySensor(const char* caption, const } JsonObject deviceObj = root.createNestedObject("dev"); - createDeviceInfo(deviceObj); + createDeviceInfo(deviceObj, spMpptData); + + if (Utils::checkJsonOverflow(root, __FUNCTION__, __LINE__)) { return; } char buffer[512]; serializeJson(root, buffer); publish(configTopic, buffer); } -void MqttHandleVedirectHassClass::createDeviceInfo(JsonObject& object) +void MqttHandleVedirectHassClass::createDeviceInfo(JsonObject &object, + const VeDirectMpptController::spData_t &spMpptData) { - auto spMpptData = VictronMppt.getData(); String serial = spMpptData->SER; object["name"] = "Victron(" + serial + ")"; object["ids"] = serial; diff --git a/src/MqttHandleBatteryHass.cpp b/src/MqttHandleBatteryHass.cpp index 7cad09222..75912817f 100644 --- a/src/MqttHandleBatteryHass.cpp +++ b/src/MqttHandleBatteryHass.cpp @@ -111,6 +111,14 @@ void MqttHandleBatteryHassClass::loop() case 2: // SoC from MQTT break; case 3: // Victron SmartShunt + publishSensor("Voltage", "mdi:battery-charging", "voltage", "voltage", "measurement", "V"); + publishSensor("Current", "mdi:current-dc", "current", "current", "measurement", "A"); + publishSensor("Instantaneous Power", NULL, "instantaneousPower", "power", "measurement", "W"); + publishSensor("Charged Energy", NULL, "chargedEnergy", "energy", "total_increasing", "kWh"); + publishSensor("Discharged Energy", NULL, "dischargedEnergy", "energy", "total_increasing", "kWh"); + publishSensor("Charge Cycles", "mdi:counter", "chargeCycles"); + publishSensor("Consumed Amp Hours", NULL, "consumedAmpHours", NULL, "measurement", "Ah"); + publishSensor("Last Full Charge", "mdi:timelapse", "lastFullCharge", NULL, NULL, "min"); break; } @@ -156,7 +164,7 @@ void MqttHandleBatteryHassClass::publishSensor(const char* caption, const char* createDeviceInfo(deviceObj); if (Configuration.get().Mqtt.Hass.Expire) { - root["exp_aft"] = Battery.getStats()->getMqttFullPublishIntervalMs() * 3; + root["exp_aft"] = Battery.getStats()->getMqttFullPublishIntervalMs() / 1000 * 3; } if (deviceClass != NULL) { root["dev_cla"] = deviceClass; @@ -165,6 +173,8 @@ void MqttHandleBatteryHassClass::publishSensor(const char* caption, const char* root["stat_cla"] = stateClass; } + if (Utils::checkJsonOverflow(root, __FUNCTION__, __LINE__)) { return; } + char buffer[512]; serializeJson(root, buffer); publish(configTopic, buffer); @@ -208,6 +218,8 @@ void MqttHandleBatteryHassClass::publishBinarySensor(const char* caption, const JsonObject deviceObj = root.createNestedObject("dev"); createDeviceInfo(deviceObj); + if (Utils::checkJsonOverflow(root, __FUNCTION__, __LINE__)) { return; } + char buffer[512]; serializeJson(root, buffer); publish(configTopic, buffer); diff --git a/src/MqttHandlePowerLimiter.cpp b/src/MqttHandlePowerLimiter.cpp index b807fef0e..411fa3f1a 100644 --- a/src/MqttHandlePowerLimiter.cpp +++ b/src/MqttHandlePowerLimiter.cpp @@ -25,8 +25,25 @@ void MqttHandlePowerLimiterClass::init(Scheduler& scheduler) using std::placeholders::_5; using std::placeholders::_6; - String topic = MqttSettings.getPrefix() + "powerlimiter/cmd/mode"; - MqttSettings.subscribe(topic.c_str(), 0, std::bind(&MqttHandlePowerLimiterClass::onCmdMode, this, _1, _2, _3, _4, _5, _6)); + String const& prefix = MqttSettings.getPrefix(); + + auto subscribe = [&prefix, this](char const* subTopic, MqttPowerLimiterCommand command) { + String fullTopic(prefix + "powerlimiter/cmd/" + subTopic); + MqttSettings.subscribe(fullTopic.c_str(), 0, + std::bind(&MqttHandlePowerLimiterClass::onMqttCmd, this, command, + std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4, + std::placeholders::_5, std::placeholders::_6)); + }; + + subscribe("threshold/soc/start", MqttPowerLimiterCommand::BatterySoCStartThreshold); + subscribe("threshold/soc/stop", MqttPowerLimiterCommand::BatterySoCStopThreshold); + subscribe("threshold/soc/full_solar_passthrough", MqttPowerLimiterCommand::FullSolarPassthroughSoC); + subscribe("threshold/voltage/start", MqttPowerLimiterCommand::VoltageStartThreshold); + subscribe("threshold/voltage/stop", MqttPowerLimiterCommand::VoltageStopThreshold); + subscribe("threshold/voltage/full_solar_passthrough_start", MqttPowerLimiterCommand::FullSolarPassThroughStartVoltage); + subscribe("threshold/voltage/full_solar_passthrough_stop", MqttPowerLimiterCommand::FullSolarPassThroughStopVoltage); + subscribe("mode", MqttPowerLimiterCommand::Mode); _lastPublish = millis(); } @@ -50,51 +67,113 @@ void MqttHandlePowerLimiterClass::loop() if (!MqttSettings.getConnected() ) { return; } - if ((millis() - _lastPublish) > (config.Mqtt.PublishInterval * 1000) ) { - auto val = static_cast(PowerLimiter.getMode()); - MqttSettings.publish("powerlimiter/status/mode", String(val)); + if ((millis() - _lastPublish) < (config.Mqtt.PublishInterval * 1000)) { + return; + } + + _lastPublish = millis(); + + auto val = static_cast(PowerLimiter.getMode()); + MqttSettings.publish("powerlimiter/status/mode", String(val)); - yield(); - _lastPublish = millis(); + // no thresholds are relevant for setups without a battery + if (config.PowerLimiter.IsInverterSolarPowered) { return; } + + MqttSettings.publish("powerlimiter/status/threshold/voltage/start", String(config.PowerLimiter.VoltageStartThreshold)); + MqttSettings.publish("powerlimiter/status/threshold/voltage/stop", String(config.PowerLimiter.VoltageStopThreshold)); + + if (config.Vedirect.Enabled) { + MqttSettings.publish("powerlimiter/status/threshold/voltage/full_solar_passthrough_start", String(config.PowerLimiter.FullSolarPassThroughStartVoltage)); + MqttSettings.publish("powerlimiter/status/threshold/voltage/full_solar_passthrough_stop", String(config.PowerLimiter.FullSolarPassThroughStopVoltage)); } -} + if (!config.Battery.Enabled || config.PowerLimiter.IgnoreSoc) { return; } + + MqttSettings.publish("powerlimiter/status/threshold/soc/start", String(config.PowerLimiter.BatterySocStartThreshold)); + MqttSettings.publish("powerlimiter/status/threshold/soc/stop", String(config.PowerLimiter.BatterySocStopThreshold)); -void MqttHandlePowerLimiterClass::onCmdMode(const espMqttClientTypes::MessageProperties& properties, - const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) + if (config.Vedirect.Enabled) { + MqttSettings.publish("powerlimiter/status/threshold/soc/full_solar_passthrough", String(config.PowerLimiter.FullSolarPassThroughSoc)); + } +} + +void MqttHandlePowerLimiterClass::onMqttCmd(MqttPowerLimiterCommand command, const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) { + CONFIG_T& config = Configuration.get(); + std::string strValue(reinterpret_cast(payload), len); - int intValue = -1; + float payload_val = -1; try { - intValue = std::stoi(strValue); + payload_val = std::stof(strValue); } catch (std::invalid_argument const& e) { - MessageOutput.printf("PowerLimiter MQTT handler: cannot parse payload of topic '%s' as int: %s\r\n", + MessageOutput.printf("PowerLimiter MQTT handler: cannot parse payload of topic '%s' as float: %s\r\n", topic, strValue.c_str()); return; } + const int intValue = static_cast(payload_val); std::lock_guard mqttLock(_mqttMutex); - using Mode = PowerLimiterClass::Mode; - switch (static_cast(intValue)) { - case Mode::UnconditionalFullSolarPassthrough: - MessageOutput.println("Power limiter unconditional full solar PT"); - _mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode, - &PowerLimiter, Mode::UnconditionalFullSolarPassthrough)); + switch (command) { + case MqttPowerLimiterCommand::Mode: + { + using Mode = PowerLimiterClass::Mode; + Mode mode = static_cast(intValue); + if (mode == Mode::UnconditionalFullSolarPassthrough) { + MessageOutput.println("Power limiter unconditional full solar PT"); + _mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode, + &PowerLimiter, Mode::UnconditionalFullSolarPassthrough)); + } else if (mode == Mode::Disabled) { + MessageOutput.println("Power limiter disabled (override)"); + _mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode, + &PowerLimiter, Mode::Disabled)); + } else if (mode == Mode::Normal) { + MessageOutput.println("Power limiter normal operation"); + _mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode, + &PowerLimiter, Mode::Normal)); + } else { + MessageOutput.printf("PowerLimiter - unknown mode %d\r\n", intValue); + } + return; + } + case MqttPowerLimiterCommand::BatterySoCStartThreshold: + if (config.PowerLimiter.BatterySocStartThreshold == intValue) { return; } + MessageOutput.printf("Setting battery SoC start threshold to: %d %%\r\n", intValue); + config.PowerLimiter.BatterySocStartThreshold = intValue; + break; + case MqttPowerLimiterCommand::BatterySoCStopThreshold: + if (config.PowerLimiter.BatterySocStopThreshold == intValue) { return; } + MessageOutput.printf("Setting battery SoC stop threshold to: %d %%\r\n", intValue); + config.PowerLimiter.BatterySocStopThreshold = intValue; break; - case Mode::Disabled: - MessageOutput.println("Power limiter disabled (override)"); - _mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode, - &PowerLimiter, Mode::Disabled)); + case MqttPowerLimiterCommand::FullSolarPassthroughSoC: + if (config.PowerLimiter.FullSolarPassThroughSoc == intValue) { return; } + MessageOutput.printf("Setting full solar passthrough SoC to: %d %%\r\n", intValue); + config.PowerLimiter.FullSolarPassThroughSoc = intValue; break; - case Mode::Normal: - MessageOutput.println("Power limiter normal operation"); - _mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode, - &PowerLimiter, Mode::Normal)); + case MqttPowerLimiterCommand::VoltageStartThreshold: + if (config.PowerLimiter.VoltageStartThreshold == payload_val) { return; } + MessageOutput.printf("Setting voltage start threshold to: %.2f V\r\n", payload_val); + config.PowerLimiter.VoltageStartThreshold = payload_val; break; - default: - MessageOutput.printf("PowerLimiter - unknown mode %d\r\n", intValue); + case MqttPowerLimiterCommand::VoltageStopThreshold: + if (config.PowerLimiter.VoltageStopThreshold == payload_val) { return; } + MessageOutput.printf("Setting voltage stop threshold to: %.2f V\r\n", payload_val); + config.PowerLimiter.VoltageStopThreshold = payload_val; + break; + case MqttPowerLimiterCommand::FullSolarPassThroughStartVoltage: + if (config.PowerLimiter.FullSolarPassThroughStartVoltage == payload_val) { return; } + MessageOutput.printf("Setting full solar passthrough start voltage to: %.2f V\r\n", payload_val); + config.PowerLimiter.FullSolarPassThroughStartVoltage = payload_val; + break; + case MqttPowerLimiterCommand::FullSolarPassThroughStopVoltage: + if (config.PowerLimiter.FullSolarPassThroughStopVoltage == payload_val) { return; } + MessageOutput.printf("Setting full solar passthrough stop voltage to: %.2f V\r\n", payload_val); + config.PowerLimiter.FullSolarPassThroughStopVoltage = payload_val; break; } -} \ No newline at end of file + + // not reached if the value did not change + Configuration.write(); +} diff --git a/src/MqttHandlePowerLimiterHass.cpp b/src/MqttHandlePowerLimiterHass.cpp new file mode 100644 index 000000000..9576a84ad --- /dev/null +++ b/src/MqttHandlePowerLimiterHass.cpp @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ +#include "MqttHandlePowerLimiterHass.h" +#include "Configuration.h" +#include "MqttSettings.h" +#include "NetworkSettings.h" +#include "MessageOutput.h" +#include "Utils.h" + +MqttHandlePowerLimiterHassClass MqttHandlePowerLimiterHass; + +void MqttHandlePowerLimiterHassClass::init(Scheduler& scheduler) +{ + scheduler.addTask(_loopTask); + _loopTask.setCallback(std::bind(&MqttHandlePowerLimiterHassClass::loop, this)); + _loopTask.setIterations(TASK_FOREVER); + _loopTask.enable(); +} + +void MqttHandlePowerLimiterHassClass::loop() +{ + if (!Configuration.get().PowerLimiter.Enabled) { + return; + } + if (_updateForced) { + publishConfig(); + _updateForced = false; + } + + if (MqttSettings.getConnected() && !_wasConnected) { + // Connection established + _wasConnected = true; + publishConfig(); + } else if (!MqttSettings.getConnected() && _wasConnected) { + // Connection lost + _wasConnected = false; + } +} + +void MqttHandlePowerLimiterHassClass::forceUpdate() +{ + _updateForced = true; +} + +void MqttHandlePowerLimiterHassClass::publishConfig() +{ + auto const& config = Configuration.get(); + + if (!config.Mqtt.Hass.Enabled) { + return; + } + + if (!MqttSettings.getConnected()) { + return; + } + + if (!config.PowerLimiter.Enabled) { + return; + } + + publishSelect("DPL Mode", "mdi:gauge", "config", "mode", "mode"); + + if (config.PowerLimiter.IsInverterSolarPowered) { + return; + } + + // as this project revolves around Hoymiles inverters, 16 - 60 V is a reasonable voltage range + publishNumber("DPL battery voltage start threshold", "mdi:battery-charging", + "config", "threshold/voltage/start", "threshold/voltage/start", "V", 16, 60); + publishNumber("DPL battery voltage stop threshold", "mdi:battery-charging", + "config", "threshold/voltage/stop", "threshold/voltage/stop", "V", 16, 60); + + if (config.Vedirect.Enabled) { + publishNumber("DPL full solar passthrough start voltage", + "mdi:transmission-tower-import", "config", + "threshold/voltage/full_solar_passthrough_start", + "threshold/voltage/full_solar_passthrough_start", "V", 16, 60); + publishNumber("DPL full solar passthrough stop voltage", + "mdi:transmission-tower-import", "config", + "threshold/voltage/full_solar_passthrough_stop", + "threshold/voltage/full_solar_passthrough_stop", "V", 16, 60); + } + + if (config.Battery.Enabled && !config.PowerLimiter.IgnoreSoc) { + publishNumber("DPL battery SoC start threshold", "mdi:battery-charging", + "config", "threshold/soc/start", "threshold/soc/start", "%", 0, 100); + publishNumber("DPL battery SoC stop threshold", "mdi:battery-charging", + "config", "threshold/soc/stop", "threshold/soc/stop", "%", 0, 100); + + if (config.Vedirect.Enabled) { + publishNumber("DPL full solar passthrough SoC", + "mdi:transmission-tower-import", "config", + "threshold/soc/full_solar_passthrough", + "threshold/soc/full_solar_passthrough", "%", 0, 100); + } + } +} + +void MqttHandlePowerLimiterHassClass::publishSelect( + const char* caption, const char* icon, const char* category, + const char* commandTopic, const char* stateTopic) +{ + + String selectId = caption; + selectId.replace(" ", "_"); + selectId.toLowerCase(); + + const String configTopic = "select/powerlimiter/" + selectId + "/config"; + + const String cmdTopic = MqttSettings.getPrefix() + "powerlimiter/cmd/" + commandTopic; + const String statTopic = MqttSettings.getPrefix() + "powerlimiter/status/" + stateTopic; + + DynamicJsonDocument root(1024); + if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { + return; + } + + root["name"] = caption; + root["uniq_id"] = selectId; + if (strcmp(icon, "")) { + root["ic"] = icon; + } + root["ent_cat"] = category; + root["cmd_t"] = cmdTopic; + root["stat_t"] = statTopic; + JsonArray options = root.createNestedArray("options"); + options.add("0"); + options.add("1"); + options.add("2"); + + JsonObject deviceObj = root.createNestedObject("dev"); + createDeviceInfo(deviceObj); + + if (Utils::checkJsonOverflow(root, __FUNCTION__, __LINE__)) { return; } + + String buffer; + serializeJson(root, buffer); + publish(configTopic, buffer); +} + +void MqttHandlePowerLimiterHassClass::publishNumber( + const char* caption, const char* icon, const char* category, + const char* commandTopic, const char* stateTopic, const char* unitOfMeasure, + const int16_t min, const int16_t max) +{ + + String numberId = caption; + numberId.replace(" ", "_"); + numberId.toLowerCase(); + + const String configTopic = "number/powerlimiter/" + numberId + "/config"; + + const String cmdTopic = MqttSettings.getPrefix() + "powerlimiter/cmd/" + commandTopic; + const String statTopic = MqttSettings.getPrefix() + "powerlimiter/status/" + stateTopic; + + DynamicJsonDocument root(1024); + if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { + return; + } + + root["name"] = caption; + root["uniq_id"] = numberId; + if (strcmp(icon, "")) { + root["ic"] = icon; + } + root["ent_cat"] = category; + root["cmd_t"] = cmdTopic; + root["stat_t"] = statTopic; + root["unit_of_meas"] = unitOfMeasure; + root["min"] = min; + root["max"] = max; + root["mode"] = "box"; + + auto const& config = Configuration.get(); + if (config.Mqtt.Hass.Expire) { + root["exp_aft"] = config.Mqtt.PublishInterval * 3; + } + + JsonObject deviceObj = root.createNestedObject("dev"); + createDeviceInfo(deviceObj); + + if (Utils::checkJsonOverflow(root, __FUNCTION__, __LINE__)) { return; } + + String buffer; + serializeJson(root, buffer); + publish(configTopic, buffer); +} + +void MqttHandlePowerLimiterHassClass::createDeviceInfo(JsonObject& object) +{ + object["name"] = "Dynamic Power Limiter"; + object["ids"] = "0002"; + object["cu"] = String("http://") + NetworkSettings.localIP().toString(); + object["mf"] = "OpenDTU"; + object["mdl"] = "Dynamic Power Limiter"; + object["sw"] = AUTO_GIT_HASH; +} + +void MqttHandlePowerLimiterHassClass::publish(const String& subtopic, const String& payload) +{ + String topic = Configuration.get().Mqtt.Hass.Topic; + topic += subtopic; + MqttSettings.publishGeneric(topic.c_str(), payload.c_str(), Configuration.get().Mqtt.Hass.Retain); +} diff --git a/src/MqttHandleVedirect.cpp b/src/MqttHandleVedirect.cpp index af659f0d7..8cfd6efce 100644 --- a/src/MqttHandleVedirect.cpp +++ b/src/MqttHandleVedirect.cpp @@ -17,7 +17,7 @@ MqttHandleVedirectClass MqttHandleVedirect; void MqttHandleVedirectClass::init(Scheduler& scheduler) { scheduler.addTask(_loopTask); - _loopTask.setCallback(std::bind(&MqttHandleVedirectClass::loop, this)); + _loopTask.setCallback([this] { loop(); }); _loopTask.setIterations(TASK_FOREVER); _loopTask.enable(); @@ -41,10 +41,6 @@ void MqttHandleVedirectClass::loop() return; } - if (!VictronMppt.isDataValid()) { - return; - } - if ((millis() >= _nextPublishFull) || (millis() >= _nextPublishUpdatesOnly)) { // determine if this cycle should publish full values or updates only if (_nextPublishFull <= _nextPublishUpdatesOnly) { @@ -62,82 +58,23 @@ void MqttHandleVedirectClass::loop() } #endif - auto spMpptData = VictronMppt.getData(); - String value; - String topic = "victron/"; - topic.concat(spMpptData->SER); - topic.concat("/"); - - if (_PublishFull || spMpptData->PID != _kvFrame.PID) - MqttSettings.publish(topic + "PID", spMpptData->getPidAsString().data()); - if (_PublishFull || strcmp(spMpptData->SER, _kvFrame.SER) != 0) - MqttSettings.publish(topic + "SER", spMpptData->SER ); - if (_PublishFull || strcmp(spMpptData->FW, _kvFrame.FW) != 0) - MqttSettings.publish(topic + "FW", spMpptData->FW); - if (_PublishFull || spMpptData->LOAD != _kvFrame.LOAD) - MqttSettings.publish(topic + "LOAD", spMpptData->LOAD == true ? "ON": "OFF"); - if (_PublishFull || spMpptData->CS != _kvFrame.CS) - MqttSettings.publish(topic + "CS", spMpptData->getCsAsString().data()); - if (_PublishFull || spMpptData->ERR != _kvFrame.ERR) - MqttSettings.publish(topic + "ERR", spMpptData->getErrAsString().data()); - if (_PublishFull || spMpptData->OR != _kvFrame.OR) - MqttSettings.publish(topic + "OR", spMpptData->getOrAsString().data()); - if (_PublishFull || spMpptData->MPPT != _kvFrame.MPPT) - MqttSettings.publish(topic + "MPPT", spMpptData->getMpptAsString().data()); - if (_PublishFull || spMpptData->HSDS != _kvFrame.HSDS) { - value = spMpptData->HSDS; - MqttSettings.publish(topic + "HSDS", value); - } - if (_PublishFull || spMpptData->V != _kvFrame.V) { - value = spMpptData->V; - MqttSettings.publish(topic + "V", value); - } - if (_PublishFull || spMpptData->I != _kvFrame.I) { - value = spMpptData->I; - MqttSettings.publish(topic + "I", value); - } - if (_PublishFull || spMpptData->P != _kvFrame.P) { - value = spMpptData->P; - MqttSettings.publish(topic + "P", value); - } - if (_PublishFull || spMpptData->VPV != _kvFrame.VPV) { - value = spMpptData->VPV; - MqttSettings.publish(topic + "VPV", value); - } - if (_PublishFull || spMpptData->IPV != _kvFrame.IPV) { - value = spMpptData->IPV; - MqttSettings.publish(topic + "IPV", value); - } - if (_PublishFull || spMpptData->PPV != _kvFrame.PPV) { - value = spMpptData->PPV; - MqttSettings.publish(topic + "PPV", value); - } - if (_PublishFull || spMpptData->E != _kvFrame.E) { - value = spMpptData->E; - MqttSettings.publish(topic + "E", value); - } - if (_PublishFull || spMpptData->H19 != _kvFrame.H19) { - value = spMpptData->H19; - MqttSettings.publish(topic + "H19", value); - } - if (_PublishFull || spMpptData->H20 != _kvFrame.H20) { - value = spMpptData->H20; - MqttSettings.publish(topic + "H20", value); - } - if (_PublishFull || spMpptData->H21 != _kvFrame.H21) { - value = spMpptData->H21; - MqttSettings.publish(topic + "H21", value); - } - if (_PublishFull || spMpptData->H22 != _kvFrame.H22) { - value = spMpptData->H22; - MqttSettings.publish(topic + "H22", value); - } - if (_PublishFull || spMpptData->H23 != _kvFrame.H23) { - value = spMpptData->H23; - MqttSettings.publish(topic + "H23", value); - } - if (!_PublishFull) { - _kvFrame = *spMpptData; + for (int idx = 0; idx < VictronMppt.controllerAmount(); ++idx) { + if (!VictronMppt.isDataValid(idx)) { + continue; + } + + std::optional spOptMpptData = VictronMppt.getData(idx); + if (!spOptMpptData.has_value()) { + continue; + } + + VeDirectMpptController::spData_t &spMpptData = spOptMpptData.value(); + + VeDirectMpptController::veMpptStruct _kvFrame = _kvFrames[spMpptData->SER]; + publish_mppt_data(spMpptData, _kvFrame); + if (!_PublishFull) { + _kvFrames[spMpptData->SER] = *spMpptData; + } } // now calculate next points of time to publish @@ -165,4 +102,81 @@ void MqttHandleVedirectClass::loop() MessageOutput.printf("MqttHandleVedirectClass::loop _nextPublishUpdatesOnly %u _nextPublishFull %u\r\n", _nextPublishUpdatesOnly, _nextPublishFull); #endif } -} \ No newline at end of file +} + +void MqttHandleVedirectClass::publish_mppt_data(const VeDirectMpptController::spData_t &spMpptData, + VeDirectMpptController::veMpptStruct &frame) const { + String value; + String topic = "victron/"; + topic.concat(spMpptData->SER); + topic.concat("/"); + + if (_PublishFull || spMpptData->PID != frame.PID) + MqttSettings.publish(topic + "PID", spMpptData->getPidAsString().data()); + if (_PublishFull || strcmp(spMpptData->SER, frame.SER) != 0) + MqttSettings.publish(topic + "SER", spMpptData->SER ); + if (_PublishFull || strcmp(spMpptData->FW, frame.FW) != 0) + MqttSettings.publish(topic + "FW", spMpptData->FW); + if (_PublishFull || spMpptData->LOAD != frame.LOAD) + MqttSettings.publish(topic + "LOAD", spMpptData->LOAD ? "ON" : "OFF"); + if (_PublishFull || spMpptData->CS != frame.CS) + MqttSettings.publish(topic + "CS", spMpptData->getCsAsString().data()); + if (_PublishFull || spMpptData->ERR != frame.ERR) + MqttSettings.publish(topic + "ERR", spMpptData->getErrAsString().data()); + if (_PublishFull || spMpptData->OR != frame.OR) + MqttSettings.publish(topic + "OR", spMpptData->getOrAsString().data()); + if (_PublishFull || spMpptData->MPPT != frame.MPPT) + MqttSettings.publish(topic + "MPPT", spMpptData->getMpptAsString().data()); + if (_PublishFull || spMpptData->HSDS != frame.HSDS) { + value = spMpptData->HSDS; + MqttSettings.publish(topic + "HSDS", value); + } + if (_PublishFull || spMpptData->V != frame.V) { + value = spMpptData->V; + MqttSettings.publish(topic + "V", value); + } + if (_PublishFull || spMpptData->I != frame.I) { + value = spMpptData->I; + MqttSettings.publish(topic + "I", value); + } + if (_PublishFull || spMpptData->P != frame.P) { + value = spMpptData->P; + MqttSettings.publish(topic + "P", value); + } + if (_PublishFull || spMpptData->VPV != frame.VPV) { + value = spMpptData->VPV; + MqttSettings.publish(topic + "VPV", value); + } + if (_PublishFull || spMpptData->IPV != frame.IPV) { + value = spMpptData->IPV; + MqttSettings.publish(topic + "IPV", value); + } + if (_PublishFull || spMpptData->PPV != frame.PPV) { + value = spMpptData->PPV; + MqttSettings.publish(topic + "PPV", value); + } + if (_PublishFull || spMpptData->E != frame.E) { + value = spMpptData->E; + MqttSettings.publish(topic + "E", value); + } + if (_PublishFull || spMpptData->H19 != frame.H19) { + value = spMpptData->H19; + MqttSettings.publish(topic + "H19", value); + } + if (_PublishFull || spMpptData->H20 != frame.H20) { + value = spMpptData->H20; + MqttSettings.publish(topic + "H20", value); + } + if (_PublishFull || spMpptData->H21 != frame.H21) { + value = spMpptData->H21; + MqttSettings.publish(topic + "H21", value); + } + if (_PublishFull || spMpptData->H22 != frame.H22) { + value = spMpptData->H22; + MqttSettings.publish(topic + "H22", value); + } + if (_PublishFull || spMpptData->H23 != frame.H23) { + value = spMpptData->H23; + MqttSettings.publish(topic + "H23", value); + } +} diff --git a/src/PinMapping.cpp b/src/PinMapping.cpp index bf78e4ede..7c8bec186 100644 --- a/src/PinMapping.cpp +++ b/src/PinMapping.cpp @@ -144,6 +144,18 @@ #define HUAWEI_PIN_POWER -1 #endif +#ifndef POWERMETER_PIN_RX +#define POWERMETER_PIN_RX -1 +#endif + +#ifndef POWERMETER_PIN_TX +#define POWERMETER_PIN_TX -1 +#endif + +#ifndef POWERMETER_PIN_DERE +#define POWERMETER_PIN_DERE -1 +#endif + PinMappingClass PinMapping; PinMappingClass::PinMappingClass() @@ -181,9 +193,16 @@ PinMappingClass::PinMappingClass() _pinMapping.display_clk = DISPLAY_CLK; _pinMapping.display_cs = DISPLAY_CS; _pinMapping.display_reset = DISPLAY_RESET; - - _pinMapping.victron_tx = VICTRON_PIN_TX; + + _pinMapping.led[0] = LED0; + _pinMapping.led[1] = LED1; + + // OpenDTU-OnBattery-specific pins below _pinMapping.victron_rx = VICTRON_PIN_RX; + _pinMapping.victron_tx = VICTRON_PIN_TX; + + _pinMapping.victron_rx2 = VICTRON_PIN_RX; + _pinMapping.victron_tx2 = VICTRON_PIN_TX; _pinMapping.battery_rx = BATTERY_PIN_RX; _pinMapping.battery_rxen = BATTERY_PIN_RXEN; @@ -196,8 +215,10 @@ PinMappingClass::PinMappingClass() _pinMapping.huawei_cs = HUAWEI_PIN_CS; _pinMapping.huawei_irq = HUAWEI_PIN_IRQ; _pinMapping.huawei_power = HUAWEI_PIN_POWER; - _pinMapping.led[0] = LED0; - _pinMapping.led[1] = LED1; + + _pinMapping.powermeter_rx = POWERMETER_PIN_RX; + _pinMapping.powermeter_tx = POWERMETER_PIN_TX; + _pinMapping.powermeter_dere = POWERMETER_PIN_DERE; } PinMapping_t& PinMappingClass::get() @@ -257,8 +278,14 @@ bool PinMappingClass::init(const String& deviceMapping) _pinMapping.display_cs = doc[i]["display"]["cs"] | DISPLAY_CS; _pinMapping.display_reset = doc[i]["display"]["reset"] | DISPLAY_RESET; + _pinMapping.led[0] = doc[i]["led"]["led0"] | LED0; + _pinMapping.led[1] = doc[i]["led"]["led1"] | LED1; + + // OpenDTU-OnBattery-specific pins below _pinMapping.victron_rx = doc[i]["victron"]["rx"] | VICTRON_PIN_RX; _pinMapping.victron_tx = doc[i]["victron"]["tx"] | VICTRON_PIN_TX; + _pinMapping.victron_rx2 = doc[i]["victron"]["rx2"] | VICTRON_PIN_RX; + _pinMapping.victron_tx2 = doc[i]["victron"]["tx2"] | VICTRON_PIN_TX; _pinMapping.battery_rx = doc[i]["battery"]["rx"] | BATTERY_PIN_RX; _pinMapping.battery_rxen = doc[i]["battery"]["rxen"] | BATTERY_PIN_RXEN; @@ -272,8 +299,9 @@ bool PinMappingClass::init(const String& deviceMapping) _pinMapping.huawei_cs = doc[i]["huawei"]["cs"] | HUAWEI_PIN_CS; _pinMapping.huawei_power = doc[i]["huawei"]["power"] | HUAWEI_PIN_POWER; - _pinMapping.led[0] = doc[i]["led"]["led0"] | LED0; - _pinMapping.led[1] = doc[i]["led"]["led1"] | LED1; + _pinMapping.powermeter_rx = doc[i]["powermeter"]["rx"] | POWERMETER_PIN_RX; + _pinMapping.powermeter_tx = doc[i]["powermeter"]["tx"] | POWERMETER_PIN_TX; + _pinMapping.powermeter_dere = doc[i]["powermeter"]["dere"] | POWERMETER_PIN_DERE; return true; } diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index a128f9584..b2ce2392a 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -31,7 +31,7 @@ frozen::string const& PowerLimiterClass::getStatusText(PowerLimiterClass::Status { static const frozen::string missing = "programmer error: missing status text"; - static const frozen::map texts = { + static const frozen::map texts = { { Status::Initializing, "initializing (should not see me)" }, { Status::DisabledByConfig, "disabled by configuration" }, { Status::DisabledByMqtt, "disabled by MQTT" }, @@ -47,8 +47,11 @@ frozen::string const& PowerLimiterClass::getStatusText(PowerLimiterClass::Status { Status::InverterPowerCmdPending, "waiting for a start/stop/restart command to complete" }, { Status::InverterDevInfoPending, "waiting for inverter device information to be available" }, { Status::InverterStatsPending, "waiting for sufficiently recent inverter data" }, + { Status::CalculatedLimitBelowMinLimit, "calculated limit is less than lower power limit" }, { Status::UnconditionalSolarPassthrough, "unconditionally passing through all solar power (MQTT override)" }, { Status::NoVeDirect, "VE.Direct disabled, connection broken, or data outdated" }, + { Status::NoEnergy, "no energy source available to power the inverter from" }, + { Status::HuaweiPsu, "DPL stands by while Huawei PSU is enabled/charging" }, { Status::Stable, "the system is stable, the last power limit is still valid" }, }; @@ -69,8 +72,8 @@ void PowerLimiterClass::announceStatus(PowerLimiterClass::Status status) // should just be silent while it is disabled. if (status == Status::DisabledByConfig && _lastStatus == status) { return; } - MessageOutput.printf("[%11.3f] DPL: %s\r\n", - static_cast(millis())/1000, getStatusText(status).data()); + MessageOutput.printf("[DPL::announceStatus] %s\r\n", + getStatusText(status).data()); _lastStatus = status; _lastStatusPrinted = millis(); @@ -89,7 +92,15 @@ bool PowerLimiterClass::shutdown(PowerLimiterClass::Status status) _shutdownPending = true; _oTargetPowerState = false; - _oTargetPowerLimitWatts = Configuration.get().PowerLimiter.LowerPowerLimit; + + auto const& config = Configuration.get(); + if ( (Status::PowerMeterTimeout == status || + Status::CalculatedLimitBelowMinLimit == status) + && config.PowerLimiter.IsInverterSolarPowered) { + _oTargetPowerState = true; + } + + _oTargetPowerLimitWatts = config.PowerLimiter.LowerPowerLimit; return updateInverter(); } @@ -126,7 +137,13 @@ void PowerLimiterClass::loop() } std::shared_ptr currentInverter = - Hoymiles.getInverterByPos(config.PowerLimiter.InverterId); + Hoymiles.getInverterBySerial(config.PowerLimiter.InverterId); + + if (currentInverter == nullptr && config.PowerLimiter.InverterId < INV_MAX_COUNT) { + // we previously had an index saved as InverterId. fall back to the + // respective positional lookup if InverterId is not a known serial. + currentInverter = Hoymiles.getInverterByPos(config.PowerLimiter.InverterId); + } // in case of (newly) broken configuration, shut down // the last inverter we worked with (if any) @@ -224,38 +241,32 @@ void PowerLimiterClass::loop() } } - // Battery charging cycle conditions - // First we always disable discharge if the battery is empty - if (isStopThresholdReached()) { - // Disable battery discharge when empty - _batteryDischargeEnabled = false; - } else { - // UI: Solar Passthrough Enabled -> false - // Battery discharge can be enabled when start threshold is reached - if (!config.PowerLimiter.SolarPassThroughEnabled && isStartThresholdReached()) { - _batteryDischargeEnabled = true; - } - - // UI: Solar Passthrough Enabled -> true && EMPTY_AT_NIGHT - if (config.PowerLimiter.SolarPassThroughEnabled && config.PowerLimiter.BatteryDrainStategy == EMPTY_AT_NIGHT) { - if(isStartThresholdReached()) { - // In this case we should only discharge the battery as long it is above startThreshold - _batteryDischargeEnabled = true; - } - else { - // In this case we should only discharge the battery when there is no sunshine - _batteryDischargeEnabled = !canUseDirectSolarPower(); + auto getBatteryPower = [this,&config]() -> bool { + if (config.PowerLimiter.IsInverterSolarPowered) { return false; } + + if (isStopThresholdReached()) { return false; } + + if (isStartThresholdReached()) { return true; } + + // with solar passthrough, and the respective switch enabled, we + // may start discharging the battery when it is nighttime. we also + // stop the discharge cycle if it becomes daytime again. + // TODO(schlimmchen): should be supported by sunrise and sunset, such + // that a thunderstorm or other events that drastically lower the solar + // power do not cause the start of a discharge cycle during the day. + if (config.PowerLimiter.SolarPassThroughEnabled && + config.PowerLimiter.BatteryAlwaysUseAtNight) { + return getSolarPower() == 0; } - } - // UI: Solar Passthrough Enabled -> true && EMPTY_WHEN_FULL - // Battery discharge can be enabled when start threshold is reached - if (config.PowerLimiter.SolarPassThroughEnabled && isStartThresholdReached() && config.PowerLimiter.BatteryDrainStategy == EMPTY_WHEN_FULL) { - _batteryDischargeEnabled = true; - } - } + // we are between start and stop threshold and keep the state that was + // last triggered, either charging or discharging. + return _batteryDischargeEnabled; + }; - if (_verboseLogging) { + _batteryDischargeEnabled = getBatteryPower(); + + if (_verboseLogging && !config.PowerLimiter.IsInverterSolarPowered) { MessageOutput.printf("[DPL::loop] battery interface %s, SoC: %d %%, StartTH: %d %%, StopTH: %d %%, SoC age: %d s, ignore: %s\r\n", (config.Battery.Enabled?"enabled":"disabled"), Battery.getStats()->getSoC(), @@ -270,24 +281,15 @@ void PowerLimiterClass::loop() config.PowerLimiter.VoltageStartThreshold, config.PowerLimiter.VoltageStopThreshold); - MessageOutput.printf("[DPL::loop] StartTH reached: %s, StopTH reached: %s, inverter %s producing\r\n", + MessageOutput.printf("[DPL::loop] StartTH reached: %s, StopTH reached: %s, SolarPT %sabled, use at night: %s\r\n", (isStartThresholdReached()?"yes":"no"), (isStopThresholdReached()?"yes":"no"), - (_inverter->isProducing()?"is":"is NOT")); - - MessageOutput.printf("[DPL::loop] SolarPT %s, Drain Strategy: %i, canUseDirectSolarPower: %s\r\n", - (config.PowerLimiter.SolarPassThroughEnabled?"enabled":"disabled"), - config.PowerLimiter.BatteryDrainStategy, (canUseDirectSolarPower()?"yes":"no")); - - MessageOutput.printf("[DPL::loop] battery discharging %s, PowerMeter: %d W, target consumption: %d W\r\n", - (_batteryDischargeEnabled?"allowed":"prevented"), - static_cast(round(PowerMeter.getPowerTotal())), - config.PowerLimiter.TargetPowerConsumption); - } + (config.PowerLimiter.SolarPassThroughEnabled?"en":"dis"), + (config.PowerLimiter.BatteryAlwaysUseAtNight?"yes":"no")); + }; // Calculate and set Power Limit (NOTE: might reset _inverter to nullptr!) - int32_t newPowerLimit = calcPowerLimit(_inverter, canUseDirectSolarPower(), _batteryDischargeEnabled); - bool limitUpdated = setNewPowerLimit(_inverter, newPowerLimit); + bool limitUpdated = calcPowerLimit(_inverter, getSolarPower(), _batteryDischargeEnabled); _lastCalculation = millis(); @@ -310,7 +312,7 @@ void PowerLimiterClass::loop() float PowerLimiterClass::getBatteryVoltage(bool log) { if (!_inverter) { // there should be no need to call this method if no target inverter is known - MessageOutput.println("DPL getBatteryVoltage: no inverter (programmer error)"); + MessageOutput.println("[DPL::getBatteryVoltage] no inverter (programmer error)"); return 0.0; } @@ -401,47 +403,59 @@ uint8_t PowerLimiterClass::getPowerLimiterState() { return PL_UI_STATE_INACTIVE; } -bool PowerLimiterClass::canUseDirectSolarPower() +// Logic table +// | Case # | batteryPower | solarPower > 0 | useFullSolarPassthrough | Result | +// | 1 | false | false | doesn't matter | PL = 0 | +// | 2 | false | true | doesn't matter | PL = Victron Power | +// | 3 | true | doesn't matter | false | PL = PowerMeter value (Battery can supply unlimited energy) | +// | 4 | true | false | true | PL = PowerMeter value | +// | 5 | true | true | true | PL = max(PowerMeter value, Victron Power) | + +bool PowerLimiterClass::calcPowerLimit(std::shared_ptr inverter, int32_t solarPowerDC, bool batteryPower) { - CONFIG_T& config = Configuration.get(); + if (_verboseLogging) { + MessageOutput.printf("[DPL::calcPowerLimit] battery use %s, solar power (DC): %d W\r\n", + (batteryPower?"allowed":"prevented"), solarPowerDC); + } - if (!config.PowerLimiter.SolarPassThroughEnabled - || isBelowStopThreshold() - || !VictronMppt.isDataValid()) { - return false; + if (solarPowerDC <= 0 && !batteryPower) { + return shutdown(Status::NoEnergy); } - return VictronMppt.getPowerOutputWatts() >= 20; // enough power? -} + // We check if the PSU is on and disable the Power Limiter in this case. + // The PSU should reduce power or shut down first before the Power Limiter + // kicks in. The only case where this is not desired is if the battery is + // over the Full Solar Passthrough Threshold. In this case the Power + // Limiter should run and the PSU will shut down as a consequence. + if (!useFullSolarPassthrough() && HuaweiCan.getAutoPowerStatus()) { + return shutdown(Status::HuaweiPsu); + } + auto powerMeter = static_cast(PowerMeter.getPowerTotal()); -// Logic table -// | Case # | batteryDischargeEnabled | solarPowerEnabled | useFullSolarPassthrough | Result | -// | 1 | false | false | doesn't matter | PL = 0 | -// | 2 | false | true | doesn't matter | PL = Victron Power | -// | 3 | true | doesn't matter | false | PL = PowerMeter value (Battery can supply unlimited energy) | -// | 4 | true | false | true | PL = PowerMeter value | -// | 5 | true | true | true | PL = max(PowerMeter value, Victron Power) | - -int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr inverter, bool solarPowerEnabled, bool batteryDischargeEnabled) -{ - CONFIG_T& config = Configuration.get(); - - int32_t acPower = 0; - int32_t newPowerLimit = round(PowerMeter.getPowerTotal()); + auto inverterOutput = static_cast(inverter->Statistics()->getChannelFieldValue(TYPE_AC, CH0, FLD_PAC)); + + auto solarPowerAC = inverterPowerDcToAc(inverter, solarPowerDC); + + auto const& config = Configuration.get(); - if (!solarPowerEnabled && !batteryDischargeEnabled) { - // Case 1 - No energy sources available - return 0; + if (_verboseLogging) { + MessageOutput.printf("[DPL::calcPowerLimit] power meter: %d W, " + "target consumption: %d W, inverter output: %d W, solar power (AC): %d\r\n", + powerMeter, + config.PowerLimiter.TargetPowerConsumption, + inverterOutput, + solarPowerAC); } + auto newPowerLimit = powerMeter; + if (config.PowerLimiter.IsInverterBehindPowerMeter) { // If the inverter the behind the power meter (part of measurement), // the produced power of this inverter has also to be taken into account. // We don't use FLD_PAC from the statistics, because that // data might be too old and unreliable. - acPower = static_cast(inverter->Statistics()->getChannelFieldValue(TYPE_AC, CH0, FLD_PAC)); - newPowerLimit += acPower; + newPowerLimit += inverterOutput; } // We're not trying to hit 0 exactly but take an offset into account @@ -449,38 +463,37 @@ int32_t PowerLimiterClass::calcPowerLimit(std::shared_ptr inve // Case 3 newPowerLimit -= config.PowerLimiter.TargetPowerConsumption; - // At this point we've calculated the required energy to compensate for household consumption. - // If the battery is enabled this can always be supplied since we assume that the battery can supply unlimited power - // The next step is to determine if the Solar power as provided by the Victron charger - // actually constrains or dictates another inverter power value - int32_t adjustedVictronChargePower = inverterPowerDcToAc(inverter, getSolarChargePower()); + if (!batteryPower) { + newPowerLimit = std::min(newPowerLimit, solarPowerAC); - // Battery can be discharged and we should output max (Victron solar power || power meter value) - if(batteryDischargeEnabled && useFullSolarPassthrough()) { - // Case 5 - newPowerLimit = newPowerLimit > adjustedVictronChargePower ? newPowerLimit : adjustedVictronChargePower; - } else { - // We check if the PSU is on and disable the Power Limiter in this case. - // The PSU should reduce power or shut down first before the Power Limiter kicks in - // The only case where this is not desired is if the battery is over the Full Solar Passthrough Threshold - // In this case the Power Limiter should start. The PSU will shutdown when the Power Limiter is active - if (HuaweiCan.getAutoPowerStatus()) { - return 0; - } + // do not drain the battery. use as much power as needed to match the + // household consumption, but not more than the available solar power. + if (_verboseLogging) { + MessageOutput.printf("[DPL::calcPowerLimit] limited to solar power: %d W\r\n", + newPowerLimit); + } + + return setNewPowerLimit(inverter, newPowerLimit); } - // We should use Victron solar power only (corrected by efficiency factor) - if (solarPowerEnabled && !batteryDischargeEnabled) { - // Case 2 - Limit power to solar power only + // convert all solar power if full solar-passthrough is active + if (useFullSolarPassthrough()) { + newPowerLimit = std::max(newPowerLimit, solarPowerAC); + if (_verboseLogging) { - MessageOutput.printf("[DPL::loop] Consuming Solar Power Only -> adjustedVictronChargePower: %d W, newPowerLimit: %d W\r\n", - adjustedVictronChargePower, newPowerLimit); + MessageOutput.printf("[DPL::calcPowerLimit] full solar-passthrough active: %d W\r\n", + newPowerLimit); } - newPowerLimit = std::min(newPowerLimit, adjustedVictronChargePower); + return setNewPowerLimit(inverter, newPowerLimit); } - return newPowerLimit; + if (_verboseLogging) { + MessageOutput.printf("[DPL::calcPowerLimit] match power meter with limit of %d W\r\n", + newPowerLimit); + } + + return setNewPowerLimit(inverter, newPowerLimit); } /** @@ -571,7 +584,7 @@ bool PowerLimiterClass::updateInverter() uint32_t lastLimitCommandMillis = _inverter->SystemConfigPara()->getLastUpdateCommand(); if ((lastLimitCommandMillis - *_oUpdateStartMillis) < halfOfAllMillis && CMD_OK == lastLimitCommandState) { - MessageOutput.printf("[DPL:updateInverter] actual limit is %.1f %% " + MessageOutput.printf("[DPL::updateInverter] actual limit is %.1f %% " "(%.0f W respectively), effective %d ms after update started, " "requested were %.1f %%\r\n", currentRelativeLimit, @@ -580,7 +593,7 @@ bool PowerLimiterClass::updateInverter() newRelativeLimit); if (std::abs(newRelativeLimit - currentRelativeLimit) > 2.0) { - MessageOutput.printf("[DPL:updateInverter] NOTE: expected limit of %.1f %% " + MessageOutput.printf("[DPL::updateInverter] NOTE: expected limit of %.1f %% " "and actual limit of %.1f %% mismatch by more than 2 %%, " "is the DPL in exclusive control over the inverter?\r\n", newRelativeLimit, currentRelativeLimit); @@ -657,9 +670,10 @@ static int32_t scalePowerLimit(std::shared_ptr inverter, int32 if (dcProdChnls == 0 || dcProdChnls == dcTotalChnls) { return newLimit; } - MessageOutput.printf("[DPL::scalePowerLimit] %d channels total, %d producing " - "channels, scaling power limit\r\n", dcTotalChnls, dcProdChnls); - return round(newLimit * static_cast(dcTotalChnls) / dcProdChnls); + auto scaled = static_cast(newLimit * static_cast(dcTotalChnls) / dcProdChnls); + MessageOutput.printf("[DPL::scalePowerLimit] %d/%d channels are producing, " + "scaling from %d to %d W\r\n", dcProdChnls, dcTotalChnls, newLimit, scaled); + return scaled; } /** @@ -670,17 +684,23 @@ static int32_t scalePowerLimit(std::shared_ptr inverter, int32 */ bool PowerLimiterClass::setNewPowerLimit(std::shared_ptr inverter, int32_t newPowerLimit) { - CONFIG_T& config = Configuration.get(); + auto const& config = Configuration.get(); + auto lowerLimit = config.PowerLimiter.LowerPowerLimit; + auto upperLimit = config.PowerLimiter.UpperPowerLimit; + auto hysteresis = config.PowerLimiter.TargetPowerConsumptionHysteresis; + + if (_verboseLogging) { + MessageOutput.printf("[DPL::setNewPowerLimit] input limit: %d W, " + "lower limit: %d W, upper limit: %d W, hysteresis: %d W\r\n", + newPowerLimit, lowerLimit, upperLimit, hysteresis); + } - // Stop the inverter if limit is below threshold. - if (newPowerLimit < config.PowerLimiter.LowerPowerLimit) { - // the status must not change outside of loop(). this condition is - // communicated through log messages already. - return shutdown(); + if (newPowerLimit < lowerLimit) { + return shutdown(Status::CalculatedLimitBelowMinLimit); } // enforce configured upper power limit - int32_t effPowerLimit = std::min(newPowerLimit, config.PowerLimiter.UpperPowerLimit); + int32_t effPowerLimit = std::min(newPowerLimit, upperLimit); // early in the loop we make it a pre-requisite that this // value is non-zero, so we can assume it to be valid. @@ -694,12 +714,12 @@ bool PowerLimiterClass::setNewPowerLimit(std::shared_ptr inver effPowerLimit = std::min(effPowerLimit, maxPower); auto diff = std::abs(currentLimitAbs - effPowerLimit); - auto hysteresis = config.PowerLimiter.TargetPowerConsumptionHysteresis; if (_verboseLogging) { - MessageOutput.printf("[DPL::setNewPowerLimit] calculated: %d W, " - "requesting: %d W, reported: %d W, diff: %d W, hysteresis: %d W\r\n", - newPowerLimit, effPowerLimit, currentLimitAbs, diff, hysteresis); + MessageOutput.printf("[DPL::setNewPowerLimit] inverter max: %d W, " + "inverter %s producing, requesting: %d W, reported: %d W, " + "diff: %d W\r\n", maxPower, (inverter->isProducing()?"is":"is NOT"), + effPowerLimit, currentLimitAbs, diff); } if (diff > hysteresis) { @@ -710,20 +730,33 @@ bool PowerLimiterClass::setNewPowerLimit(std::shared_ptr inver return updateInverter(); } -int32_t PowerLimiterClass::getSolarChargePower() +int32_t PowerLimiterClass::getSolarPower() { - if (!canUseDirectSolarPower()) { + auto const& config = Configuration.get(); + + if (config.PowerLimiter.IsInverterSolarPowered) { + // the returned value is arbitrary, as long as it's + // greater than the inverters max DC power consumption. + return 10 * 1000; + } + + if (!config.PowerLimiter.SolarPassThroughEnabled + || isBelowStopThreshold() + || !VictronMppt.isDataValid()) { return 0; } - return VictronMppt.getPowerOutputWatts(); + auto solarPower = VictronMppt.getPowerOutputWatts(); + if (solarPower < 20) { return 0; } // too little to work with + + return solarPower; } float PowerLimiterClass::getLoadCorrectedVoltage() { if (!_inverter) { // there should be no need to call this method if no target inverter is known - MessageOutput.println("DPL getLoadCorrectedVoltage: no inverter (programmer error)"); + MessageOutput.println("[DPL::getLoadCorrectedVoltage] no inverter (programmer error)"); return 0.0; } @@ -805,6 +838,12 @@ void PowerLimiterClass::calcNextInverterRestart() return; } + if (config.PowerLimiter.IsInverterSolarPowered) { + _nextInverterRestart = 1; + MessageOutput.println("[DPL::calcNextInverterRestart] not restarting solar-powered inverters"); + return; + } + // read time from timeserver, if time is not synced then return struct tm timeinfo; if (getLocalTime(&timeinfo, 5)) { @@ -835,12 +874,13 @@ void PowerLimiterClass::calcNextInverterRestart() bool PowerLimiterClass::useFullSolarPassthrough() { - CONFIG_T& config = Configuration.get(); + auto const& config = Configuration.get(); + + // solar passthrough only applies to setups with battery-powered inverters + if (config.PowerLimiter.IsInverterSolarPowered) { return false; } // We only do full solar PT if general solar PT is enabled - if(!config.PowerLimiter.SolarPassThroughEnabled) { - return false; - } + if(!config.PowerLimiter.SolarPassThroughEnabled) { return false; } if (testThreshold(config.PowerLimiter.FullSolarPassThroughSoc, config.PowerLimiter.FullSolarPassThroughStartVoltage, diff --git a/src/PowerMeter.cpp b/src/PowerMeter.cpp index e45b5837d..72526b7d7 100644 --- a/src/PowerMeter.cpp +++ b/src/PowerMeter.cpp @@ -4,20 +4,16 @@ */ #include "PowerMeter.h" #include "Configuration.h" +#include "PinMapping.h" #include "HttpPowerMeter.h" #include "MqttSettings.h" #include "NetworkSettings.h" -#include "SDM.h" #include "MessageOutput.h" #include -#include +#include PowerMeterClass PowerMeter; -SDM sdm(Serial2, 9600, NOT_A_PIN, SERIAL_8N1, SDM_RX_PIN, SDM_TX_PIN); - -SoftwareSerial inputSerial; - void PowerMeterClass::init(Scheduler& scheduler) { scheduler.addTask(_loopTask); @@ -37,8 +33,12 @@ void PowerMeterClass::init(Scheduler& scheduler) return; } - switch(config.PowerMeter.Source) { - case SOURCE_MQTT: { + const PinMapping_t& pin = PinMapping.get(); + MessageOutput.printf("[PowerMeter] rx = %d, tx = %d, dere = %d\r\n", + pin.powermeter_rx, pin.powermeter_tx, pin.powermeter_dere); + + switch(static_cast(config.PowerMeter.Source)) { + case Source::MQTT: { auto subscribe = [this](char const* topic, float* target) { if (strlen(topic) == 0) { return; } MqttSettings.subscribe(topic, 0, @@ -56,21 +56,38 @@ void PowerMeterClass::init(Scheduler& scheduler) break; } - case SOURCE_SDM1PH: - case SOURCE_SDM3PH: - sdm.begin(); + case Source::SDM1PH: + case Source::SDM3PH: + if (pin.powermeter_rx < 0 || pin.powermeter_tx < 0) { + MessageOutput.println("[PowerMeter] invalid pin config for SDM power meter (RX and TX pins must be defined)"); + return; + } + + _upSdm = std::make_unique(Serial2, 9600, pin.powermeter_dere, + SERIAL_8N1, pin.powermeter_rx, pin.powermeter_tx); + _upSdm->begin(); break; - case SOURCE_HTTP: + case Source::HTTP: HttpPowerMeter.init(); break; - case SOURCE_SML: - pinMode(SML_RX_PIN, INPUT); - inputSerial.begin(9600, SWSERIAL_8N1, SML_RX_PIN, -1, false, 128, 95); - inputSerial.enableRx(true); - inputSerial.enableTx(false); - inputSerial.flush(); + case Source::SML: + if (pin.powermeter_rx < 0) { + MessageOutput.println("[PowerMeter] invalid pin config for SML power meter (RX pin must be defined)"); + return; + } + + pinMode(pin.powermeter_rx, INPUT); + _upSmlSerial = std::make_unique(); + _upSmlSerial->begin(9600, SWSERIAL_8N1, pin.powermeter_rx, -1, false, 128, 95); + _upSmlSerial->enableRx(true); + _upSmlSerial->enableTx(false); + _upSmlSerial->flush(); + break; + + case Source::SMAHM2: + SMA_HM.init(scheduler, config.PowerMeter.VerboseLogging); break; } } @@ -108,30 +125,34 @@ float PowerMeterClass::getPowerTotal(bool forceUpdate) readPowerMeter(); } } + + std::lock_guard l(_mutex); return _powerMeter1Power + _powerMeter2Power + _powerMeter3Power; } uint32_t PowerMeterClass::getLastPowerMeterUpdate() { + std::lock_guard l(_mutex); return _lastPowerMeterUpdate; } void PowerMeterClass::mqtt() { - if (!MqttSettings.getConnected()) { - return; - } else { - String topic = "powermeter"; - MqttSettings.publish(topic + "/power1", String(_powerMeter1Power)); - MqttSettings.publish(topic + "/power2", String(_powerMeter2Power)); - MqttSettings.publish(topic + "/power3", String(_powerMeter3Power)); - MqttSettings.publish(topic + "/powertotal", String(getPowerTotal())); - MqttSettings.publish(topic + "/voltage1", String(_powerMeter1Voltage)); - MqttSettings.publish(topic + "/voltage2", String(_powerMeter2Voltage)); - MqttSettings.publish(topic + "/voltage3", String(_powerMeter3Voltage)); - MqttSettings.publish(topic + "/import", String(_powerMeterImport)); - MqttSettings.publish(topic + "/export", String(_powerMeterExport)); - } + if (!MqttSettings.getConnected()) { return; } + + String topic = "powermeter"; + auto totalPower = getPowerTotal(); + + std::lock_guard l(_mutex); + MqttSettings.publish(topic + "/power1", String(_powerMeter1Power)); + MqttSettings.publish(topic + "/power2", String(_powerMeter2Power)); + MqttSettings.publish(topic + "/power3", String(_powerMeter3Power)); + MqttSettings.publish(topic + "/powertotal", String(totalPower)); + MqttSettings.publish(topic + "/voltage1", String(_powerMeter1Voltage)); + MqttSettings.publish(topic + "/voltage2", String(_powerMeter2Voltage)); + MqttSettings.publish(topic + "/voltage3", String(_powerMeter3Voltage)); + MqttSettings.publish(topic + "/import", String(_powerMeterImport)); + MqttSettings.publish(topic + "/export", String(_powerMeterExport)); } void PowerMeterClass::loop() @@ -141,12 +162,10 @@ void PowerMeterClass::loop() if (!config.PowerMeter.Enabled) { return; } - if (config.PowerMeter.Source == SOURCE_SML) { - if (!smlReadLoop()) { - return; - } else { - _lastPowerMeterUpdate = millis(); - } + if (static_cast(config.PowerMeter.Source) == Source::SML && + nullptr != _upSmlSerial) { + if (!smlReadLoop()) { return; } + _lastPowerMeterUpdate = millis(); } if ((millis() - _lastPowerMeterCheck) < (config.PowerMeter.Interval * 1000)) { @@ -165,46 +184,79 @@ void PowerMeterClass::loop() void PowerMeterClass::readPowerMeter() { CONFIG_T& config = Configuration.get(); - - uint8_t _address = config.PowerMeter.SdmAddress; - if (config.PowerMeter.Source == SOURCE_SDM1PH) { - _powerMeter1Power = static_cast(sdm.readVal(SDM_PHASE_1_POWER, _address)); - _powerMeter2Power = 0.0; - _powerMeter3Power = 0.0; - _powerMeter1Voltage = static_cast(sdm.readVal(SDM_PHASE_1_VOLTAGE, _address)); - _powerMeter2Voltage = 0.0; - _powerMeter3Voltage = 0.0; - _powerMeterImport = static_cast(sdm.readVal(SDM_IMPORT_ACTIVE_ENERGY, _address)); - _powerMeterExport = static_cast(sdm.readVal(SDM_EXPORT_ACTIVE_ENERGY, _address)); + uint8_t _address = config.PowerMeter.SdmAddress; + Source configuredSource = static_cast(config.PowerMeter.Source); + + if (configuredSource == Source::SDM1PH) { + if (!_upSdm) { return; } + + // this takes a "very long" time as each readVal() is a synchronous + // exchange of serial messages. cache the values and write later. + auto phase1Power = _upSdm->readVal(SDM_PHASE_1_POWER, _address); + auto phase1Voltage = _upSdm->readVal(SDM_PHASE_1_VOLTAGE, _address); + auto energyImport = _upSdm->readVal(SDM_IMPORT_ACTIVE_ENERGY, _address); + auto energyExport = _upSdm->readVal(SDM_EXPORT_ACTIVE_ENERGY, _address); + + std::lock_guard l(_mutex); + _powerMeter1Power = static_cast(phase1Power); + _powerMeter2Power = 0; + _powerMeter3Power = 0; + _powerMeter1Voltage = static_cast(phase1Voltage); + _powerMeter2Voltage = 0; + _powerMeter3Voltage = 0; + _powerMeterImport = static_cast(energyImport); + _powerMeterExport = static_cast(energyExport); _lastPowerMeterUpdate = millis(); } - else if (config.PowerMeter.Source == SOURCE_SDM3PH) { - _powerMeter1Power = static_cast(sdm.readVal(SDM_PHASE_1_POWER, _address)); - _powerMeter2Power = static_cast(sdm.readVal(SDM_PHASE_2_POWER, _address)); - _powerMeter3Power = static_cast(sdm.readVal(SDM_PHASE_3_POWER, _address)); - _powerMeter1Voltage = static_cast(sdm.readVal(SDM_PHASE_1_VOLTAGE, _address)); - _powerMeter2Voltage = static_cast(sdm.readVal(SDM_PHASE_2_VOLTAGE, _address)); - _powerMeter3Voltage = static_cast(sdm.readVal(SDM_PHASE_3_VOLTAGE, _address)); - _powerMeterImport = static_cast(sdm.readVal(SDM_IMPORT_ACTIVE_ENERGY, _address)); - _powerMeterExport = static_cast(sdm.readVal(SDM_EXPORT_ACTIVE_ENERGY, _address)); + else if (configuredSource == Source::SDM3PH) { + if (!_upSdm) { return; } + + // this takes a "very long" time as each readVal() is a synchronous + // exchange of serial messages. cache the values and write later. + auto phase1Power = _upSdm->readVal(SDM_PHASE_1_POWER, _address); + auto phase2Power = _upSdm->readVal(SDM_PHASE_2_POWER, _address); + auto phase3Power = _upSdm->readVal(SDM_PHASE_3_POWER, _address); + auto phase1Voltage = _upSdm->readVal(SDM_PHASE_1_VOLTAGE, _address); + auto phase2Voltage = _upSdm->readVal(SDM_PHASE_2_VOLTAGE, _address); + auto phase3Voltage = _upSdm->readVal(SDM_PHASE_3_VOLTAGE, _address); + auto energyImport = _upSdm->readVal(SDM_IMPORT_ACTIVE_ENERGY, _address); + auto energyExport = _upSdm->readVal(SDM_EXPORT_ACTIVE_ENERGY, _address); + + std::lock_guard l(_mutex); + _powerMeter1Power = static_cast(phase1Power); + _powerMeter2Power = static_cast(phase2Power); + _powerMeter3Power = static_cast(phase3Power); + _powerMeter1Voltage = static_cast(phase1Voltage); + _powerMeter2Voltage = static_cast(phase2Voltage); + _powerMeter3Voltage = static_cast(phase3Voltage); + _powerMeterImport = static_cast(energyImport); + _powerMeterExport = static_cast(energyExport); _lastPowerMeterUpdate = millis(); } - else if (config.PowerMeter.Source == SOURCE_HTTP) { + else if (configuredSource == Source::HTTP) { if (HttpPowerMeter.updateValues()) { + std::lock_guard l(_mutex); _powerMeter1Power = HttpPowerMeter.getPower(1); _powerMeter2Power = HttpPowerMeter.getPower(2); _powerMeter3Power = HttpPowerMeter.getPower(3); _lastPowerMeterUpdate = millis(); } } + else if (configuredSource == Source::SMAHM2) { + std::lock_guard l(_mutex); + _powerMeter1Power = SMA_HM.getPowerL1(); + _powerMeter2Power = SMA_HM.getPowerL2(); + _powerMeter3Power = SMA_HM.getPowerL3(); + _lastPowerMeterUpdate = millis(); + } } bool PowerMeterClass::smlReadLoop() { - while (inputSerial.available()) { + while (_upSmlSerial->available()) { double readVal = 0; - unsigned char smlCurrentChar = inputSerial.read(); + unsigned char smlCurrentChar = _upSmlSerial->read(); sml_states_t smlCurrentState = smlState(smlCurrentChar); if (smlCurrentState == SML_LISTEND) { for (auto& handler: smlHandlerList) { diff --git a/src/SMA_HM.cpp b/src/SMA_HM.cpp new file mode 100644 index 000000000..7a3a9fe2e --- /dev/null +++ b/src/SMA_HM.cpp @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2024 Holger-Steffen Stapf + */ +#include "SMA_HM.h" +#include +#include "Configuration.h" +#include "NetworkSettings.h" +#include +#include "MessageOutput.h" + +unsigned int multicastPort = 9522; // local port to listen on +IPAddress multicastIP(239, 12, 255, 254); +WiFiUDP SMAUdp; + +constexpr uint32_t interval = 1000; + +SMA_HMClass SMA_HM; + +void SMA_HMClass::Soutput(int kanal, int index, int art, int tarif, + char const* name, float value, uint32_t timestamp) +{ + if (!_verboseLogging) { return; } + + MessageOutput.printf("SMA_HM: %s = %.1f (timestamp %d)\r\n", + name, value, timestamp); +} + +void SMA_HMClass::init(Scheduler& scheduler, bool verboseLogging) +{ + _verboseLogging = verboseLogging; + scheduler.addTask(_loopTask); + _loopTask.setCallback(std::bind(&SMA_HMClass::loop, this)); + _loopTask.setIterations(TASK_FOREVER); + _loopTask.enable(); + SMAUdp.begin(multicastPort); + SMAUdp.beginMulticast(multicastIP, multicastPort); +} + +void SMA_HMClass::loop() +{ + uint32_t currentMillis = millis(); + if (currentMillis - _previousMillis >= interval) { + _previousMillis = currentMillis; + event1(); + } +} + +uint8_t* SMA_HMClass::decodeGroup(uint8_t* offset, uint16_t grouplen) +{ + float Pbezug = 0; + float BezugL1 = 0; + float BezugL2 = 0; + float BezugL3 = 0; + float Peinspeisung = 0; + float EinspeisungL1 = 0; + float EinspeisungL2 = 0; + float EinspeisungL3 = 0; + + uint8_t* endOfGroup = offset + grouplen; + + // not used: uint16_t protocolID = (offset[0] << 8) + offset[1]; + offset += 2; + + // not used: uint16_t susyID = (offset[0] << 8) + offset[1]; + offset += 2; + + _serial = (offset[0] << 24) + (offset[1] << 16) + (offset[2] << 8) + offset[3]; + offset += 4; + + uint32_t timestamp = (offset[0] << 24) + (offset[1] << 16) + (offset[2] << 8) + offset[3]; + offset += 4; + + unsigned count = 0; + while (offset < endOfGroup) { + uint8_t kanal = offset[0]; + uint8_t index = offset[1]; + uint8_t art = offset[2]; + uint8_t tarif = offset[3]; + offset += 4; + + if (kanal == 144) { + // Optional: Versionsnummer auslesen... aber interessiert die? + offset += 4; + continue; + } + + if (art == 8) { + offset += 8; + continue; + } + + if (art == 4) { + uint32_t data = (offset[0] << 24) + + (offset[1] << 16) + + (offset[2] << 8) + + offset[3]; + offset += 4; + + switch (index) { + case (1): + Pbezug = data * 0.1; + ++count; + break; + case (2): + Peinspeisung = data * 0.1; + ++count; + break; + case (21): + BezugL1 = data * 0.1; + ++count; + break; + case (22): + EinspeisungL1 = data * 0.1; + ++count; + break; + case (41): + BezugL2 = data * 0.1; + ++count; + break; + case (42): + EinspeisungL2 = data * 0.1; + ++count; + break; + case (61): + BezugL3 = data * 0.1; + ++count; + break; + case (62): + EinspeisungL3 = data * 0.1; + ++count; + break; + default: + break; + } + + if (count == 8) { + _powerMeterPower = Pbezug - Peinspeisung; + _powerMeterL1 = BezugL1 - EinspeisungL1; + _powerMeterL2 = BezugL2 - EinspeisungL2; + _powerMeterL3 = BezugL3 - EinspeisungL3; + Soutput(kanal, index, art, tarif, "Leistung", _powerMeterPower, timestamp); + Soutput(kanal, index, art, tarif, "Leistung L1", _powerMeterL1, timestamp); + Soutput(kanal, index, art, tarif, "Leistung L2", _powerMeterL2, timestamp); + Soutput(kanal, index, art, tarif, "Leistung L3", _powerMeterL3, timestamp); + count = 0; + } + + continue; + } + + MessageOutput.printf("SMA_HM: Skipped unknown measurement: %d %d %d %d\r\n", + kanal, index, art, tarif); + offset += art; + } + + return offset; +} + +void SMA_HMClass::event1() +{ + int packetSize = SMAUdp.parsePacket(); + if (!packetSize) { return; } + + uint8_t buffer[1024]; + int rSize = SMAUdp.read(buffer, 1024); + if (buffer[0] != 'S' || buffer[1] != 'M' || buffer[2] != 'A') { + MessageOutput.println("SMA_HM: Not an SMA packet?"); + return; + } + + uint16_t grouplen; + uint16_t grouptag; + uint8_t* offset = buffer + 4; // skips the header 'SMA\0' + + do { + grouplen = (offset[0] << 8) + offset[1]; + grouptag = (offset[2] << 8) + offset[3]; + offset += 4; + + if (grouplen == 0xffff) return; + + if (grouptag == 0x02A0 && grouplen == 4) { + offset += 4; + continue; + } + + if (grouptag == 0x0010) { + offset = decodeGroup(offset, grouplen); + continue; + } + + if (grouptag == 0) { + // end marker + offset += grouplen; + continue; + } + + MessageOutput.printf("SMA_HM: Unhandled group 0x%04x with length %d\r\n", + grouptag, grouplen); + offset += grouplen; + } while (grouplen > 0 && offset + 4 < buffer + rSize); +} diff --git a/src/SerialPortManager.cpp b/src/SerialPortManager.cpp new file mode 100644 index 000000000..c4eb8514a --- /dev/null +++ b/src/SerialPortManager.cpp @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "SerialPortManager.h" +#include "MessageOutput.h" + +#define MAX_CONTROLLERS 3 + +SerialPortManagerClass SerialPortManager; + +bool SerialPortManagerClass::allocateBatteryPort(int port) +{ + return allocatePort(port, Owner::BATTERY); +} + +bool SerialPortManagerClass::allocateMpptPort(int port) +{ + return allocatePort(port, Owner::MPPT); +} + +bool SerialPortManagerClass::allocatePort(uint8_t port, Owner owner) +{ + if (port >= MAX_CONTROLLERS) { + MessageOutput.printf("[SerialPortManager] Invalid serial port = %d \r\n", port); + return false; + } + + return allocatedPorts.insert({port, owner}).second; +} + +void SerialPortManagerClass::invalidateBatteryPort() +{ + invalidate(Owner::BATTERY); +} + +void SerialPortManagerClass::invalidateMpptPorts() +{ + invalidate(Owner::MPPT); +} + +void SerialPortManagerClass::invalidate(Owner owner) +{ + for (auto it = allocatedPorts.begin(); it != allocatedPorts.end();) { + if (it->second == owner) { + MessageOutput.printf("[SerialPortManager] Removing port = %d, owner = %s \r\n", it->first, print(owner)); + it = allocatedPorts.erase(it); + } else { + ++it; + } + } +} + +const char* SerialPortManagerClass::print(Owner owner) +{ + switch (owner) { + case BATTERY: + return "BATTERY"; + case MPPT: + return "MPPT"; + } + return "unknown"; +} diff --git a/src/Utils.cpp b/src/Utils.cpp index 7ad072938..938b002da 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -79,6 +79,16 @@ bool Utils::checkJsonAlloc(const DynamicJsonDocument& doc, const char* function, return true; } +bool Utils::checkJsonOverflow(const DynamicJsonDocument& doc, const char* function, const uint16_t line) +{ + if (doc.overflowed()) { + MessageOutput.printf("DynamicJsonDocument overflowed: %s, %d\r\n", function, line); + return true; + } + + return false; +} + /// @brief Remove all files but the PINMAPPING_FILENAME void Utils::removeAllFiles() { diff --git a/src/VictronMppt.cpp b/src/VictronMppt.cpp index c4dd0bd5a..e39cf3aad 100644 --- a/src/VictronMppt.cpp +++ b/src/VictronMppt.cpp @@ -3,13 +3,14 @@ #include "Configuration.h" #include "PinMapping.h" #include "MessageOutput.h" +#include "SerialPortManager.h" VictronMpptClass VictronMppt; void VictronMpptClass::init(Scheduler& scheduler) { scheduler.addTask(_loopTask); - _loopTask.setCallback(std::bind(&VictronMpptClass::loop, this)); + _loopTask.setCallback([this] { loop(); }); _loopTask.setIterations(TASK_FOREVER); _loopTask.enable(); @@ -21,24 +22,41 @@ void VictronMpptClass::updateSettings() std::lock_guard lock(_mutex); _controllers.clear(); + SerialPortManager.invalidateMpptPorts(); CONFIG_T& config = Configuration.get(); if (!config.Vedirect.Enabled) { return; } const PinMapping_t& pin = PinMapping.get(); - int8_t rx = pin.victron_rx; - int8_t tx = pin.victron_tx; - MessageOutput.printf("[VictronMppt] rx = %d, tx = %d\r\n", rx, tx); + int hwSerialPort = 1; + bool initSuccess = initController(pin.victron_rx, pin.victron_tx, config.Vedirect.VerboseLogging, hwSerialPort); + if (initSuccess) { + hwSerialPort++; + } + + initController(pin.victron_rx2, pin.victron_tx2, config.Vedirect.VerboseLogging, hwSerialPort); +} + +bool VictronMpptClass::initController(int8_t rx, int8_t tx, bool logging, int hwSerialPort) +{ + MessageOutput.printf("[VictronMppt] rx = %d, tx = %d, hwSerialPort = %d\r\n", rx, tx, hwSerialPort); if (rx < 0) { - MessageOutput.println("[VictronMppt] invalid pin config"); - return; + MessageOutput.printf("[VictronMppt] invalid pin config rx = %d, tx = %d\r\n", rx, tx); + return false; + } + + if (!SerialPortManager.allocateMpptPort(hwSerialPort)) { + MessageOutput.printf("[VictronMppt] Serial port %d already in use. Initialization aborted!\r\n", + hwSerialPort); + return false; } auto upController = std::make_unique(); - upController->init(rx, tx, &MessageOutput, config.Vedirect.VerboseLogging); + upController->init(rx, tx, &MessageOutput, logging, hwSerialPort); _controllers.push_back(std::move(upController)); + return true; } void VictronMpptClass::loop() @@ -54,13 +72,24 @@ bool VictronMpptClass::isDataValid() const { std::lock_guard lock(_mutex); - for (auto const& upController : _controllers) { + for (auto const& upController: _controllers) { if (!upController->isDataValid()) { return false; } } return !_controllers.empty(); } +bool VictronMpptClass::isDataValid(size_t idx) const +{ + std::lock_guard lock(_mutex); + + if (_controllers.empty() || idx >= _controllers.size()) { + return false; + } + + return _controllers[idx]->isDataValid(); +} + uint32_t VictronMpptClass::getDataAgeMillis() const { std::lock_guard lock(_mutex); @@ -81,17 +110,26 @@ uint32_t VictronMpptClass::getDataAgeMillis() const return age; } -VeDirectMpptController::spData_t VictronMpptClass::getData(size_t idx) const +uint32_t VictronMpptClass::getDataAgeMillis(size_t idx) const +{ + std::lock_guard lock(_mutex); + + if (_controllers.empty() || idx >= _controllers.size()) { return 0; } + + return millis() - _controllers[idx]->getLastUpdate(); +} + +std::optional VictronMpptClass::getData(size_t idx) const { std::lock_guard lock(_mutex); if (_controllers.empty() || idx >= _controllers.size()) { MessageOutput.printf("ERROR: MPPT controller index %d is out of bounds (%d controllers)\r\n", - idx, _controllers.size()); - return std::make_shared(); + idx, _controllers.size()); + return std::nullopt; } - return _controllers[idx]->getData(); + return std::optional{_controllers[idx]->getData()}; } int32_t VictronMpptClass::getPowerOutputWatts() const @@ -99,6 +137,7 @@ int32_t VictronMpptClass::getPowerOutputWatts() const int32_t sum = 0; for (const auto& upController : _controllers) { + if (!upController->isDataValid()) { continue; } sum += upController->getData()->P; } @@ -110,6 +149,7 @@ int32_t VictronMpptClass::getPanelPowerWatts() const int32_t sum = 0; for (const auto& upController : _controllers) { + if (!upController->isDataValid()) { continue; } sum += upController->getData()->PPV; } @@ -121,6 +161,7 @@ double VictronMpptClass::getYieldTotal() const double sum = 0; for (const auto& upController : _controllers) { + if (!upController->isDataValid()) { continue; } sum += upController->getData()->H19; } @@ -132,6 +173,7 @@ double VictronMpptClass::getYieldDay() const double sum = 0; for (const auto& upController : _controllers) { + if (!upController->isDataValid()) { continue; } sum += upController->getData()->H20; } @@ -143,6 +185,7 @@ double VictronMpptClass::getOutputVoltage() const double min = -1; for (const auto& upController : _controllers) { + if (!upController->isDataValid()) { continue; } double volts = upController->getData()->V; if (min == -1) { min = volts; } min = std::min(min, volts); diff --git a/src/WebApi_battery.cpp b/src/WebApi_battery.cpp index 9e2230c4e..3f26d83cc 100644 --- a/src/WebApi_battery.cpp +++ b/src/WebApi_battery.cpp @@ -8,6 +8,7 @@ #include "Battery.h" #include "Configuration.h" #include "MqttHandleBatteryHass.h" +#include "MqttHandlePowerLimiterHass.h" #include "WebApi.h" #include "WebApi_battery.h" #include "WebApi_errors.h" @@ -114,4 +115,7 @@ void WebApiBatteryClass::onAdminPost(AsyncWebServerRequest* request) Battery.updateSettings(); MqttHandleBatteryHass.forceUpdate(); + + // potentially make SoC thresholds auto-discoverable + MqttHandlePowerLimiterHass.forceUpdate(); } diff --git a/src/WebApi_device.cpp b/src/WebApi_device.cpp index cc08dfaab..9ab8d4fa2 100644 --- a/src/WebApi_device.cpp +++ b/src/WebApi_device.cpp @@ -86,9 +86,11 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request) led["brightness"] = config.Led_Single[i].Brightness; } - JsonObject victronPinObj = curPin.createNestedObject("victron"); + auto victronPinObj = curPin.createNestedObject("victron"); victronPinObj["rx"] = pin.victron_rx; victronPinObj["tx"] = pin.victron_tx; + victronPinObj["rx2"] = pin.victron_rx2; + victronPinObj["tx2"] = pin.victron_tx2; JsonObject batteryPinObj = curPin.createNestedObject("battery"); batteryPinObj["rx"] = pin.battery_rx; diff --git a/src/WebApi_dtu.cpp b/src/WebApi_dtu.cpp index bbdfd0708..2ccb20b27 100644 --- a/src/WebApi_dtu.cpp +++ b/src/WebApi_dtu.cpp @@ -131,7 +131,10 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) return; } - if (root["serial"].as() == 0) { + // Interpret the string as a hex value and convert it to uint64_t + const uint64_t serial = strtoll(root["serial"].as().c_str(), NULL, 16); + + if (serial == 0) { retMsg["message"] = "Serial cannot be zero!"; retMsg["code"] = WebApiError::DtuSerialZero; response->setLength(); @@ -187,8 +190,7 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) CONFIG_T& config = Configuration.get(); - // Interpret the string as a hex value and convert it to uint64_t - config.Dtu.Serial = strtoll(root["serial"].as().c_str(), NULL, 16); + config.Dtu.Serial = serial; config.Dtu.PollInterval = root["pollinterval"].as(); config.Dtu.VerboseLogging = root["verbose_logging"].as(); config.Dtu.Nrf.PaLevel = root["nrf_palevel"].as(); diff --git a/src/WebApi_inverter.cpp b/src/WebApi_inverter.cpp index 32a472350..5f5e41016 100644 --- a/src/WebApi_inverter.cpp +++ b/src/WebApi_inverter.cpp @@ -129,7 +129,10 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) return; } - if (root["serial"].as() == 0) { + // Interpret the string as a hex value and convert it to uint64_t + const uint64_t serial = strtoll(root["serial"].as().c_str(), NULL, 16); + + if (serial == 0) { retMsg["message"] = "Serial must be a number > 0!"; retMsg["code"] = WebApiError::InverterSerialZero; response->setLength(); @@ -158,7 +161,7 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) } // Interpret the string as a hex value and convert it to uint64_t - inverter->Serial = strtoll(root["serial"].as().c_str(), NULL, 16); + inverter->Serial = serial; strncpy(inverter->Name, root["name"].as().c_str(), INV_MAX_NAME_STRLEN); @@ -233,7 +236,10 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) return; } - if (root["serial"].as() == 0) { + // Interpret the string as a hex value and convert it to uint64_t + const uint64_t serial = strtoll(root["serial"].as().c_str(), NULL, 16); + + if (serial == 0) { retMsg["message"] = "Serial must be a number > 0!"; retMsg["code"] = WebApiError::InverterSerialZero; response->setLength(); @@ -261,7 +267,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) INVERTER_CONFIG_T& inverter = Configuration.get().Inverter[root["id"].as()]; - uint64_t new_serial = strtoll(root["serial"].as().c_str(), NULL, 16); + uint64_t new_serial = serial; uint64_t old_serial = inverter.Serial; // Interpret the string as a hex value and convert it to uint64_t @@ -380,8 +386,7 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request) Hoymiles.removeInverterBySerial(inverter.Serial); - inverter.Serial = 0; - strncpy(inverter.Name, "", sizeof(inverter.Name)); + Configuration.deleteInverterById(inverter_id); WebApi.writeConfig(retMsg, WebApiError::InverterDeleted, "Inverter deleted!"); diff --git a/src/WebApi_limit.cpp b/src/WebApi_limit.cpp index 1d9c111a5..b5b9e1726 100644 --- a/src/WebApi_limit.cpp +++ b/src/WebApi_limit.cpp @@ -100,7 +100,10 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) return; } - if (root["serial"].as() == 0) { + // Interpret the string as a hex value and convert it to uint64_t + const uint64_t serial = strtoll(root["serial"].as().c_str(), NULL, 16); + + if (serial == 0) { retMsg["message"] = "Serial must be a number > 0!"; retMsg["code"] = WebApiError::LimitSerialZero; response->setLength(); @@ -129,7 +132,6 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) return; } - uint64_t serial = strtoll(root["serial"].as().c_str(), NULL, 16); uint16_t limit = root["limit_value"].as(); PowerLimitControlType type = root["limit_type"].as(); diff --git a/src/WebApi_power.cpp b/src/WebApi_power.cpp index b51967894..08fe9c051 100644 --- a/src/WebApi_power.cpp +++ b/src/WebApi_power.cpp @@ -93,7 +93,10 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) return; } - if (root["serial"].as() == 0) { + // Interpret the string as a hex value and convert it to uint64_t + const uint64_t serial = strtoll(root["serial"].as().c_str(), NULL, 16); + + if (serial == 0) { retMsg["message"] = "Serial must be a number > 0!"; retMsg["code"] = WebApiError::PowerSerialZero; response->setLength(); @@ -101,7 +104,6 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) return; } - uint64_t serial = strtoll(root["serial"].as().c_str(), NULL, 16); auto inv = Hoymiles.getInverterBySerial(serial); if (inv == nullptr) { retMsg["message"] = "Invalid inverter specified!"; diff --git a/src/WebApi_powerlimiter.cpp b/src/WebApi_powerlimiter.cpp index df530ca8b..81987a231 100644 --- a/src/WebApi_powerlimiter.cpp +++ b/src/WebApi_powerlimiter.cpp @@ -7,10 +7,7 @@ #include "ArduinoJson.h" #include "AsyncJson.h" #include "Configuration.h" -#include "MqttHandleHass.h" -#include "MqttHandleVedirectHass.h" -#include "MqttSettings.h" -#include "PowerMeter.h" +#include "MqttHandlePowerLimiterHass.h" #include "PowerLimiter.h" #include "WebApi.h" #include "helper.h" @@ -25,21 +22,24 @@ void WebApiPowerLimiterClass::init(AsyncWebServer& server, Scheduler& scheduler) _server->on("/api/powerlimiter/status", HTTP_GET, std::bind(&WebApiPowerLimiterClass::onStatus, this, _1)); _server->on("/api/powerlimiter/config", HTTP_GET, std::bind(&WebApiPowerLimiterClass::onAdminGet, this, _1)); _server->on("/api/powerlimiter/config", HTTP_POST, std::bind(&WebApiPowerLimiterClass::onAdminPost, this, _1)); + _server->on("/api/powerlimiter/metadata", HTTP_GET, std::bind(&WebApiPowerLimiterClass::onMetaData, this, _1)); } void WebApiPowerLimiterClass::onStatus(AsyncWebServerRequest* request) { - AsyncJsonResponse* response = new AsyncJsonResponse(); + auto const& config = Configuration.get(); + + AsyncJsonResponse* response = new AsyncJsonResponse(false, 512); auto& root = response->getRoot(); - const CONFIG_T& config = Configuration.get(); root["enabled"] = config.PowerLimiter.Enabled; root["verbose_logging"] = config.PowerLimiter.VerboseLogging; root["solar_passthrough_enabled"] = config.PowerLimiter.SolarPassThroughEnabled; root["solar_passthrough_losses"] = config.PowerLimiter.SolarPassThroughLosses; - root["battery_drain_strategy"] = config.PowerLimiter.BatteryDrainStategy; + root["battery_always_use_at_night"] = config.PowerLimiter.BatteryAlwaysUseAtNight; root["is_inverter_behind_powermeter"] = config.PowerLimiter.IsInverterBehindPowerMeter; - root["inverter_id"] = config.PowerLimiter.InverterId; + root["is_inverter_solar_powered"] = config.PowerLimiter.IsInverterSolarPowered; + root["inverter_serial"] = String(config.PowerLimiter.InverterId); root["inverter_channel_id"] = config.PowerLimiter.InverterChannelId; root["target_power_consumption"] = config.PowerLimiter.TargetPowerConsumption; root["target_power_consumption_hysteresis"] = config.PowerLimiter.TargetPowerConsumptionHysteresis; @@ -60,6 +60,54 @@ void WebApiPowerLimiterClass::onStatus(AsyncWebServerRequest* request) request->send(response); } +void WebApiPowerLimiterClass::onMetaData(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentials(request)) { return; } + + auto const& config = Configuration.get(); + + size_t invAmount = 0; + for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { + if (config.Inverter[i].Serial != 0) { ++invAmount; } + } + + AsyncJsonResponse* response = new AsyncJsonResponse(false, 256 + 256 * invAmount); + auto& root = response->getRoot(); + + root["power_meter_enabled"] = config.PowerMeter.Enabled; + root["battery_enabled"] = config.Battery.Enabled; + root["charge_controller_enabled"] = config.Vedirect.Enabled; + + JsonObject inverters = root.createNestedObject("inverters"); + for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { + if (config.Inverter[i].Serial == 0) { continue; } + + // we use the integer (base 10) representation of the inverter serial, + // rather than the hex represenation as used when handling the inverter + // serial elsewhere in the web application, because in this case, the + // serial is actually not displayed but only used as a value/index. + JsonObject obj = inverters.createNestedObject(String(config.Inverter[i].Serial)); + obj["pos"] = i; + obj["name"] = String(config.Inverter[i].Name); + obj["poll_enable"] = config.Inverter[i].Poll_Enable; + obj["poll_enable_night"] = config.Inverter[i].Poll_Enable_Night; + obj["command_enable"] = config.Inverter[i].Command_Enable; + obj["command_enable_night"] = config.Inverter[i].Command_Enable_Night; + + obj["type"] = "Unknown"; + obj["channels"] = 1; + auto inv = Hoymiles.getInverterBySerial(config.Inverter[i].Serial); + if (inv != nullptr) { + obj["type"] = inv->typeName(); + auto channels = inv->Statistics()->getChannelsByType(TYPE_DC); + obj["channels"] = channels.size(); + } + } + + response->setLength(); + request->send(response); +} + void WebApiPowerLimiterClass::onAdminGet(AsyncWebServerRequest* request) { if (!WebApi.checkCredentials(request)) { @@ -105,13 +153,14 @@ void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request) return; } - if (!(root.containsKey("enabled") - && root.containsKey("lower_power_limit") - && root.containsKey("inverter_id") - && root.containsKey("inverter_channel_id") - && root.containsKey("target_power_consumption") - && root.containsKey("target_power_consumption_hysteresis") - )) { + // we were not actually checking for all the keys we (unconditionally) + // access below for a long time, and it is technically not needed if users + // use the web application to submit settings. the web app will always + // submit all keys. users who send HTTP requests manually need to beware + // anyways to always include the keys accessed below. if we wanted to + // support a simpler API, like only sending the "enabled" key which only + // changes that key, we need to refactor all of the code below. + if (!root.containsKey("enabled")) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; response->setLength(); @@ -119,33 +168,43 @@ void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request) return; } - CONFIG_T& config = Configuration.get(); config.PowerLimiter.Enabled = root["enabled"].as(); PowerLimiter.setMode(PowerLimiterClass::Mode::Normal); // User input sets PL to normal operation config.PowerLimiter.VerboseLogging = root["verbose_logging"].as(); - config.PowerLimiter.SolarPassThroughEnabled = root["solar_passthrough_enabled"].as(); - config.PowerLimiter.SolarPassThroughLosses = root["solar_passthrough_losses"].as(); - config.PowerLimiter.BatteryDrainStategy= root["battery_drain_strategy"].as(); + + if (config.Vedirect.Enabled) { + config.PowerLimiter.SolarPassThroughEnabled = root["solar_passthrough_enabled"].as(); + config.PowerLimiter.SolarPassThroughLosses = root["solar_passthrough_losses"].as(); + config.PowerLimiter.BatteryAlwaysUseAtNight= root["battery_always_use_at_night"].as(); + config.PowerLimiter.FullSolarPassThroughStartVoltage = static_cast(root["full_solar_passthrough_start_voltage"].as() * 100) / 100.0; + config.PowerLimiter.FullSolarPassThroughStopVoltage = static_cast(root["full_solar_passthrough_stop_voltage"].as() * 100) / 100.0; + } + config.PowerLimiter.IsInverterBehindPowerMeter = root["is_inverter_behind_powermeter"].as(); - config.PowerLimiter.InverterId = root["inverter_id"].as(); + config.PowerLimiter.IsInverterSolarPowered = root["is_inverter_solar_powered"].as(); + config.PowerLimiter.InverterId = root["inverter_serial"].as(); config.PowerLimiter.InverterChannelId = root["inverter_channel_id"].as(); config.PowerLimiter.TargetPowerConsumption = root["target_power_consumption"].as(); config.PowerLimiter.TargetPowerConsumptionHysteresis = root["target_power_consumption_hysteresis"].as(); config.PowerLimiter.LowerPowerLimit = root["lower_power_limit"].as(); config.PowerLimiter.UpperPowerLimit = root["upper_power_limit"].as(); - config.PowerLimiter.IgnoreSoc = root["ignore_soc"].as(); - config.PowerLimiter.BatterySocStartThreshold = root["battery_soc_start_threshold"].as(); - config.PowerLimiter.BatterySocStopThreshold = root["battery_soc_stop_threshold"].as(); + + if (config.Battery.Enabled) { + config.PowerLimiter.IgnoreSoc = root["ignore_soc"].as(); + config.PowerLimiter.BatterySocStartThreshold = root["battery_soc_start_threshold"].as(); + config.PowerLimiter.BatterySocStopThreshold = root["battery_soc_stop_threshold"].as(); + if (config.Vedirect.Enabled) { + config.PowerLimiter.FullSolarPassThroughSoc = root["full_solar_passthrough_soc"].as(); + } + } + config.PowerLimiter.VoltageStartThreshold = root["voltage_start_threshold"].as(); config.PowerLimiter.VoltageStartThreshold = static_cast(config.PowerLimiter.VoltageStartThreshold * 100) / 100.0; config.PowerLimiter.VoltageStopThreshold = root["voltage_stop_threshold"].as(); config.PowerLimiter.VoltageStopThreshold = static_cast(config.PowerLimiter.VoltageStopThreshold * 100) / 100.0; config.PowerLimiter.VoltageLoadCorrectionFactor = root["voltage_load_correction_factor"].as(); config.PowerLimiter.RestartHour = root["inverter_restart_hour"].as(); - config.PowerLimiter.FullSolarPassThroughSoc = root["full_solar_passthrough_soc"].as(); - config.PowerLimiter.FullSolarPassThroughStartVoltage = static_cast(root["full_solar_passthrough_start_voltage"].as() * 100) / 100.0; - config.PowerLimiter.FullSolarPassThroughStopVoltage = static_cast(root["full_solar_passthrough_stop_voltage"].as() * 100) / 100.0; WebApi.writeConfig(retMsg); @@ -153,4 +212,7 @@ void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request) request->send(response); PowerLimiter.calcNextInverterRestart(); + + // potentially make thresholds auto-discoverable + MqttHandlePowerLimiterHass.forceUpdate(); } diff --git a/src/WebApi_powermeter.cpp b/src/WebApi_powermeter.cpp index 7340df464..137168baf 100644 --- a/src/WebApi_powermeter.cpp +++ b/src/WebApi_powermeter.cpp @@ -118,7 +118,7 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request) return; } - if (root["source"].as() == PowerMeter.SOURCE_HTTP) { + if (static_cast(root["source"].as()) == PowerMeterClass::Source::HTTP) { JsonArray http_phases = root["http_phases"]; for (uint8_t i = 0; i < http_phases.size(); i++) { JsonObject phase = http_phases[i].as(); diff --git a/src/WebApi_vedirect.cpp b/src/WebApi_vedirect.cpp index 088be259f..4e1e352b2 100644 --- a/src/WebApi_vedirect.cpp +++ b/src/WebApi_vedirect.cpp @@ -10,6 +10,7 @@ #include "WebApi.h" #include "WebApi_errors.h" #include "helper.h" +#include "MqttHandlePowerLimiterHass.h" void WebApiVedirectClass::init(AsyncWebServer& server, Scheduler& scheduler) { @@ -118,4 +119,7 @@ void WebApiVedirectClass::onVedirectAdminPost(AsyncWebServerRequest* request) request->send(response); VictronMppt.updateSettings(); + + // potentially make solar passthrough thresholds auto-discoverable + MqttHandlePowerLimiterHass.forceUpdate(); } diff --git a/src/WebApi_ws_Huawei.cpp b/src/WebApi_ws_Huawei.cpp index 1bf6870e9..c674e05d6 100644 --- a/src/WebApi_ws_Huawei.cpp +++ b/src/WebApi_ws_Huawei.cpp @@ -64,6 +64,8 @@ void WebApiWsHuaweiLiveClass::sendDataTaskCb() JsonVariant var = root; generateJsonResponse(var); + if (Utils::checkJsonOverflow(root, __FUNCTION__, __LINE__)) { return; } + String buffer; serializeJson(root, buffer); diff --git a/src/WebApi_ws_battery.cpp b/src/WebApi_ws_battery.cpp index 669df955f..39aaf7289 100644 --- a/src/WebApi_ws_battery.cpp +++ b/src/WebApi_ws_battery.cpp @@ -67,6 +67,11 @@ void WebApiWsBatteryLiveClass::sendDataTaskCb() JsonVariant var = root; generateJsonResponse(var); + if (Utils::checkJsonOverflow(root, __FUNCTION__, __LINE__)) { return; } + + // battery provider does not generate a card, e.g., MQTT provider + if (root.isNull()) { return; } + String buffer; serializeJson(root, buffer); diff --git a/src/WebApi_ws_live.cpp b/src/WebApi_ws_live.cpp index d2ed35d9d..7f42bf443 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -56,25 +56,32 @@ void WebApiWsLiveClass::wsCleanupTaskCb() void WebApiWsLiveClass::generateOnBatteryJsonResponse(JsonVariant& root, bool all) { + auto const& config = Configuration.get(); auto constexpr halfOfAllMillis = std::numeric_limits::max() / 2; - if (all || (millis() - _lastPublishVictron) > VictronMppt.getDataAgeMillis()) { + auto victronAge = VictronMppt.getDataAgeMillis(); + if (all || (victronAge > 0 && (millis() - _lastPublishVictron) > victronAge)) { JsonObject vedirectObj = root.createNestedObject("vedirect"); - vedirectObj["enabled"] = Configuration.get().Vedirect.Enabled; - JsonObject totalVeObj = vedirectObj.createNestedObject("total"); + vedirectObj["enabled"] = config.Vedirect.Enabled; - addTotalField(totalVeObj, "Power", VictronMppt.getPanelPowerWatts(), "W", 1); - addTotalField(totalVeObj, "YieldDay", VictronMppt.getYieldDay() * 1000, "Wh", 0); - addTotalField(totalVeObj, "YieldTotal", VictronMppt.getYieldTotal(), "kWh", 2); + if (config.Vedirect.Enabled) { + JsonObject totalVeObj = vedirectObj.createNestedObject("total"); + addTotalField(totalVeObj, "Power", VictronMppt.getPanelPowerWatts(), "W", 1); + addTotalField(totalVeObj, "YieldDay", VictronMppt.getYieldDay() * 1000, "Wh", 0); + addTotalField(totalVeObj, "YieldTotal", VictronMppt.getYieldTotal(), "kWh", 2); + } if (!all) { _lastPublishVictron = millis(); } } if (all || (HuaweiCan.getLastUpdate() - _lastPublishHuawei) < halfOfAllMillis ) { JsonObject huaweiObj = root.createNestedObject("huawei"); - huaweiObj["enabled"] = Configuration.get().Huawei.Enabled; - const RectifierParameters_t * rp = HuaweiCan.get(); - addTotalField(huaweiObj, "Power", rp->output_power, "W", 2); + huaweiObj["enabled"] = config.Huawei.Enabled; + + if (config.Huawei.Enabled) { + const RectifierParameters_t * rp = HuaweiCan.get(); + addTotalField(huaweiObj, "Power", rp->output_power, "W", 2); + } if (!all) { _lastPublishHuawei = millis(); } } @@ -82,16 +89,22 @@ void WebApiWsLiveClass::generateOnBatteryJsonResponse(JsonVariant& root, bool al auto spStats = Battery.getStats(); if (all || spStats->updateAvailable(_lastPublishBattery)) { JsonObject batteryObj = root.createNestedObject("battery"); - batteryObj["enabled"] = Configuration.get().Battery.Enabled; - addTotalField(batteryObj, "soc", spStats->getSoC(), "%", 0); + batteryObj["enabled"] = config.Battery.Enabled; + + if (config.Battery.Enabled) { + addTotalField(batteryObj, "soc", spStats->getSoC(), "%", 0); + } if (!all) { _lastPublishBattery = millis(); } } if (all || (PowerMeter.getLastPowerMeterUpdate() - _lastPublishPowerMeter) < halfOfAllMillis) { JsonObject powerMeterObj = root.createNestedObject("power_meter"); - powerMeterObj["enabled"] = Configuration.get().PowerMeter.Enabled; - addTotalField(powerMeterObj, "Power", PowerMeter.getPowerTotal(false), "W", 1); + powerMeterObj["enabled"] = config.PowerMeter.Enabled; + + if (config.PowerMeter.Enabled) { + addTotalField(powerMeterObj, "Power", PowerMeter.getPowerTotal(false), "W", 1); + } if (!all) { _lastPublishPowerMeter = millis(); } } @@ -99,7 +112,7 @@ void WebApiWsLiveClass::generateOnBatteryJsonResponse(JsonVariant& root, bool al void WebApiWsLiveClass::sendOnBatteryStats() { - DynamicJsonDocument root(512); + DynamicJsonDocument root(1024); if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { return; } JsonVariant var = root; @@ -108,6 +121,10 @@ void WebApiWsLiveClass::sendOnBatteryStats() if (all) { _lastPublishOnBatteryFull = millis(); } generateOnBatteryJsonResponse(var, all); + if (root.isNull()) { return; } + + if (Utils::checkJsonOverflow(root, __FUNCTION__, __LINE__)) { return; } + String buffer; serializeJson(root, buffer); diff --git a/src/WebApi_ws_vedirect_live.cpp b/src/WebApi_ws_vedirect_live.cpp index df59172e8..967372ccc 100644 --- a/src/WebApi_ws_vedirect_live.cpp +++ b/src/WebApi_ws_vedirect_live.cpp @@ -32,7 +32,7 @@ void WebApiWsVedirectLiveClass::init(AsyncWebServer& server, Scheduler& schedule _server->addHandler(&_ws); _ws.onEvent(std::bind(&WebApiWsVedirectLiveClass::onWebsocketEvent, this, _1, _2, _3, _4, _5, _6)); - + scheduler.addTask(_wsCleanupTask); _wsCleanupTask.setCallback(std::bind(&WebApiWsVedirectLiveClass::wsCleanupTaskCb, this)); _wsCleanupTask.setIterations(TASK_FOREVER); @@ -52,32 +52,50 @@ void WebApiWsVedirectLiveClass::wsCleanupTaskCb() _ws.cleanupClients(); } +bool WebApiWsVedirectLiveClass::hasUpdate(size_t idx) +{ + auto dataAgeMillis = VictronMppt.getDataAgeMillis(idx); + if (dataAgeMillis == 0) { return false; } + auto publishAgeMillis = millis() - _lastPublish; + return dataAgeMillis < publishAgeMillis; +} + +uint16_t WebApiWsVedirectLiveClass::responseSize() const +{ + // estimated with ArduinoJson assistant + return VictronMppt.controllerAmount() * (1024 + 512) + 128/*DPL status and structure*/; +} + void WebApiWsVedirectLiveClass::sendDataTaskCb() { // do nothing if no WS client is connected - if (_ws.count() == 0) { - return; - } - - // we assume this loop to be running at least twice for every - // update from a VE.Direct MPPT data producer, so _dataAgeMillis - // acutally grows in between updates. - auto lastDataAgeMillis = _dataAgeMillis; - _dataAgeMillis = VictronMppt.getDataAgeMillis(); + if (_ws.count() == 0) { return; } // Update on ve.direct change or at least after 10 seconds - if (millis() - _lastWsPublish > (10 * 1000) || lastDataAgeMillis > _dataAgeMillis) { - + bool fullUpdate = (millis() - _lastFullPublish > (10 * 1000)); + bool updateAvailable = false; + if (!fullUpdate) { + for (size_t idx = 0; idx < VictronMppt.controllerAmount(); ++idx) { + if (hasUpdate(idx)) { + updateAvailable = true; + break; + } + } + } + + if (fullUpdate || updateAvailable) { try { std::lock_guard lock(_mutex); - DynamicJsonDocument root(_responseSize); + DynamicJsonDocument root(responseSize()); if (Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { JsonVariant var = root; - generateJsonResponse(var); + generateJsonResponse(var, fullUpdate); + + if (Utils::checkJsonOverflow(root, __FUNCTION__, __LINE__)) { return; } String buffer; serializeJson(root, buffer); - + if (Configuration.get().Security.AllowReadonly) { _ws.setAuthentication("", ""); } else { @@ -92,22 +110,50 @@ void WebApiWsVedirectLiveClass::sendDataTaskCb() } catch (const std::exception& exc) { MessageOutput.printf("Unknown exception in /api/vedirectlivedata/status. Reason: \"%s\".\r\n", exc.what()); } + } - _lastWsPublish = millis(); + if (fullUpdate) { + _lastFullPublish = millis(); } } -void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root) +void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root, bool fullUpdate) { - auto spMpptData = VictronMppt.getData(); + const JsonObject &array = root["vedirect"].createNestedObject("instances"); + root["vedirect"]["full_update"] = fullUpdate; + + for (size_t idx = 0; idx < VictronMppt.controllerAmount(); ++idx) { + std::optional spOptMpptData = VictronMppt.getData(idx); + if (!spOptMpptData.has_value()) { + continue; + } + if (!fullUpdate && !hasUpdate(idx)) { continue; } + + VeDirectMpptController::spData_t &spMpptData = spOptMpptData.value(); + + String serial(spMpptData->SER); + if (serial.isEmpty()) { continue; } // serial required as index + + const JsonObject &nested = array.createNestedObject(serial); + nested["data_age_ms"] = VictronMppt.getDataAgeMillis(idx); + populateJson(nested, spMpptData); + _lastPublish = millis(); + } + + // power limiter state + root["dpl"]["PLSTATE"] = -1; + if (Configuration.get().PowerLimiter.Enabled) + root["dpl"]["PLSTATE"] = PowerLimiter.getPowerLimiterState(); + root["dpl"]["PLLIMIT"] = PowerLimiter.getLastRequestedPowerLimit(); +} + +void WebApiWsVedirectLiveClass::populateJson(const JsonObject &root, const VeDirectMpptController::spData_t &spMpptData) { // device info - root["device"]["data_age"] = VictronMppt.getDataAgeMillis() / 1000; - root["device"]["age_critical"] = !VictronMppt.isDataValid(); root["device"]["PID"] = spMpptData->getPidAsString(); root["device"]["SER"] = spMpptData->SER; root["device"]["FW"] = spMpptData->FW; - root["device"]["LOAD"] = spMpptData->LOAD == true ? "ON" : "OFF"; + root["device"]["LOAD"] = spMpptData->LOAD ? "ON" : "OFF"; root["device"]["CS"] = spMpptData->getCsAsString(); root["device"]["ERR"] = spMpptData->getErrAsString(); root["device"]["OR"] = spMpptData->getOrAsString(); @@ -115,7 +161,7 @@ void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root) root["device"]["HSDS"]["v"] = spMpptData->HSDS; root["device"]["HSDS"]["u"] = "d"; - // battery info + // battery info root["output"]["P"]["v"] = spMpptData->P; root["output"]["P"]["u"] = "W"; root["output"]["P"]["d"] = 0; @@ -154,12 +200,6 @@ void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root) root["input"]["MaximumPowerYesterday"]["v"] = spMpptData->H23; root["input"]["MaximumPowerYesterday"]["u"] = "W"; root["input"]["MaximumPowerYesterday"]["d"] = 0; - - // power limiter state - root["dpl"]["PLSTATE"] = -1; - if (Configuration.get().PowerLimiter.Enabled) - root["dpl"]["PLSTATE"] = PowerLimiter.getPowerLimiterState(); - root["dpl"]["PLLIMIT"] = PowerLimiter.getLastRequestedPowerLimit(); } void WebApiWsVedirectLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len) @@ -184,10 +224,10 @@ void WebApiWsVedirectLiveClass::onLivedataStatus(AsyncWebServerRequest* request) } try { std::lock_guard lock(_mutex); - AsyncJsonResponse* response = new AsyncJsonResponse(false, _responseSize); + AsyncJsonResponse* response = new AsyncJsonResponse(false, responseSize()); auto& root = response->getRoot(); - generateJsonResponse(root); + generateJsonResponse(root, true/*fullUpdate*/); response->setLength(); request->send(response); @@ -199,4 +239,4 @@ void WebApiWsVedirectLiveClass::onLivedataStatus(AsyncWebServerRequest* request) MessageOutput.printf("Unknown exception in /api/vedirectlivedata/status. Reason: \"%s\".\r\n", exc.what()); WebApi.sendTooManyRequests(request); } -} \ No newline at end of file +} diff --git a/src/main.cpp b/src/main.cpp index c72bde4a7..e0a54c155 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -20,6 +20,7 @@ #include "MqttHandleVedirect.h" #include "MqttHandleHuawei.h" #include "MqttHandlePowerLimiter.h" +#include "MqttHandlePowerLimiterHass.h" #include "MqttSettings.h" #include "NetworkSettings.h" #include "NtpSettings.h" @@ -123,6 +124,7 @@ void setup() MqttHandleBatteryHass.init(scheduler); MqttHandleHuawei.init(scheduler); MqttHandlePowerLimiter.init(scheduler); + MqttHandlePowerLimiterHass.init(scheduler); MessageOutput.println("done"); // Initialize WebApi diff --git a/webapp/package.json b/webapp/package.json index 418eae1aa..5088ac7fb 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -13,37 +13,37 @@ }, "dependencies": { "@popperjs/core": "^2.11.8", - "bootstrap": "^5.3.2", + "bootstrap": "^5.3.3", "bootstrap-icons-vue": "^1.11.3", "mitt": "^3.0.1", "sortablejs": "^1.15.2", "spark-md5": "^3.0.2", - "vue": "^3.4.19", - "vue-i18n": "^9.9.1", - "vue-router": "^4.2.5" + "vue": "^3.4.21", + "vue-i18n": "^9.10.2", + "vue-router": "^4.3.0" }, "devDependencies": { - "@intlify/unplugin-vue-i18n": "^2.0.0", - "@rushstack/eslint-patch": "^1.7.2", + "@intlify/unplugin-vue-i18n": "^3.0.1", + "@rushstack/eslint-patch": "^1.8.0", "@tsconfig/node18": "^18.2.2", "@types/bootstrap": "^5.2.10", - "@types/node": "^20.11.19", + "@types/node": "^20.11.30", "@types/pulltorefreshjs": "^0.1.7", - "@types/sortablejs": "^1.15.7", + "@types/sortablejs": "^1.15.8", "@types/spark-md5": "^3.0.4", "@vitejs/plugin-vue": "^5.0.4", - "@vue/eslint-config-typescript": "^12.0.0", + "@vue/eslint-config-typescript": "^13.0.0", "@vue/tsconfig": "^0.5.1", - "eslint": "^8.56.0", - "eslint-plugin-vue": "^9.21.1", + "eslint": "^8.57.0", + "eslint-plugin-vue": "^9.23.0", "npm-run-all": "^4.1.5", "pulltorefreshjs": "^0.1.22", - "sass": "^1.71.0", - "terser": "^5.27.1", - "typescript": "^5.3.3", - "vite": "^5.1.3", + "sass": "^1.72.0", + "terser": "^5.29.2", + "typescript": "^5.4.3", + "vite": "^5.2.3", "vite-plugin-compression": "^0.5.1", - "vite-plugin-css-injected-by-js": "^3.4.0", - "vue-tsc": "^1.8.27" + "vite-plugin-css-injected-by-js": "^3.5.0", + "vue-tsc": "^2.0.7" } } diff --git a/webapp/src/components/DevInfo.vue b/webapp/src/components/DevInfo.vue index df049b7a4..024566ad3 100644 --- a/webapp/src/components/DevInfo.vue +++ b/webapp/src/components/DevInfo.vue @@ -76,14 +76,14 @@ export default defineComponent({ }, productionYear() { return() => { - return ((parseInt(this.devInfoList.serial.toString(), 16) >> (7 * 4)) & 0xF) + 2014; + return ((parseInt(this.devInfoList.serial, 16) >> (7 * 4)) & 0xF) + 2014; } }, productionWeek() { return() => { - return ((parseInt(this.devInfoList.serial.toString(), 16) >> (5 * 4)) & 0xFF).toString(16); + return ((parseInt(this.devInfoList.serial, 16) >> (5 * 4)) & 0xFF).toString(16); } } } }); - \ No newline at end of file + diff --git a/webapp/src/components/FirmwareInfo.vue b/webapp/src/components/FirmwareInfo.vue index 907ba0fcd..531187872 100644 --- a/webapp/src/components/FirmwareInfo.vue +++ b/webapp/src/components/FirmwareInfo.vue @@ -32,17 +32,20 @@ {{ $t('firmwareinfo.FirmwareUpdate') }} - - - - {{ systemStatus.update_text }} - - - - -
+ +
+
diff --git a/webapp/src/components/InputSerial.vue b/webapp/src/components/InputSerial.vue new file mode 100644 index 000000000..9f5ee343b --- /dev/null +++ b/webapp/src/components/InputSerial.vue @@ -0,0 +1,114 @@ + + + diff --git a/webapp/src/components/InverterTotalInfo.vue b/webapp/src/components/InverterTotalInfo.vue index 755a870ce..426550b78 100644 --- a/webapp/src/components/InverterTotalInfo.vue +++ b/webapp/src/components/InverterTotalInfo.vue @@ -73,7 +73,7 @@
-
+

{{ $n(totalBattData.soc.v, 'decimal', { @@ -84,7 +84,7 @@

-
+

{{ $n(powerMeterData.Power.v, 'decimal', { @@ -95,7 +95,7 @@

-
+

{{ $n(huaweiData.Power.v, 'decimal', { diff --git a/webapp/src/components/VedirectView.vue b/webapp/src/components/VedirectView.vue index 215ce80de..91efeaebc 100644 --- a/webapp/src/components/VedirectView.vue +++ b/webapp/src/components/VedirectView.vue @@ -9,25 +9,25 @@ @@ -178,7 +178,7 @@ \ No newline at end of file + diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index c751eed36..568476407 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -10,7 +10,7 @@ "DTUSettings": "DTU", "DeviceManager": "Hardware", "VedirectSettings": "VE.Direct", - "PowerMeterSettings": "Power Meter", + "PowerMeterSettings": "Stromzähler", "BatterySettings": "Batterie", "AcChargerSettings": "AC Ladegerät", "ConfigManagement": "Konfigurationsverwaltung", @@ -553,6 +553,7 @@ "typeSDM3ph": "SDM 3 phase (SDM72/630)", "typeHTTP": "HTTP(S) + JSON", "typeSML": "SML (OBIS 16.7.0)", + "typeSMAHM2": "SMA Homemanager 2.0", "MqttTopicPowerMeter1": "MQTT topic - Stromzähler #1", "MqttTopicPowerMeter2": "MQTT topic - Stromzähler #2 (Optional)", "MqttTopicPowerMeter3": "MQTT topic - Stromzähler #3 (Optional)", @@ -574,47 +575,52 @@ "testHttpRequest": "Testen" }, "powerlimiteradmin": { - "PowerLimiterSettings": "Power Limiter Einstellungen", - "PowerLimiterConfiguration": "Power Limiter Konfiguration", + "PowerLimiterSettings": "Dynamic Power Limiter Einstellungen", + "ConfigAlertMessage": "Eine oder mehrere Voraussetzungen zum Betrieb des Dynamic Power Limiter sind nicht erfüllt.", + "ConfigHints": "Konfigurationshinweise", + "ConfigHintRequirement": "Erforderlich", + "ConfigHintOptional": "Optional", + "ConfigHintsIntro": "Folgende Hinweise zur Konfiguration des Dynamic Power Limiter (DPL) sollen beachtet werden:", + "ConfigHintPowerMeterDisabled": "Zum Betrieb des DPL muss der Power Meter konfiguriert sein und Daten liefern.", + "ConfigHintNoInverter": "Vor dem Festlegen von Einstellungen des DPL muss mindestens ein Inverter konfiguriert sein.", + "ConfigHintInverterCommunication": "Das Abrufen von Daten und Senden von Kommandos muss für den zu regelnden Wechselrichter aktiviert sein.", + "ConfigHintNoChargeController": "Die Solar-Passthrough Funktion kann nur mit aktivierter VE.Direct Schnittstelle genutzt werden.", + "ConfigHintNoBatteryInterface": "SoC-basierte Schwellwerte können nur mit konfigurierter Batteriekommunikationsschnittstelle genutzt werden.", "General": "Allgemein", "Enable": "Aktiviert", "VerboseLogging": "@:base.VerboseLogging", + "SolarPassthrough": "Solar-Passthrough", "EnableSolarPassthrough": "Aktiviere Solar-Passthrough", - "SolarPassthroughLosses": "(Full) Solar-Passthrough Verluste:", + "SolarPassthroughLosses": "(Full) Solar-Passthrough Verluste", "SolarPassthroughLossesInfo": "Hinweis: Bei der Übertragung von Energie vom Solarladeregler zum Inverter sind Leitungsverluste zu erwarten. Um eine schleichende Entladung der Batterie im (Full) Solar-Passthrough Modus zu unterbinden, können diese Verluste berücksichtigt werden. Das am Inverter einzustellende Power Limit wird nach Berücksichtigung von dessen Effizienz zusätzlich um diesen Faktor verringert.", - "BatteryDrainStrategy": "Strategie zur Batterieentleerung", - "BatteryDrainWhenFull": "Leeren, wenn voll", - "BatteryDrainAtNight": "Leeren zur Nacht", - "SolarpassthroughInfo": "Diese Einstellung aktiviert die direkte Weitergabe der aktuell vom Laderegler gemeldeten Solarleistung an den Wechselrichter um eine unnötige Speicherung zu vermeiden und die Energieverluste zu minimieren.", - "InverterId": "Wechselrichter ID", - "InverterIdHint": "Wähle den Wechselrichter an dem die Batterie hängt.", - "InverterChannelId": "Kanal ID", - "InverterChannelIdHint": "Wähle den Kanal an dem die Batterie hängt.", + "BatteryDischargeAtNight": "Batterie nachts sogar teilweise geladen nutzen", + "SolarpassthroughInfo": "Diese Funktion ermöglicht den unmittelbaren Verbauch der verfügbaren Solarleistung. Dazu wird die aktuell vom Laderegler gemeldete Solarleistung am Wechselrichter als Limit eingestellt, selbst wenn sich die Batterie in einem Ladezyklus befindet. Somit wird eine unnötige Speicherung vermieden, die verlustbehaftet wäre.", + "InverterSettings": "Wechselrichter", + "Inverter": "Zu regelnder Wechselrichter", + "SelectInverter": "Inverter auswählen...", + "InverterChannelId": "Eingang für Spannungsmessungen", "TargetPowerConsumption": "Angestrebter Netzbezug", - "TargetPowerConsumptionHint": "Angestrebter erlaubter Stromverbrauch aus dem Netz.", - "TargetPowerConsumptionHysteresis": "Hysterese für das berechnete Limit", + "TargetPowerConsumptionHint": "Angestrebter erlaubter Stromverbrauch aus dem Netz. Wert darf negativ sein.", + "TargetPowerConsumptionHysteresis": "Hysterese", "TargetPowerConsumptionHysteresisHint": "Neu berechnetes Limit nur dann an den Inverter senden, wenn es vom zuletzt gesendeten Limit um mindestens diesen Betrag abweicht.", "LowerPowerLimit": "Unteres Leistungslimit", "UpperPowerLimit": "Oberes Leistungslimit", - "PowerMeters": "Leistungsmesser", + "SocThresholds": "Batterie State of Charge (SoC) Schwellwerte", "IgnoreSoc": "Batterie SoC ignorieren", - "BatterySocStartThreshold": "Akku SoC - Start", - "BatterySocStopThreshold": "Akku SoC - Stop", - "BatterySocSolarPassthroughStartThreshold": "Akku SoC - Start solar passthrough", - "BatterySocSolarPassthroughStartThresholdHint": "Wenn der Batterie SoC über diesem Limit ist wird die Inverter Leistung entsprechend der Victron MPPT Leistung gesetzt (abzüglich Effizienzkorrekturfaktor). Kann verwendet werden um überschüssige Solarleistung an das Netz zu liefern wenn die Batterie voll ist.", - "VoltageStartThreshold": "DC Spannung - Start", - "VoltageStopThreshold": "DC Spannung - Stop", - "VoltageSolarPassthroughStartThreshold": "DC Spannung - Start Solar-Passthrough", - "VoltageSolarPassthroughStopThreshold": "DC Spannung - Stop Solar-Passthrough", - "VoltageSolarPassthroughStartThresholdHint": "Wenn der Batteriespannung über diesem Limit ist wird die Inverter Leistung entsprechend der Victron MPPT Leistung gesetzt (abzüglich Effizienzkorrekturfaktor). Kann verwendet werden um überschüssige Solarleistung an das Netz zu liefern wenn die Batterie voll ist. Dieser Mode wird aktiv wenn das Start Spannungslimit überschritten wird und inaktiv wenn das Stop Spannungslimit unterschritten wird.", - "VoltageLoadCorrectionFactor": "DC Spannung - Lastkorrekturfaktor", + "StartThreshold": "Batterienutzung Start-Schwellwert", + "StopThreshold": "Batterienutzung Stop-Schwellwert", + "FullSolarPassthroughStartThreshold": "Full-Solar-Passthrough Start-Schwellwert", + "FullSolarPassthroughStartThresholdHint": "Oberhalb dieses Schwellwertes wird die Inverterleistung der Victron-MPPT-Leistung gleichgesetzt (abzüglich Effizienzkorrekturfaktor). Kann verwendet werden um überschüssige Solarleistung an das Netz zu liefern wenn die Batterie voll ist.", + "VoltageSolarPassthroughStopThreshold": "Full-Solar-Passthrough Stop-Schwellwert", + "VoltageLoadCorrectionFactor": "Lastkorrekturfaktor", "BatterySocInfo": "Hinweis: Die Akku SoC (State of Charge) Werte werden nur benutzt, wenn die Batterie-Kommunikationsschnittstelle innerhalb der letzten Minute gültige Werte geschickt hat. Andernfalls werden als Fallback-Option die Spannungseinstellungen verwendet.", - "InverterIsBehindPowerMeter": "Welchselrichter ist hinter Leistungsmesser", - "Battery": "DC / Akku", - "VoltageLoadCorrectionInfo": "Hinweis: Wenn Leistung von der Batterie abgegeben wird, bricht normalerweise die Spannung etwas ein. Damit nicht vorzeitig der Wechelrichter ausgeschaltet wird sobald der \"Stop\"-Schwellenwert erreicht wird, wird der hier angegebene Korrekturfaktor mit einberechnet. Korrigierte Spannung = DC Spannung + (Aktuelle Leistung (W) * Korrekturfaktor).", - "InverterRestart": "Wechselrichter Neustart", - "InverterRestartHour": "Stunde für Neustart", - "InverterRestartHint": "Neustart des Wechselrichter einmal täglich um die \"Tagesertrag\" Werte wieder auf Null zu setzen." + "InverterIsBehindPowerMeter": "Stromzählermessung beinhaltet Wechselrichterleistung", + "InverterIsSolarPowered": "Wechselrichter wird von Solarmodulen gespeist", + "VoltageThresholds": "Batterie Spannungs-Schwellwerte ", + "VoltageLoadCorrectionInfo": "Hinweis: Wenn Leistung von der Batterie abgegeben wird, bricht ihre Spannung etwas ein. Der Spannungseinbruch skaliert mit dem Entladestrom. Damit nicht vorzeitig der Wechselrichter ausgeschaltet wird sobald der Stop-Schwellenwert unterschritten wurde, wird der hier angegebene Korrekturfaktor mit einberechnet um die Spannung zu errechnen die der Akku in Ruhe hätte. Korrigierte Spannung = DC Spannung + (Aktuelle Leistung (W) * Korrekturfaktor).", + "InverterRestartHour": "Uhrzeit für geplanten Neustart", + "InverterRestartDisabled": "Keinen automatischen Neustart planen", + "InverterRestartHint": "Der Tagesertrag des Wechselrichters wird normalerweise nachts zurückgesetzt, wenn sich der Wechselrichter mangels Licht abschaltet. Um den Tageserstrag zurückzusetzen obwohl der Wechselrichter dauerhaft von der Batterie gespeist wird, kann der Inverter täglich zur gewünschten Uhrzeit automatisch neu gestartet werden." }, "batteryadmin": { "BatterySettings": "Batterie Einstellungen", @@ -724,11 +730,11 @@ "UploadProgress": "Hochlade-Fortschritt" }, "about": { - "AboutOpendtu": "Über OpenDTU", + "AboutOpendtu": "Über OpenDTU-OnBattery", "Documentation": "Dokumentation", - "DocumentationBody": "Die Firmware- und Hardware-Dokumentation ist hier zu finden: https://www.opendtu.solar", + "DocumentationBody": "Die Firmware- und Hardware-Dokumentation des Basis-Projektes ist hier zu finden: https://www.opendtu.solar
Zusätzliche Informationen, insbesondere zu OpenDTU-OnBattery-spezifischen Funktionen, gibt es im Wiki auf Github.", "ProjectOrigin": "Projekt Ursprung", - "ProjectOriginBody1": "Das Projekt wurde aus dieser Diskussion (mikrocontroller.net) heraus gestartet.", + "ProjectOriginBody1": "OpenDTU-OnBattery ist eine Erweiterung von OpenDTU. Das Basis-Projekt OpenDTU wurde aus dieser Diskussion (mikrocontroller.net) heraus gestartet.", "ProjectOriginBody2": "Das Hoymiles-Protokoll wurde durch die freiwilligen Bemühungen vieler Teilnehmer entschlüsselt. OpenDTU wurde unter anderem auf der Grundlage dieser Arbeit entwickelt. Das Projekt ist unter einer Open-Source-Lizenz lizenziert (GNU General Public License version 2).", "ProjectOriginBody3": "Die Software wurde nach bestem Wissen und Gewissen entwickelt. Dennoch kann keine Haftung für eine Fehlfunktion oder einen Garantieverlust des Wechselrichters übernommen werden.", "ProjectOriginBody4": "OpenDTU ist frei verfügbar. Wenn Sie Geld für die Software bezahlt haben, wurden Sie wahrscheinlich abgezockt.", @@ -785,8 +791,15 @@ "Name": "Name", "ValueSelected": "Ausgewählt", "ValueActive": "Aktiv" - }, - "huawei": { + }, + "inputserial": { + "format_hoymiles": "Hoymiles Seriennummerformat", + "format_converted": "Bereits konvertierte Seriennummer", + "format_herf_valid": "E-Star HERF Format (wird konvertiert gespeichert): {serial}", + "format_herf_invalid": "E-Star HERF Format: Ungültige Prüfsumme", + "format_unknown": "Unbekanntes Format" + }, + "huawei": { "DataAge": "letzte Aktualisierung: ", "Seconds": "vor {val} Sekunden", "Input": "Eingang", @@ -812,9 +825,9 @@ "Close": "Schließen", "SetVoltageLimit": "Spannungslimit:", "SetCurrentLimit": "Stromlimit:", - "CurrentLimit": "Aktuelles Limit: " - }, - "acchargeradmin": { + "CurrentLimit": "Aktuelles Limit: " + }, + "acchargeradmin": { "ChargerSettings": "AC Ladegerät Einstellungen", "Configuration": "AC Ladegerät Konfiguration", "EnableHuawei": "Huawei R4850G2 an CAN Bus Interface aktiv", @@ -827,8 +840,8 @@ "lowerPowerLimit": "Minimale Leistung", "upperPowerLimit": "Maximale Leistung", "Seconds": "@:base.Seconds" - }, - "battery": { + }, + "battery": { "battery": "Batterie", "DataAge": "letzte Aktualisierung: ", "Seconds": "vor {val} Sekunden", @@ -895,6 +908,9 @@ "bmsInternal": "BMS intern", "chargeCycles": "Ladezyklen", "chargedEnergy": "Geladene Energie", - "dischargedEnergy": "Entladene Energie" - } + "dischargedEnergy": "Entladene Energie", + "instantaneousPower": "Aktuelle Leistung", + "consumedAmpHours": "Verbrauche Amperestunden", + "lastFullCharge": "Letztes mal Vollgeladen" + } } diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 893870324..f7e7da98b 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -555,6 +555,7 @@ "typeSDM3ph": "SDM 3 phase (SDM72/630)", "typeHTTP": "HTTP(s) + JSON", "typeSML": "SML (OBIS 16.7.0)", + "typeSMAHM2": "SMA Homemanager 2.0", "MqttTopicPowerMeter1": "MQTT topic - Power meter #1", "MqttTopicPowerMeter2": "MQTT topic - Power meter #2", "MqttTopicPowerMeter3": "MQTT topic - Power meter #3", @@ -580,47 +581,52 @@ "milliSeconds": "ms" }, "powerlimiteradmin": { - "PowerLimiterSettings": "Power Limiter Settings", - "PowerLimiterConfiguration": "Power Limiter Configuration", + "PowerLimiterSettings": "Dynamic Power Limiter Settings", + "ConfigAlertMessage": "One or more prerequisites for operating the Dynamic Power Limiter are not met.", + "ConfigHints": "Configuration Notes", + "ConfigHintRequirement": "Required", + "ConfigHintOptional": "Optional", + "ConfigHintsIntro": "The following notes regarding the Dynamic Power Limiter (DPL) configuration shall be considered:", + "ConfigHintPowerMeterDisabled": "Operating the DPL requires the Power Meter being configured and delivering data.", + "ConfigHintNoInverter": "At least one inverter must be configured prior to setting up the DPL.", + "ConfigHintInverterCommunication": "Polling data from and sending commands to the target inverter must be enabled.", + "ConfigHintNoChargeController": "The solar-passthrough feature can only be used if the VE.Direct interface is configured.", + "ConfigHintNoBatteryInterface": "SoC-based thresholds can only be used if a battery communication interface is configured.", "General": "General", "Enable": "Enable", "VerboseLogging": "@:base.VerboseLogging", + "SolarPassthrough": "Solar-Passthrough", "EnableSolarPassthrough": "Enable Solar-Passthrough", - "SolarPassthroughLosses": "(Full) Solar Passthrough Losses:", - "SolarPassthroughLossesInfo": "Hint: Line losses are to be expected when transferring energy from the solar charge controller to the inverter. These losses can be taken into account to prevent the battery from gradually discharging in (full) solar passthrough mode. The power limit to be set on the inverter is additionally reduced by this factor after taking its efficiency into account.", - "BatteryDrainStrategy": "Battery drain strategy", - "BatteryDrainWhenFull": "Empty when full", - "BatteryDrainAtNight": "Empty at night", - "SolarpassthroughInfo": "When the sun is shining, this setting enables the sychronization of the inverter limit with the current solar power of the Victron MPPT charger. This optimizes battery degradation and loses.", - "InverterId": "Inverter ID", - "InverterIdHint": "Select proper inverter ID where battery is connected to.", - "InverterChannelId": "Channel ID", - "InverterChannelIdHint": "Select proper channel where battery is connected to.", - "TargetPowerConsumption": "Target power consumption from grid", - "TargetPowerConsumptionHint": "Set the grid power consumption the limiter tries to achieve.", - "TargetPowerConsumptionHysteresis": "Hysteresis for calculated power limit", + "SolarPassthroughLosses": "(Full) Solar-Passthrough Losses", + "SolarPassthroughLossesInfo": "Hint: Line losses are to be expected when transferring energy from the solar charge controller to the inverter. These losses can be taken into account to prevent the battery from gradually discharging in (full) solar-passthrough mode. The power limit to be set on the inverter is additionally reduced by this factor after taking its efficiency into account.", + "BatteryDischargeAtNight": "Use battery at night even if only partially charged", + "SolarpassthroughInfo": "This feature allows to use the available current solar power directly. The solar power, as reported by the MPPT charge controller, is set as the inverter's limit, even if the battery is currently charging. This avoids storing energy unnecessarily, which would be lossy.", + "InverterSettings": "Inverter", + "Inverter": "Target Inverter", + "SelectInverter": "Select an inverter...", + "InverterChannelId": "Input used for voltage measurements", + "TargetPowerConsumption": "Target Grid Consumption", + "TargetPowerConsumptionHint": "Grid power consumption the limiter tries to achieve. Value may be negative.", + "TargetPowerConsumptionHysteresis": "Hysteresis", "TargetPowerConsumptionHysteresisHint": "Only send a newly calculated power limit to the inverter if the absolute difference to the last sent power limit matches or exceeds this amount.", - "LowerPowerLimit": "Lower power limit", - "UpperPowerLimit": "Upper power limit", - "PowerMeters": "Power meter", + "LowerPowerLimit": "Lower Power Limit", + "UpperPowerLimit": "Upper Power Limit", + "SocThresholds": "Battery State of Charge (SoC) Thresholds", "IgnoreSoc": "Ignore Battery SoC", - "BatterySocStartThreshold": "Battery SoC - Start threshold", - "BatterySocStopThreshold": "Battery SoC - Stop threshold", - "BatterySocSolarPassthroughStartThreshold": "Battery SoC - Start threshold for full solar passthrough", - "BatterySocSolarPassthroughStartThresholdHint": "Inverter power is set according to Victron MPPT power (minus efficiency factors) if battery SoC is over this limit. Use this if you like to supply excess power to the grid when battery is full", - "VoltageStartThreshold": "DC Voltage - Start threshold", - "VoltageStopThreshold": "DC Voltage - Stop threshold", - "VoltageSolarPassthroughStartThreshold": "DC Voltage - Start threshold for full solar passthrough", - "VoltageSolarPassthroughStopThreshold": "DC Voltage - Stop threshold for full solar passthrough", - "VoltageSolarPassthroughStartThresholdHint": "Inverter power is set according to Victron MPPT power (minus efficiency factors) when full solar passthrough is active. Use this if you like to supply excess power to the grid when battery is full. This is started when battery voltage goes over this limit and stopped if voltage drops below stop limit.", - "VoltageLoadCorrectionFactor": "DC Voltage - Load correction factor", + "StartThreshold": "Start Threshold for Battery Discharging", + "StopThreshold": "Stop Threshold for Battery Discharging", + "FullSolarPassthroughStartThreshold": "Full Solar-Passthrough Start Threshold", + "FullSolarPassthroughStartThresholdHint": "Inverter power is set equal to Victron MPPT power (minus efficiency factors) while above this threshold. Use this if you want to supply excess power to the grid when the battery is full.", + "VoltageSolarPassthroughStopThreshold": "Full Solar-Passthrough Stop Threshold", + "VoltageLoadCorrectionFactor": "Load correction factor", "BatterySocInfo": "Hint: The battery SoC (State of Charge) values are only used if the battery communication interface reported SoC updates in the last minute. Otherwise the voltage thresholds will be used as fallback.", - "InverterIsBehindPowerMeter": "Inverter is behind Power meter", - "Battery": "DC / Battery", - "VoltageLoadCorrectionInfo": "Hint: When the power output is higher, the voltage is usually decreasing. In order to not stop the inverter too early (Stop treshold), a power factor can be specified here to correct this. Corrected voltage = DC Voltage + (Current power * correction factor).", - "InverterRestart": "Inverter Restart", - "InverterRestartHour": "Restart Hour", - "InverterRestartHint": "Restart the Inverter once a day to reset the \"YieldDay\" values." + "InverterIsBehindPowerMeter": "PowerMeter reading includes inverter output", + "InverterIsSolarPowered": "Inverter is powered by solar modules", + "VoltageThresholds": "Battery Voltage Thresholds", + "VoltageLoadCorrectionInfo": "Hint: When the battery is discharged, its voltage drops. The voltage drop scales with the discharge current. In order to not stop the inverter too early (stop threshold), this load correction factor can be specified to calculate the battery voltage if it was idle. Corrected voltage = DC Voltage + (Current power * correction factor).", + "InverterRestartHour": "Automatic Restart Time", + "InverterRestartDisabled": "Do not execute automatic restart", + "InverterRestartHint": "The daily yield of the inverter is usually reset at night when the inverter turns off due to lack of light. To reset the daily yield even though the inverter is continuously powered by the battery, the inverter can be automatically restarted daily at the desired time." }, "batteryadmin": { "BatterySettings": "Battery Settings", @@ -730,11 +736,11 @@ "UploadProgress": "Upload Progress" }, "about": { - "AboutOpendtu": "About OpenDTU", + "AboutOpendtu": "About OpenDTU-OnBattery", "Documentation": "Documentation", - "DocumentationBody": "The firmware and hardware documentation can be found here: https://www.opendtu.solar", + "DocumentationBody": "The firmware and hardware documentation of the upstream project can be found here: https://www.opendtu.solar
Additional information, especially regarding OpenDTU-OnBattery-specific features, can be accessed at the Github Wiki.", "ProjectOrigin": "Project Origin", - "ProjectOriginBody1": "This project was started from this discussion. (Mikrocontroller.net)", + "ProjectOriginBody1": "OpenDTU-OnBattery is a fork of OpenDTU. The upstream project OpenDTU was started from this discussion. (Mikrocontroller.net)", "ProjectOriginBody2": "The Hoymiles protocol was decrypted through the voluntary efforts of many participants. OpenDTU, among others, was developed based on this work. The project is licensed under an Open Source License (GNU General Public License version 2).", "ProjectOriginBody3": "The software was developed to the best of our knowledge and belief. Nevertheless, no liability can be accepted for a malfunction or guarantee loss of the inverter.", "ProjectOriginBody4": "OpenDTU is freely available. If you paid money for the software, you probably got ripped off.", @@ -792,8 +798,15 @@ "Number": "Number", "ValueSelected": "Selected", "ValueActive": "Active" + }, + "inputserial": { + "format_hoymiles": "Hoymiles serial number format", + "format_converted": "Already converted serial number", + "format_herf_valid": "E-Star HERF format (will be saved converted): {serial}", + "format_herf_invalid": "E-Star HERF format: Invalid checksum", + "format_unknown": "Unknown format" }, - "huawei": { + "huawei": { "DataAge": "Data Age: ", "Seconds": " {val} seconds", "Input": "Input", @@ -819,9 +832,9 @@ "Close": "close", "SetVoltageLimit": "Voltage limit:", "SetCurrentLimit": "Current limit:", - "CurrentLimit": "Current limit:" - }, - "acchargeradmin": { + "CurrentLimit": "Current limit:" + }, + "acchargeradmin": { "ChargerSettings": "AC Charger Settings", "Configuration": "AC Charger Configuration", "EnableHuawei": "Enable Huawei R4850G2 on CAN Bus Interface", @@ -834,8 +847,8 @@ "lowerPowerLimit": "Minimum output power", "upperPowerLimit": "Maximum output power", "Seconds": "@:base.Seconds" - }, - "battery": { + }, + "battery": { "battery": "Battery", "DataAge": "Data Age: ", "Seconds": " {val} seconds", @@ -902,6 +915,9 @@ "bmsInternal": "BMS internal", "chargeCycles": "Charge cycles", "chargedEnergy": "Charged energy", - "dischargedEnergy": "Discharged energy" - } + "dischargedEnergy": "Discharged energy", + "instantaneousPower": "Instantaneous Power", + "consumedAmpHours": "Consumed Amp Hours", + "lastFullCharge": "Last full Charge" + } } diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index 88514e63d..df56ac072 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -663,44 +663,49 @@ "Cancel": "@:base.Cancel" }, "powerlimiteradmin": { - "PowerLimiterSettings": "Power Limiter Settings", - "PowerLimiterConfiguration": "Power Limiter Configuration", + "PowerLimiterSettings": "Dynamic Power Limiter Settings", + "ConfigAlertMessage": "One or more prerequisites for operating the Dynamic Power Limiter are not met.", + "ConfigHints": "Configuration Notes", + "ConfigHintRequirement": "Required", + "ConfigHintOptional": "Optional", + "ConfigHintsIntro": "The following notes regarding the Dynamic Power Limiter (DPL) configuration shall be considered:", + "ConfigHintPowerMeterDisabled": "Operating the DPL requires the Power Meter being configured and delivering data.", + "ConfigHintNoInverter": "At least one inverter must be configured prior to setting up the DPL.", + "ConfigHintInverterCommunication": "Polling data from and sending commands to the target inverter must be enabled.", + "ConfigHintNoChargeController": "The solar-passthrough feature can only be used if the VE.Direct interface is configured.", + "ConfigHintNoBatteryInterface": "SoC-based thresholds can only be used if a battery communication interface is configured.", "General": "General", "Enable": "Enable", "VerboseLogging": "@:base.VerboseLogging", + "SolarPassthrough": "Solar-Passthrough", "EnableSolarPassthrough": "Enable Solar-Passthrough", - "SolarPassthroughLosses": "(Full) Solar Passthrough Losses:", - "SolarPassthroughLossesInfo": "Hint: Line losses are to be expected when transferring energy from the solar charge controller to the inverter. These losses can be taken into account to prevent the battery from gradually discharging in (full) solar passthrough mode. The power limit to be set on the inverter is additionally reduced by this factor after taking its efficiency into account.", - "BatteryDrainStrategy": "Battery drain strategy", - "BatteryDrainWhenFull": "Empty when full", - "BatteryDrainAtNight": "Empty at night", - "SolarpassthroughInfo": "When the sun is shining, this setting enables the sychronization of the inverter limit with the current solar power of the Victron MPPT charger. This optimizes battery degradation and loses.", - "InverterId": "Inverter ID", - "InverterIdHint": "Select proper inverter ID where battery is connected to.", - "InverterChannelId": "Channel ID", - "InverterChannelIdHint": "Select proper channel where battery is connected to.", - "TargetPowerConsumption": "Target power consumption from grid", - "TargetPowerConsumptionHint": "Set the grid power consumption the limiter tries to achieve.", - "TargetPowerConsumptionHysteresis": "Hysteresis for calculated power limit", + "SolarPassthroughLosses": "(Full) Solar-Passthrough Losses", + "SolarPassthroughLossesInfo": "Hint: Line losses are to be expected when transferring energy from the solar charge controller to the inverter. These losses can be taken into account to prevent the battery from gradually discharging in (full) solar-passthrough mode. The power limit to be set on the inverter is additionally reduced by this factor after taking its efficiency into account.", + "BatteryDischargeAtNight": "Use battery at night even if only partially charged", + "SolarpassthroughInfo": "This feature allows to use the available current solar power directly. The solar power, as reported by the MPPT charge controller, is set as the inverter's limit, even if the battery is currently charging. This avoids storing energy unnecessarily, which would be lossy.", + "InverterSettings": "Inverter", + "Inverter": "Target Inverter", + "SelectInverter": "Select an inverter...", + "InverterChannelId": "Input used for voltage measurements", + "TargetPowerConsumption": "Target Grid Consumption", + "TargetPowerConsumptionHint": "Grid power consumption the limiter tries to achieve. Value may be negative.", + "TargetPowerConsumptionHysteresis": "Hysteresis", "TargetPowerConsumptionHysteresisHint": "Only send a newly calculated power limit to the inverter if the absolute difference to the last sent power limit matches or exceeds this amount.", - "LowerPowerLimit": "Lower power limit", - "UpperPowerLimit": "Upper power limit", - "PowerMeters": "Power meter", + "LowerPowerLimit": "Lower Power Limit", + "UpperPowerLimit": "Upper Power Limit", + "SocThresholds": "Battery State of Charge (SoC) Thresholds", "IgnoreSoc": "Ignore Battery SoC", - "BatterySocStartThreshold": "Battery SoC - Start threshold", - "BatterySocStopThreshold": "Battery SoC - Stop threshold", - "BatterySocSolarPassthroughStartThreshold": "Battery SoC - Start threshold for full solar passthrough", - "BatterySocSolarPassthroughStartThresholdHint": "Inverter power is set according to Victron MPPT power (minus efficiency factors) if battery SOC is over this limit. Use this if you like to supply excess power to the grid when battery is full", - "VoltageStartThreshold": "DC Voltage - Start threshold", - "VoltageStopThreshold": "DC Voltage - Stop threshold", - "VoltageSolarPassthroughStartThreshold": "DC Voltage - Start threshold for full solar passthrough", - "VoltageSolarPassthroughStopThreshold": "DC Voltage - Stop threshold for full solar passthrough", - "VoltageSolarPassthroughStartThresholdHint": "Inverter power is set according to Victron MPPT power (minus efficiency factors) when full solar passthrough is active. Use this if you like to supply excess power to the grid when battery is full. This is started when battery voltage goes over this limit and stopped if voltage drops below stop limit.", - "VoltageLoadCorrectionFactor": "DC Voltage - Load correction factor", + "StartThreshold": "Start Threshold for Battery Discharging", + "StopThreshold": "Stop Threshold for Battery Discharging", + "FullSolarPassthroughStartThreshold": "Full Solar-Passthrough Start Threshold", + "FullSolarPassthroughStartThresholdHint": "Inverter power is set equal to Victron MPPT power (minus efficiency factors) while above this threshold. Use this if you want to supply excess power to the grid when the battery is full.", + "VoltageSolarPassthroughStopThreshold": "Full Solar-Passthrough Stop Threshold", + "VoltageLoadCorrectionFactor": "Load correction factor", "BatterySocInfo": "Hint: The battery SoC (State of Charge) values are only used if the battery communication interface reported SoC updates in the last minute. Otherwise the voltage thresholds will be used as fallback.", - "InverterIsBehindPowerMeter": "Inverter is behind Power meter", - "Battery": "DC / Battery", - "VoltageLoadCorrectionInfo": "Hint: When the power output is higher, the voltage is usually decreasing. In order to not stop the inverter too early (Stop treshold), a power factor can be specified here to correct this. Corrected voltage = DC Voltage + (Current power * correction factor)." + "InverterIsBehindPowerMeter": "PowerMeter reading includes inverter output", + "InverterIsSolarPowered": "Inverter is powered by solar modules", + "VoltageThresholds": "Battery Voltage Thresholds", + "VoltageLoadCorrectionInfo": "Hint: When the battery is discharged, its voltage drops. The voltage drop scales with the discharge current. In order to not stop the inverter too early (stop threshold), this load correction factor can be specified to calculate the battery voltage if it was idle. Corrected voltage = DC Voltage + (Current power * correction factor)." }, "login": { "Login": "Connexion", @@ -723,9 +728,9 @@ "UploadProgress": "Progression du téléversement" }, "about": { - "AboutOpendtu": "À propos d'OpenDTU", + "AboutOpendtu": "À propos d'OpenDTU-OnBattery", "Documentation": "Documentation", - "DocumentationBody": "The firmware and hardware documentation can be found here: https://www.opendtu.solar", + "DocumentationBody": "The firmware and hardware documentation of the upstream project can be found here: https://www.opendtu.solar
Additional information, especially regarding OpenDTU-OnBattery-specific features, can be accessed at the Github Wiki.", "ProjectOrigin": "Origine du projet", "ProjectOriginBody1": "Ce projet a été démarré suite à cette discussion (Mikrocontroller.net).", "ProjectOriginBody2": "Le protocole Hoymiles a été décrypté grâce aux efforts volontaires de nombreux participants. OpenDTU, entre autres, a été développé sur la base de ce travail. Le projet est sous licence Open Source (GNU General Public License version 2).", @@ -784,8 +789,15 @@ "Name": "Nom", "ValueSelected": "Sélectionné", "ValueActive": "Activé" - }, - "huawei": { + }, + "inputserial": { + "format_hoymiles": "Hoymiles serial number format", + "format_converted": "Already converted serial number", + "format_herf_valid": "E-Star HERF format (will be saved converted): {serial}", + "format_herf_invalid": "E-Star HERF format: Invalid checksum", + "format_unknown": "Unknown format" + }, + "huawei": { "DataAge": "Data Age: ", "Seconds": " {val} seconds", "Input": "Input", @@ -811,9 +823,9 @@ "Close": "close", "SetVoltageLimit": "Voltage limit:", "SetCurrentLimit": "Current limit:", - "CurrentLimit": "Current limit:" - }, - "acchargeradmin": { + "CurrentLimit": "Current limit:" + }, + "acchargeradmin": { "ChargerSettings": "AC Charger Settings", "Configuration": "AC Charger Configuration", "EnableHuawei": "Enable Huawei R4850G2 on CAN Bus Interface", @@ -826,8 +838,8 @@ "lowerPowerLimit": "Minimum output power", "upperPowerLimit": "Maximum output power", "Seconds": "@:base.Seconds" - }, - "battery": { + }, + "battery": { "battery": "Battery", "DataAge": "Data Age: ", "Seconds": " {val} seconds", @@ -894,6 +906,9 @@ "bmsInternal": "BMS internal", "chargeCycles": "Charge cycles", "chargedEnergy": "Charged energy", - "dischargedEnergy": "Discharged energy" - } + "dischargedEnergy": "Discharged energy", + "instantaneousPower": "Instantaneous Power", + "consumedAmpHours": "Consumed Amp Hours", + "lastFullCharge": "Last full Charge" + } } diff --git a/webapp/src/types/DevInfoStatus.ts b/webapp/src/types/DevInfoStatus.ts index 4c09e6b46..7c37a5673 100644 --- a/webapp/src/types/DevInfoStatus.ts +++ b/webapp/src/types/DevInfoStatus.ts @@ -1,5 +1,5 @@ export interface DevInfoStatus { - serial: number; + serial: string; valid_data: boolean; fw_bootloader_version: number; fw_build_version: number; @@ -8,4 +8,4 @@ export interface DevInfoStatus { hw_version: number; hw_model_name: string; max_power: number; -} \ No newline at end of file +} diff --git a/webapp/src/types/InverterConfig.ts b/webapp/src/types/InverterConfig.ts index 1f2167aa4..da7fa43c4 100644 --- a/webapp/src/types/InverterConfig.ts +++ b/webapp/src/types/InverterConfig.ts @@ -6,7 +6,7 @@ export interface InverterChannel { export interface Inverter { id: string; - serial: number; + serial: string; name: string; type: string; order: number; diff --git a/webapp/src/types/LimitConfig.ts b/webapp/src/types/LimitConfig.ts index d311ca612..b218c1140 100644 --- a/webapp/src/types/LimitConfig.ts +++ b/webapp/src/types/LimitConfig.ts @@ -1,5 +1,5 @@ export interface LimitConfig { - serial: number; + serial: string; limit_value: number; limit_type: number; -} \ No newline at end of file +} diff --git a/webapp/src/types/LiveDataStatus.ts b/webapp/src/types/LiveDataStatus.ts index becd2253f..d43b7838f 100644 --- a/webapp/src/types/LiveDataStatus.ts +++ b/webapp/src/types/LiveDataStatus.ts @@ -22,7 +22,7 @@ export interface InverterStatistics { } export interface Inverter { - serial: number; + serial: string; name: string; order: number; data_age: number; diff --git a/webapp/src/types/PowerLimiterConfig.ts b/webapp/src/types/PowerLimiterConfig.ts index 594361d77..c6edde8d6 100644 --- a/webapp/src/types/PowerLimiterConfig.ts +++ b/webapp/src/types/PowerLimiterConfig.ts @@ -1,11 +1,32 @@ +export interface PowerLimiterInverterInfo { + pos: number; + name: string; + poll_enable: boolean; + poll_enable_night: boolean; + command_enable: boolean; + command_enable_night: boolean; + type: string; + channels: number; +} + +// meta-data not directly part of the DPL settings, +// to control visibility of DPL settings +export interface PowerLimiterMetaData { + power_meter_enabled: boolean; + battery_enabled: boolean; + charge_controller_enabled: boolean; + inverters: { [key: string]: PowerLimiterInverterInfo }; +} + export interface PowerLimiterConfig { enabled: boolean; verbose_logging: boolean; solar_passthrough_enabled: boolean; solar_passthrough_losses: number; - battery_drain_strategy: number; + battery_always_use_at_night: boolean; is_inverter_behind_powermeter: boolean; - inverter_id: number; + is_inverter_solar_powered: boolean; + inverter_serial: string; inverter_channel_id: number; target_power_consumption: number; target_power_consumption_hysteresis: number; diff --git a/webapp/src/types/VedirectLiveDataStatus.ts b/webapp/src/types/VedirectLiveDataStatus.ts index f6635c4c3..73b78a454 100644 --- a/webapp/src/types/VedirectLiveDataStatus.ts +++ b/webapp/src/types/VedirectLiveDataStatus.ts @@ -5,12 +5,22 @@ export interface DynamicPowerLimiter { PLLIMIT: number; } +export interface Vedirect { + full_update: boolean; + instances: { [key: string]: VedirectInstance }; +} + +export interface VedirectInstance { + data_age_ms: number; + device: VedirectDevice; + output: VedirectOutput; + input: VedirectInput; +} + export interface VedirectDevice { SER: string; PID: string; FW: string; - age_critical: boolean; - data_age: 0; LOAD: ValueObject; CS: ValueObject; MPPT: ValueObject; diff --git a/webapp/src/views/HomeView.vue b/webapp/src/views/HomeView.vue index 9371d0fae..d223db1df 100644 --- a/webapp/src/views/HomeView.vue +++ b/webapp/src/views/HomeView.vue @@ -355,7 +355,7 @@ export default defineComponent({ showAlertLimit: false, powerSettingView: {} as bootstrap.Modal, - powerSettingSerial: 0, + powerSettingSerial: "", powerSettingLoading: true, alertMessagePower: "", alertTypePower: "info", @@ -532,7 +532,7 @@ export default defineComponent({ this.heartInterval && clearTimeout(this.heartInterval); this.isFirstFetchAfterConnect = true; }, - onShowEventlog(serial: number) { + onShowEventlog(serial: string) { this.eventLogLoading = true; fetch("/api/eventlog/status?inv=" + serial + "&locale=" + this.$i18n.locale, { headers: authHeader() }) .then((response) => handleResponse(response, this.$emitter, this.$router)) @@ -543,7 +543,7 @@ export default defineComponent({ this.eventLogView.show(); }, - onShowDevInfo(serial: number) { + onShowDevInfo(serial: string) { this.devInfoLoading = true; fetch("/api/devinfo/status?inv=" + serial, { headers: authHeader() }) .then((response) => handleResponse(response, this.$emitter, this.$router)) @@ -555,7 +555,7 @@ export default defineComponent({ this.devInfoView.show(); }, - onShowGridProfile(serial: number) { + onShowGridProfile(serial: string) { this.gridProfileLoading = true; fetch("/api/gridprofile/status?inv=" + serial, { headers: authHeader() }) .then((response) => handleResponse(response, this.$emitter, this.$router)) @@ -572,9 +572,9 @@ export default defineComponent({ this.gridProfileView.show(); }, - onShowLimitSettings(serial: number) { + onShowLimitSettings(serial: string) { this.showAlertLimit = false; - this.targetLimitList.serial = 0; + this.targetLimitList.serial = ""; this.targetLimitList.limit_value = 0; this.targetLimitType = 1; this.targetLimitTypeText = this.$t('home.Relative'); @@ -628,9 +628,9 @@ export default defineComponent({ this.targetLimitType = type; }, - onShowPowerSettings(serial: number) { + onShowPowerSettings(serial: string) { this.showAlertPower = false; - this.powerSettingSerial = 0; + this.powerSettingSerial = ""; this.powerSettingLoading = true; fetch("/api/power/status", { headers: authHeader() }) .then((response) => handleResponse(response, this.$emitter, this.$router)) diff --git a/webapp/src/views/InverterAdminView.vue b/webapp/src/views/InverterAdminView.vue index 21ea8dd76..9216f592f 100644 --- a/webapp/src/views/InverterAdminView.vue +++ b/webapp/src/views/InverterAdminView.vue @@ -8,8 +8,7 @@
- +
@@ -91,7 +90,7 @@ - + @@ -207,6 +206,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 InputSerial from '@/components/InputSerial.vue'; import ModalDialog from '@/components/ModalDialog.vue'; import type { Inverter } from '@/types/InverterConfig'; import { authHeader, handleResponse } from '@/utils/authentication'; @@ -235,6 +235,7 @@ export default defineComponent({ BootstrapAlert, CardElement, InputElement, + InputSerial, ModalDialog, BIconInfoCircle, BIconPencil, diff --git a/webapp/src/views/PowerLimiterAdminView.vue b/webapp/src/views/PowerLimiterAdminView.vue index 898cd4f64..0a58b4a9d 100644 --- a/webapp/src/views/PowerLimiterAdminView.vue +++ b/webapp/src/views/PowerLimiterAdminView.vue @@ -1,276 +1,200 @@ @@ -284,7 +208,7 @@ import CardElement from '@/components/CardElement.vue'; import FormFooter from '@/components/FormFooter.vue'; import InputElement from '@/components/InputElement.vue'; import { BIconInfoCircle } from 'bootstrap-icons-vue'; -import type { PowerLimiterConfig } from "@/types/PowerLimiterConfig"; +import type { PowerLimiterConfig, PowerLimiterMetaData } from "@/types/PowerLimiterConfig"; export default defineComponent({ components: { @@ -299,72 +223,136 @@ export default defineComponent({ return { dataLoading: true, powerLimiterConfigList: {} as PowerLimiterConfig, - inverterList: [ - { key: 0, value: "ID 00" }, - { key: 1, value: "ID 01" }, - { key: 2, value: "ID 02" }, - { key: 3, value: "ID 03" }, - { key: 4, value: "ID 04" }, - { key: 5, value: "ID 05" }, - { key: 6, value: "ID 06" }, - { key: 7, value: "ID 07" }, - { key: 8, value: "ID 08" }, - { key: 9, value: "ID 09" }, - { key: 10, value: "ID 10" }, - ], - inverterChannelList: [ - { key: 0, value: "CH 0" }, - { key: 1, value: "CH 1" }, - { key: 2, value: "CH 2" }, - { key: 3, value: "CH 3" }, - ], - batteryDrainStrategyList: [ - { key: 0, value: "powerlimiteradmin.BatteryDrainWhenFull"}, - { key: 1, value: "powerlimiteradmin.BatteryDrainAtNight" }, - ], - restartHourList: [ - { key: -1, value: "- - - -" }, - { key: 0, value: "0:00" }, - { key: 1, value: "1:00" }, - { key: 2, value: "2:00" }, - { key: 3, value: "3:00" }, - { key: 4, value: "4:00" }, - { key: 5, value: "5:00" }, - { key: 6, value: "6:00" }, - { key: 7, value: "7:00" }, - { key: 8, value: "8:00" }, - { key: 9, value: "9:00" }, - { key: 10, value: "10:00" }, - { key: 11, value: "11:00" }, - { key: 12, value: "12:00" }, - { key: 13, value: "13:00" }, - { key: 14, value: "14:00" }, - { key: 15, value: "15:00" }, - { key: 16, value: "16:00" }, - { key: 17, value: "17:00" }, - { key: 18, value: "18:00" }, - { key: 19, value: "19:00" }, - { key: 20, value: "20:00" }, - { key: 21, value: "21:00" }, - { key: 22, value: "22:00" }, - { key: 23, value: "23:00" }, - ], + powerLimiterMetaData: {} as PowerLimiterMetaData, alertMessage: "", alertType: "info", showAlert: false, + configAlert: false, }; }, created() { - this.getPowerLimiterConfig(); + this.getAllData(); + }, + watch: { + 'powerLimiterConfigList.inverter_serial'(newVal) { + var cfg = this.powerLimiterConfigList; + var meta = this.powerLimiterMetaData; + + if (newVal === "") { return; } // do not try to convert the placeholder value + + if (meta.inverters[newVal] !== undefined) { return; } + + for (const [serial, inverter] of Object.entries(meta.inverters)) { + // cfg.inverter_serial might be too large to parse as a 32 bit + // int, so we make sure to only try to parse two characters. if + // cfg.inverter_serial is indeed an old position based index, + // it is only one character. + if (inverter.pos == Number(cfg.inverter_serial.substr(0, 2))) { + // inverter_serial uses the old position-based + // value to identify the inverter. convert to serial. + cfg.inverter_serial = serial; + return; + } + } + + // previously selected inverter was deleted. marks serial as + // invalid, selects placeholder option. + cfg.inverter_serial = ''; + } }, methods: { - getPowerLimiterConfig() { + getConfigHints() { + var cfg = this.powerLimiterConfigList; + var meta = this.powerLimiterMetaData; + var hints = []; + + if (meta.power_meter_enabled !== true) { + hints.push({severity: "requirement", subject: "PowerMeterDisabled"}); + this.configAlert = true; + } + + if (typeof meta.inverters === "undefined" || Object.keys(meta.inverters).length == 0) { + hints.push({severity: "requirement", subject: "NoInverter"}); + this.configAlert = true; + } + else { + var inv = meta.inverters[cfg.inverter_serial]; + if (inv !== undefined && !(inv.poll_enable && inv.command_enable && inv.poll_enable_night && inv.command_enable_night)) { + hints.push({severity: "requirement", subject: "InverterCommunication"}); + } + } + + if (!cfg.is_inverter_solar_powered) { + if (!meta.charge_controller_enabled) { + hints.push({severity: "optional", subject: "NoChargeController"}); + } + + if (!meta.battery_enabled) { + hints.push({severity: "optional", subject: "NoBatteryInterface"}); + } + } + + return hints; + }, + isEnabled() { + return this.powerLimiterConfigList.enabled; + }, + canUseSolarPassthrough() { + var cfg = this.powerLimiterConfigList; + var meta = this.powerLimiterMetaData; + var canUse = this.isEnabled() && meta.charge_controller_enabled && !cfg.is_inverter_solar_powered; + if (!canUse) { cfg.solar_passthrough_enabled = false; } + return canUse; + }, + canUseSoCThresholds() { + var cfg = this.powerLimiterConfigList; + var meta = this.powerLimiterMetaData; + return this.isEnabled() && meta.battery_enabled && !cfg.is_inverter_solar_powered; + }, + canUseVoltageThresholds() { + var cfg = this.powerLimiterConfigList; + return this.isEnabled() && !cfg.is_inverter_solar_powered; + }, + isSolarPassthroughEnabled() { + return this.powerLimiterConfigList.solar_passthrough_enabled; + }, + range(end: number) { + return Array.from(Array(end).keys()); + }, + needsChannelSelection() { + var cfg = this.powerLimiterConfigList; + var meta = this.powerLimiterMetaData; + + var reset = function() { + cfg.inverter_channel_id = 0; + return false; + }; + + if (cfg.inverter_serial === '') { return reset(); } + + if (cfg.is_inverter_solar_powered) { return reset(); } + + var inverter = meta.inverters[cfg.inverter_serial]; + if (inverter === undefined) { return reset(); } + + if (cfg.inverter_channel_id >= inverter.channels) { + reset(); + } + + return inverter.channels > 1; + }, + getAllData() { this.dataLoading = true; - fetch("/api/powerlimiter/config", { headers: authHeader() }) + fetch("/api/powerlimiter/metadata", { headers: authHeader() }) .then((response) => handleResponse(response, this.$emitter, this.$router)) .then((data) => { - this.powerLimiterConfigList = data; - this.dataLoading = false; + this.powerLimiterMetaData = data; + fetch("/api/powerlimiter/config", { headers: authHeader() }) + .then((response) => handleResponse(response, this.$emitter, this.$router)) + .then((data) => { + this.powerLimiterConfigList = data; + this.dataLoading = false; + }); }); }, savePowerLimiterConfig(e: Event) { diff --git a/webapp/src/views/PowerMeterAdminView.vue b/webapp/src/views/PowerMeterAdminView.vue index 280171cf1..b490b7c58 100644 --- a/webapp/src/views/PowerMeterAdminView.vue +++ b/webapp/src/views/PowerMeterAdminView.vue @@ -3,7 +3,7 @@ {{ alertMessage }} - + @@ -83,7 +83,7 @@

- +
@@ -126,7 +126,7 @@ placeholder="http://admin:supersecret@mypowermeter.home/status" prefix="GET " :tooltip="$t('powermeteradmin.httpUrlDescription')" /> - +
@@ -236,6 +236,7 @@ export default defineComponent({ { key: 2, value: this.$t('powermeteradmin.typeSDM3ph') }, { key: 3, value: this.$t('powermeteradmin.typeHTTP') }, { key: 4, value: this.$t('powermeteradmin.typeSML') }, + { key: 5, value: this.$t('powermeteradmin.typeSMAHM2') }, ], powerMeterAuthList: [ { key: 0, value: "None" }, diff --git a/webapp/src/views/SystemInfoView.vue b/webapp/src/views/SystemInfoView.vue index e5ce3868c..e0842d83b 100644 --- a/webapp/src/views/SystemInfoView.vue +++ b/webapp/src/views/SystemInfoView.vue @@ -58,12 +58,16 @@ export default defineComponent({ }) }, getUpdateInfo() { + if (this.systemDataList.git_hash === undefined) { + return; + } + // If the left char is a "g" the value is the git hash (remove the "g") this.systemDataList.git_is_hash = this.systemDataList.git_hash?.substring(0, 1) == 'g'; this.systemDataList.git_hash = this.systemDataList.git_is_hash ? this.systemDataList.git_hash?.substring(1) : this.systemDataList.git_hash; // Handle format "v0.1-5-gabcdefh" - if (this.systemDataList.git_hash.lastIndexOf("-") >= 0) { + if (this.systemDataList.git_hash?.lastIndexOf("-") >= 0) { this.systemDataList.git_hash = this.systemDataList.git_hash.substring(this.systemDataList.git_hash.lastIndexOf("-") + 2) this.systemDataList.git_is_hash = true; } @@ -96,8 +100,8 @@ export default defineComponent({ }, watch: { allowVersionInfo(allow: Boolean) { + localStorage.setItem("allowVersionInfo", allow ? "1" : "0"); if (allow) { - localStorage.setItem("allowVersionInfo", this.allowVersionInfo ? "1" : "0"); this.getUpdateInfo(); } } diff --git a/webapp/vite.config.ts b/webapp/vite.config.ts index 34af8312b..fd35a7e94 100644 --- a/webapp/vite.config.ts +++ b/webapp/vite.config.ts @@ -29,6 +29,7 @@ export default defineConfig({ fullInstall: false, forceStringify: true, strictMessage: false, + jitCompilation: false, }), ], resolve: { diff --git a/webapp/yarn.lock b/webapp/yarn.lock index 5e6d47068..af59fb24c 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -12,125 +12,125 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.11.tgz#68bb07ab3d380affa9a3f96728df07969645d2d9" integrity sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ== -"@babel/parser@^7.21.3": - version "7.21.8" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.21.8.tgz#642af7d0333eab9c0ad70b14ac5e76dbde7bfdf8" - integrity sha512-6zavDGdzG3gUqAdWvlLFfk+36RilI+Pwyuuh7HItyeScCWP3k6i8vKclAQ0bM/0y/Kz/xiwvxhMv9MgTJP5gmA== - "@babel/parser@^7.23.9": version "7.23.9" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.9.tgz#7b903b6149b0f8fa7ad564af646c4c38a77fc44b" integrity sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA== -"@esbuild/android-arm64@0.19.5": - version "0.19.5" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.5.tgz#276c5f99604054d3dbb733577e09adae944baa90" - integrity sha512-5d1OkoJxnYQfmC+Zd8NBFjkhyCNYwM4n9ODrycTFY6Jk1IGiZ+tjVJDDSwDt77nK+tfpGP4T50iMtVi4dEGzhQ== - -"@esbuild/android-arm@0.19.5": - version "0.19.5" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.5.tgz#4a3cbf14758166abaae8ba9c01a80e68342a4eec" - integrity sha512-bhvbzWFF3CwMs5tbjf3ObfGqbl/17ict2/uwOSfr3wmxDE6VdS2GqY/FuzIPe0q0bdhj65zQsvqfArI9MY6+AA== - -"@esbuild/android-x64@0.19.5": - version "0.19.5" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.5.tgz#21a3d11cd4613d2d3c5ccb9e746c254eb9265b0a" - integrity sha512-9t+28jHGL7uBdkBjL90QFxe7DVA+KGqWlHCF8ChTKyaKO//VLuoBricQCgwhOjA1/qOczsw843Fy4cbs4H3DVA== - -"@esbuild/darwin-arm64@0.19.5": - version "0.19.5" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.5.tgz#714cb839f467d6a67b151ee8255886498e2b9bf6" - integrity sha512-mvXGcKqqIqyKoxq26qEDPHJuBYUA5KizJncKOAf9eJQez+L9O+KfvNFu6nl7SCZ/gFb2QPaRqqmG0doSWlgkqw== - -"@esbuild/darwin-x64@0.19.5": - version "0.19.5" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.5.tgz#2c553e97a6d2b4ae76a884e35e6cbab85a990bbf" - integrity sha512-Ly8cn6fGLNet19s0X4unjcniX24I0RqjPv+kurpXabZYSXGM4Pwpmf85WHJN3lAgB8GSth7s5A0r856S+4DyiA== - -"@esbuild/freebsd-arm64@0.19.5": - version "0.19.5" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.5.tgz#d554f556718adb31917a0da24277bf84b6ee87f3" - integrity sha512-GGDNnPWTmWE+DMchq1W8Sd0mUkL+APvJg3b11klSGUDvRXh70JqLAO56tubmq1s2cgpVCSKYywEiKBfju8JztQ== - -"@esbuild/freebsd-x64@0.19.5": - version "0.19.5" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.5.tgz#288f7358a3bb15d99e73c65c9adaa3dabb497432" - integrity sha512-1CCwDHnSSoA0HNwdfoNY0jLfJpd7ygaLAp5EHFos3VWJCRX9DMwWODf96s9TSse39Br7oOTLryRVmBoFwXbuuQ== - -"@esbuild/linux-arm64@0.19.5": - version "0.19.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.5.tgz#95933ae86325c93cb6b5e8333d22120ecfdc901b" - integrity sha512-o3vYippBmSrjjQUCEEiTZ2l+4yC0pVJD/Dl57WfPwwlvFkrxoSO7rmBZFii6kQB3Wrn/6GwJUPLU5t52eq2meA== - -"@esbuild/linux-arm@0.19.5": - version "0.19.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.5.tgz#0acef93aa3e0579e46d33b666627bddb06636664" - integrity sha512-lrWXLY/vJBzCPC51QN0HM71uWgIEpGSjSZZADQhq7DKhPcI6NH1IdzjfHkDQws2oNpJKpR13kv7/pFHBbDQDwQ== - -"@esbuild/linux-ia32@0.19.5": - version "0.19.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.5.tgz#b6e5c9e80b42131cbd6b1ddaa48c92835f1ed67f" - integrity sha512-MkjHXS03AXAkNp1KKkhSKPOCYztRtK+KXDNkBa6P78F8Bw0ynknCSClO/ztGszILZtyO/lVKpa7MolbBZ6oJtQ== - -"@esbuild/linux-loong64@0.19.5": - version "0.19.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.5.tgz#e5f0cf95a180158b01ff5f417da796a1c09dfbea" - integrity sha512-42GwZMm5oYOD/JHqHska3Jg0r+XFb/fdZRX+WjADm3nLWLcIsN27YKtqxzQmGNJgu0AyXg4HtcSK9HuOk3v1Dw== - -"@esbuild/linux-mips64el@0.19.5": - version "0.19.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.5.tgz#ae36fb86c7d5f641f3a0c8472e83dcb6ea36a408" - integrity sha512-kcjndCSMitUuPJobWCnwQ9lLjiLZUR3QLQmlgaBfMX23UEa7ZOrtufnRds+6WZtIS9HdTXqND4yH8NLoVVIkcg== - -"@esbuild/linux-ppc64@0.19.5": - version "0.19.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.5.tgz#7960cb1666f0340ddd9eef7b26dcea3835d472d0" - integrity sha512-yJAxJfHVm0ZbsiljbtFFP1BQKLc8kUF6+17tjQ78QjqjAQDnhULWiTA6u0FCDmYT1oOKS9PzZ2z0aBI+Mcyj7Q== - -"@esbuild/linux-riscv64@0.19.5": - version "0.19.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.5.tgz#32207df26af60a3a9feea1783fc21b9817bade19" - integrity sha512-5u8cIR/t3gaD6ad3wNt1MNRstAZO+aNyBxu2We8X31bA8XUNyamTVQwLDA1SLoPCUehNCymhBhK3Qim1433Zag== - -"@esbuild/linux-s390x@0.19.5": - version "0.19.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.5.tgz#b38d5681db89a3723862dfa792812397b1510a7d" - integrity sha512-Z6JrMyEw/EmZBD/OFEFpb+gao9xJ59ATsoTNlj39jVBbXqoZm4Xntu6wVmGPB/OATi1uk/DB+yeDPv2E8PqZGw== - -"@esbuild/linux-x64@0.19.5": - version "0.19.5" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.5.tgz#46feba2ad041a241379d150f415b472fe3885075" - integrity sha512-psagl+2RlK1z8zWZOmVdImisMtrUxvwereIdyJTmtmHahJTKb64pAcqoPlx6CewPdvGvUKe2Jw+0Z/0qhSbG1A== - -"@esbuild/netbsd-x64@0.19.5": - version "0.19.5" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.5.tgz#3b5c1fb068f26bfc681d31f682adf1bea4ef0702" - integrity sha512-kL2l+xScnAy/E/3119OggX8SrWyBEcqAh8aOY1gr4gPvw76la2GlD4Ymf832UCVbmuWeTf2adkZDK+h0Z/fB4g== - -"@esbuild/openbsd-x64@0.19.5": - version "0.19.5" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.5.tgz#ca6830316ca68056c5c88a875f103ad3235e00db" - integrity sha512-sPOfhtzFufQfTBgRnE1DIJjzsXukKSvZxloZbkJDG383q0awVAq600pc1nfqBcl0ice/WN9p4qLc39WhBShRTA== - -"@esbuild/sunos-x64@0.19.5": - version "0.19.5" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.5.tgz#9efc4eb9539a7be7d5a05ada52ee43cda0d8e2dd" - integrity sha512-dGZkBXaafuKLpDSjKcB0ax0FL36YXCvJNnztjKV+6CO82tTYVDSH2lifitJ29jxRMoUhgkg9a+VA/B03WK5lcg== - -"@esbuild/win32-arm64@0.19.5": - version "0.19.5" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.5.tgz#29f8184afa7a02a956ebda4ed638099f4b8ff198" - integrity sha512-dWVjD9y03ilhdRQ6Xig1NWNgfLtf2o/STKTS+eZuF90fI2BhbwD6WlaiCGKptlqXlURVB5AUOxUj09LuwKGDTg== - -"@esbuild/win32-ia32@0.19.5": - version "0.19.5" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.5.tgz#f3de07afb292ecad651ae4bb8727789de2d95b05" - integrity sha512-4liggWIA4oDgUxqpZwrDhmEfAH4d0iljanDOK7AnVU89T6CzHon/ony8C5LeOdfgx60x5cnQJFZwEydVlYx4iw== - -"@esbuild/win32-x64@0.19.5": - version "0.19.5" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.5.tgz#faad84c41ba12e3a0acb52571df9bff37bee75f6" - integrity sha512-czTrygUsB/jlM8qEW5MD8bgYU2Xg14lo6kBDXW6HdxKjh8M5PzETGiSHaz9MtbXBYDloHNUAUW2tMiKW4KM9Mw== +"@esbuild/aix-ppc64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537" + integrity sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g== + +"@esbuild/android-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz#db1c9202a5bc92ea04c7b6840f1bbe09ebf9e6b9" + integrity sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg== + +"@esbuild/android-arm@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.2.tgz#3b488c49aee9d491c2c8f98a909b785870d6e995" + integrity sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w== + +"@esbuild/android-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.2.tgz#3b1628029e5576249d2b2d766696e50768449f98" + integrity sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg== + +"@esbuild/darwin-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz#6e8517a045ddd86ae30c6608c8475ebc0c4000bb" + integrity sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA== + +"@esbuild/darwin-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz#90ed098e1f9dd8a9381695b207e1cff45540a0d0" + integrity sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA== + +"@esbuild/freebsd-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz#d71502d1ee89a1130327e890364666c760a2a911" + integrity sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw== + +"@esbuild/freebsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz#aa5ea58d9c1dd9af688b8b6f63ef0d3d60cea53c" + integrity sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw== + +"@esbuild/linux-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz#055b63725df678379b0f6db9d0fa85463755b2e5" + integrity sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A== + +"@esbuild/linux-arm@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz#76b3b98cb1f87936fbc37f073efabad49dcd889c" + integrity sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg== + +"@esbuild/linux-ia32@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz#c0e5e787c285264e5dfc7a79f04b8b4eefdad7fa" + integrity sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig== + +"@esbuild/linux-loong64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz#a6184e62bd7cdc63e0c0448b83801001653219c5" + integrity sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ== + +"@esbuild/linux-mips64el@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz#d08e39ce86f45ef8fc88549d29c62b8acf5649aa" + integrity sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA== + +"@esbuild/linux-ppc64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz#8d252f0b7756ffd6d1cbde5ea67ff8fd20437f20" + integrity sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg== + +"@esbuild/linux-riscv64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz#19f6dcdb14409dae607f66ca1181dd4e9db81300" + integrity sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg== + +"@esbuild/linux-s390x@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz#3c830c90f1a5d7dd1473d5595ea4ebb920988685" + integrity sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ== + +"@esbuild/linux-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz#86eca35203afc0d9de0694c64ec0ab0a378f6fff" + integrity sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw== + +"@esbuild/netbsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz#e771c8eb0e0f6e1877ffd4220036b98aed5915e6" + integrity sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ== + +"@esbuild/openbsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz#9a795ae4b4e37e674f0f4d716f3e226dd7c39baf" + integrity sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ== + +"@esbuild/sunos-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz#7df23b61a497b8ac189def6e25a95673caedb03f" + integrity sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w== + +"@esbuild/win32-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz#f1ae5abf9ca052ae11c1bc806fb4c0f519bacf90" + integrity sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ== + +"@esbuild/win32-ia32@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz#241fe62c34d8e8461cd708277813e1d0ba55ce23" + integrity sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ== + +"@esbuild/win32-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc" + integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ== "@eslint-community/eslint-utils@^4.2.0": version "4.2.0" @@ -171,18 +171,18 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@8.56.0": - version "8.56.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.56.0.tgz#ef20350fec605a7f7035a01764731b2de0f3782b" - integrity sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A== +"@eslint/js@8.57.0": + version "8.57.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" + integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== -"@humanwhocodes/config-array@^0.11.13": - version "0.11.13" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.13.tgz#075dc9684f40a531d9b26b0822153c1e832ee297" - integrity sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ== +"@humanwhocodes/config-array@^0.11.14": + version "0.11.14" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" + integrity sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg== dependencies: - "@humanwhocodes/object-schema" "^2.0.1" - debug "^4.1.1" + "@humanwhocodes/object-schema" "^2.0.2" + debug "^4.3.1" minimatch "^3.0.5" "@humanwhocodes/module-importer@^1.0.1": @@ -190,10 +190,10 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== -"@humanwhocodes/object-schema@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz#e5211452df060fa8522b55c7b3c0c4d1981cb044" - integrity sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw== +"@humanwhocodes/object-schema@^2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz#d9fae00a2d5cb40f92cfe64b47ad749fbc38f917" + integrity sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw== "@intlify/bundle-utils@^7.4.0": version "7.4.0" @@ -211,20 +211,20 @@ source-map-js "^1.0.1" yaml-eslint-parser "^1.2.2" -"@intlify/core-base@9.9.1": - version "9.9.1" - resolved "https://registry.yarnpkg.com/@intlify/core-base/-/core-base-9.9.1.tgz#97ff0a98bf416c3f895e2a4fbcb0da353326b71a" - integrity sha512-qsV15dg7jNX2faBRyKMgZS8UcFJViWEUPLdzZ9UR0kQZpFVeIpc0AG7ZOfeP7pX2T9SQ5jSiorq/tii9nkkafA== +"@intlify/core-base@9.10.2": + version "9.10.2" + resolved "https://registry.yarnpkg.com/@intlify/core-base/-/core-base-9.10.2.tgz#e7f8857f8011184e4afbdcfae7dbd85c50ba5271" + integrity sha512-HGStVnKobsJL0DoYIyRCGXBH63DMQqEZxDUGrkNI05FuTcruYUtOAxyL3zoAZu/uDGO6mcUvm3VXBaHG2GdZCg== dependencies: - "@intlify/message-compiler" "9.9.1" - "@intlify/shared" "9.9.1" + "@intlify/message-compiler" "9.10.2" + "@intlify/shared" "9.10.2" -"@intlify/message-compiler@9.9.1": - version "9.9.1" - resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-9.9.1.tgz#4cd9c5a408be27784928e4cd57a77ea6ddb17e56" - integrity sha512-zTvP6X6HeumHOXuAE1CMMsV6tTX+opKMOxO1OHTCg5N5Sm/F7d8o2jdT6W6L5oHUsJ/vvkGefHIs7Q3hfowmsA== +"@intlify/message-compiler@9.10.2": + version "9.10.2" + resolved "https://registry.yarnpkg.com/@intlify/message-compiler/-/message-compiler-9.10.2.tgz#c44cbb915bdd0d62780a38595a84006c781f717a" + integrity sha512-ntY/kfBwQRtX5Zh6wL8cSATujPzWW2ZQd1QwKyWwAy5fMqJyyixHMeovN4fmEyCqSu+hFfYOE63nU94evsy4YA== dependencies: - "@intlify/shared" "9.9.1" + "@intlify/shared" "9.10.2" source-map-js "^1.0.2" "@intlify/message-compiler@^9.4.0": @@ -235,24 +235,24 @@ "@intlify/shared" "9.4.0" source-map-js "^1.0.2" +"@intlify/shared@9.10.2": + version "9.10.2" + resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-9.10.2.tgz#693300ea033868cbe4086b832170612f002e24a9" + integrity sha512-ttHCAJkRy7R5W2S9RVnN9KYQYPIpV2+GiS79T4EE37nrPyH6/1SrOh3bmdCRC1T3ocL8qCDx7x2lBJ0xaITU7Q== + "@intlify/shared@9.4.0", "@intlify/shared@^9.4.0": version "9.4.0" resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-9.4.0.tgz#4a78d462fc82433db900981e12eb5b1aae3d6085" integrity sha512-AFqymip2kToqA0B6KZPg5jSrdcVHoli9t/VhGKE2iiMq9utFuMoGdDC/JOCIZgwxo6aXAk86QyU2XtzEoMuZ6A== -"@intlify/shared@9.9.1": - version "9.9.1" - resolved "https://registry.yarnpkg.com/@intlify/shared/-/shared-9.9.1.tgz#b602d012b35f6c336b29a8098296dfac96a005f5" - integrity sha512-b3Pta1nwkz5rGq434v0psHwEwHGy1pYCttfcM22IE//K9owbpkEvFptx9VcuRAxjQdrO2If249cmDDjBu5wMDA== - -"@intlify/unplugin-vue-i18n@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@intlify/unplugin-vue-i18n/-/unplugin-vue-i18n-2.0.0.tgz#5b087e17b4eb4381d0a111cd89df4037880e932f" - integrity sha512-1oKvm92L9l2od2H9wKx2ZvR4tzn7gUtd7bPLI7AWUmm7U9H1iEypndt5d985ypxGsEs0gToDaKTrytbBIJwwSg== +"@intlify/unplugin-vue-i18n@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@intlify/unplugin-vue-i18n/-/unplugin-vue-i18n-3.0.1.tgz#8bed58d5cbaadda056c2ff88acf99300db516639" + integrity sha512-q1zJhA/WpoLBzAAuKA5/AEp0e+bMOM10ll/HxT4g1VAw/9JhC4TTobP9KobKH90JMZ4U2daLFlYQfKNd29lpqw== dependencies: "@intlify/bundle-utils" "^7.4.0" "@intlify/shared" "^9.4.0" - "@rollup/pluginutils" "^5.0.2" + "@rollup/pluginutils" "^5.1.0" "@vue/compiler-sfc" "^3.2.47" debug "^4.3.3" fast-glob "^3.2.12" @@ -339,79 +339,84 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.5.tgz#db5a11bf66bdab39569719555b0f76e138d7bd64" integrity sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw== -"@rollup/pluginutils@^5.0.2": - version "5.0.2" - resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.0.2.tgz#012b8f53c71e4f6f9cb317e311df1404f56e7a33" - integrity sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA== +"@rollup/pluginutils@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.1.0.tgz#7e53eddc8c7f483a4ad0b94afb1f7f5fd3c771e0" + integrity sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g== dependencies: "@types/estree" "^1.0.0" estree-walker "^2.0.2" picomatch "^2.3.1" -"@rollup/rollup-android-arm-eabi@4.4.1": - version "4.4.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.4.1.tgz#f276b0fa322270aa42d1f56c982db6ef8d6a4393" - integrity sha512-Ss4suS/sd+6xLRu+MLCkED2mUrAyqHmmvZB+zpzZ9Znn9S8wCkTQCJaQ8P8aHofnvG5L16u9MVnJjCqioPErwQ== - -"@rollup/rollup-android-arm64@4.4.1": - version "4.4.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.4.1.tgz#f0492f00d18e1067785f8e820e137c00528c5e62" - integrity sha512-sRSkGTvGsARwWd7TzC8LKRf8FiPn7257vd/edzmvG4RIr9x68KBN0/Ek48CkuUJ5Pj/Dp9vKWv6PEupjKWjTYA== - -"@rollup/rollup-darwin-arm64@4.4.1": - version "4.4.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.4.1.tgz#40443db7f4559171d797581e0618ec1a4c8dcee9" - integrity sha512-nz0AiGrrXyaWpsmBXUGOBiRDU0wyfSXbFuF98pPvIO8O6auQsPG6riWsfQqmCCC5FNd8zKQ4JhgugRNAkBJ8mQ== - -"@rollup/rollup-darwin-x64@4.4.1": - version "4.4.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.4.1.tgz#2868f37a9f9c2c22c091b6209f6ce7454437edf9" - integrity sha512-Ogqvf4/Ve/faMaiPRvzsJEqajbqs00LO+8vtrPBVvLgdw4wBg6ZDXdkDAZO+4MLnrc8mhGV6VJAzYScZdPLtJg== - -"@rollup/rollup-linux-arm-gnueabihf@4.4.1": - version "4.4.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.4.1.tgz#d78d7ad358d24058166ab5599de3dcb5ab951add" - integrity sha512-9zc2tqlr6HfO+hx9+wktUlWTRdje7Ub15iJqKcqg5uJZ+iKqmd2CMxlgPpXi7+bU7bjfDIuvCvnGk7wewFEhCg== - -"@rollup/rollup-linux-arm64-gnu@4.4.1": - version "4.4.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.4.1.tgz#5d07588b40a04f5b6fbd9e0169c8dc32c1c2ed21" - integrity sha512-phLb1fN3rq2o1j1v+nKxXUTSJnAhzhU0hLrl7Qzb0fLpwkGMHDem+o6d+ZI8+/BlTXfMU4kVWGvy6g9k/B8L6Q== - -"@rollup/rollup-linux-arm64-musl@4.4.1": - version "4.4.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.4.1.tgz#d452e88a02755f449f6e98d4ce424d655ef42cfe" - integrity sha512-M2sDtw4tf57VPSjbTAN/lz1doWUqO2CbQuX3L9K6GWIR5uw9j+ROKCvvUNBY8WUbMxwaoc8mH9HmmBKsLht7+w== - -"@rollup/rollup-linux-x64-gnu@4.4.1": - version "4.4.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.4.1.tgz#e8e8e87ab098784383a5ced4aa4bbfa7b2c92a4e" - integrity sha512-mHIlRLX+hx+30cD6c4BaBOsSqdnCE4ok7/KDvjHYAHoSuveoMMxIisZFvcLhUnyZcPBXDGZTuBoalcuh43UfQQ== - -"@rollup/rollup-linux-x64-musl@4.4.1": - version "4.4.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.4.1.tgz#3e5da42626672e2d620ed12746158b0cf6143b23" - integrity sha512-tB+RZuDi3zxFx7vDrjTNGVLu2KNyzYv+UY8jz7e4TMEoAj7iEt8Qk6xVu6mo3pgjnsHj6jnq3uuRsHp97DLwOA== - -"@rollup/rollup-win32-arm64-msvc@4.4.1": - version "4.4.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.4.1.tgz#0f0d0c6b75c53643fab8238c76889a95bca3b9cc" - integrity sha512-Hdn39PzOQowK/HZzYpCuZdJC91PE6EaGbTe2VCA9oq2u18evkisQfws0Smh9QQGNNRa/T7MOuGNQoLeXhhE3PQ== - -"@rollup/rollup-win32-ia32-msvc@4.4.1": - version "4.4.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.4.1.tgz#8bb9e8fbf0fdf96fe3bebcee23f5cfdbbd9a4a0a" - integrity sha512-tLpKb1Elm9fM8c5w3nl4N1eLTP4bCqTYw9tqUBxX8/hsxqHO3dxc2qPbZ9PNkdK4tg4iLEYn0pOUnVByRd2CbA== - -"@rollup/rollup-win32-x64-msvc@4.4.1": - version "4.4.1" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.4.1.tgz#8311b77e6cce322865ba12ada8c3779369610d18" - integrity sha512-eAhItDX9yQtZVM3yvXS/VR3qPqcnXvnLyx1pLXl4JzyNMBNO3KC986t/iAg2zcMzpAp9JSvxB5VZGnBiNoA98w== - -"@rushstack/eslint-patch@^1.7.2": - version "1.7.2" - resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.7.2.tgz#2d4260033e199b3032a08b41348ac10de21c47e9" - integrity sha512-RbhOOTCNoCrbfkRyoXODZp75MlpiHMgbE5MEBZAnnnLyQNgrigEj4p0lzsMDyc1zVsJDLrivB58tgg3emX0eEA== +"@rollup/rollup-android-arm-eabi@4.13.0": + version "4.13.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz#b98786c1304b4ff8db3a873180b778649b5dff2b" + integrity sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg== + +"@rollup/rollup-android-arm64@4.13.0": + version "4.13.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz#8833679af11172b1bf1ab7cb3bad84df4caf0c9e" + integrity sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q== + +"@rollup/rollup-darwin-arm64@4.13.0": + version "4.13.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz#ef02d73e0a95d406e0eb4fd61a53d5d17775659b" + integrity sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g== + +"@rollup/rollup-darwin-x64@4.13.0": + version "4.13.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz#3ce5b9bcf92b3341a5c1c58a3e6bcce0ea9e7455" + integrity sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg== + +"@rollup/rollup-linux-arm-gnueabihf@4.13.0": + version "4.13.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz#3d3d2c018bdd8e037c6bfedd52acfff1c97e4be4" + integrity sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ== + +"@rollup/rollup-linux-arm64-gnu@4.13.0": + version "4.13.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz#5fc8cc978ff396eaa136d7bfe05b5b9138064143" + integrity sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w== + +"@rollup/rollup-linux-arm64-musl@4.13.0": + version "4.13.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz#f2ae7d7bed416ffa26d6b948ac5772b520700eef" + integrity sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw== + +"@rollup/rollup-linux-riscv64-gnu@4.13.0": + version "4.13.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz#303d57a328ee9a50c85385936f31cf62306d30b6" + integrity sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA== + +"@rollup/rollup-linux-x64-gnu@4.13.0": + version "4.13.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz#f672f6508f090fc73f08ba40ff76c20b57424778" + integrity sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA== + +"@rollup/rollup-linux-x64-musl@4.13.0": + version "4.13.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz#d2f34b1b157f3e7f13925bca3288192a66755a89" + integrity sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw== + +"@rollup/rollup-win32-arm64-msvc@4.13.0": + version "4.13.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz#8ffecc980ae4d9899eb2f9c4ae471a8d58d2da6b" + integrity sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA== + +"@rollup/rollup-win32-ia32-msvc@4.13.0": + version "4.13.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz#a7505884f415662e088365b9218b2b03a88fc6f2" + integrity sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw== + +"@rollup/rollup-win32-x64-msvc@4.13.0": + version "4.13.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz#6abd79db7ff8d01a58865ba20a63cfd23d9e2a10" + integrity sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw== + +"@rushstack/eslint-patch@^1.8.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.8.0.tgz#c5545e6a5d2bd5c26b4021c357177a28698c950e" + integrity sha512-0HejFckBN2W+ucM6cUOlwsByTKt9/+0tWhqUffNIcHqCXkthY/mZ7AuYPK/2IIaGWhdl0h+tICDO0ssLMd6XMQ== "@tsconfig/node18@^18.2.2": version "18.2.2" @@ -425,6 +430,11 @@ dependencies: "@popperjs/core" "^2.9.2" +"@types/estree@1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== + "@types/estree@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2" @@ -435,10 +445,10 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA== -"@types/node@^20.11.19": - version "20.11.19" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.19.tgz#b466de054e9cb5b3831bee38938de64ac7f81195" - integrity sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ== +"@types/node@^20.11.30": + version "20.11.30" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.30.tgz#9c33467fc23167a347e73834f788f4b9f399d66f" + integrity sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw== dependencies: undici-types "~5.26.4" @@ -452,26 +462,26 @@ resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.1.tgz#0480eeb7221eb9bc398ad7432c9d7e14b1a5a367" integrity sha512-cJRQXpObxfNKkFAZbJl2yjWtJCqELQIdShsogr1d2MilP8dKD9TE/nEKHkJgUNHdGKCQaf9HbIynuV2csLGVLg== -"@types/sortablejs@^1.15.7": - version "1.15.7" - resolved "https://registry.yarnpkg.com/@types/sortablejs/-/sortablejs-1.15.7.tgz#11f85e98fce2854708e5c6d6011f7a236d79ae9f" - integrity sha512-PvgWCx1Lbgm88FdQ6S7OGvLIjWS66mudKPlfdrWil0TjsO5zmoZmzoKiiwRShs1dwPgrlkr0N4ewuy0/+QUXYQ== +"@types/sortablejs@^1.15.8": + version "1.15.8" + resolved "https://registry.yarnpkg.com/@types/sortablejs/-/sortablejs-1.15.8.tgz#11ed555076046e00869a5ef85d1e7651e7a66ef6" + integrity sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg== "@types/spark-md5@^3.0.4": version "3.0.4" resolved "https://registry.yarnpkg.com/@types/spark-md5/-/spark-md5-3.0.4.tgz#c1221d63c069d95aba0c06a765b80661cacc12bf" integrity sha512-qtOaDz+IXiNndPgYb6t1YoutnGvFRtWSNzpVjkAPCfB2UzTyybuD4Tjgs7VgRawum3JnJNRwNQd4N//SvrHg1Q== -"@typescript-eslint/eslint-plugin@^6.7.0": - version "6.7.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.0.tgz#ed2a38867190f8a688af85ad7c8a74670b8b3675" - integrity sha512-gUqtknHm0TDs1LhY12K2NA3Rmlmp88jK9Tx8vGZMfHeNMLE3GH2e9TRub+y+SOjuYgtOmok+wt1AyDPZqxbNag== +"@typescript-eslint/eslint-plugin@^7.1.1": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.2.0.tgz#5a5fcad1a7baed85c10080d71ad901f98c38d5b7" + integrity sha512-mdekAHOqS9UjlmyF/LSs6AIEvfceV749GFxoBAjwAv0nkevfKHWQFDMcBZWUiIC5ft6ePWivXoS36aKQ0Cy3sw== dependencies: "@eslint-community/regexpp" "^4.5.1" - "@typescript-eslint/scope-manager" "6.7.0" - "@typescript-eslint/type-utils" "6.7.0" - "@typescript-eslint/utils" "6.7.0" - "@typescript-eslint/visitor-keys" "6.7.0" + "@typescript-eslint/scope-manager" "7.2.0" + "@typescript-eslint/type-utils" "7.2.0" + "@typescript-eslint/utils" "7.2.0" + "@typescript-eslint/visitor-keys" "7.2.0" debug "^4.3.4" graphemer "^1.4.0" ignore "^5.2.4" @@ -479,72 +489,73 @@ semver "^7.5.4" ts-api-utils "^1.0.1" -"@typescript-eslint/parser@^6.7.0": - version "6.7.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.7.0.tgz#332fe9c7ecf6783d3250b4c8a960bd4af0995807" - integrity sha512-jZKYwqNpNm5kzPVP5z1JXAuxjtl2uG+5NpaMocFPTNC2EdYIgbXIPImObOkhbONxtFTTdoZstLZefbaK+wXZng== +"@typescript-eslint/parser@^7.1.1": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.2.0.tgz#44356312aea8852a3a82deebdacd52ba614ec07a" + integrity sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg== dependencies: - "@typescript-eslint/scope-manager" "6.7.0" - "@typescript-eslint/types" "6.7.0" - "@typescript-eslint/typescript-estree" "6.7.0" - "@typescript-eslint/visitor-keys" "6.7.0" + "@typescript-eslint/scope-manager" "7.2.0" + "@typescript-eslint/types" "7.2.0" + "@typescript-eslint/typescript-estree" "7.2.0" + "@typescript-eslint/visitor-keys" "7.2.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@6.7.0": - version "6.7.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.7.0.tgz#6b3c22187976e2bf5ed0dc0d9095f1f2cbd1d106" - integrity sha512-lAT1Uau20lQyjoLUQ5FUMSX/dS07qux9rYd5FGzKz/Kf8W8ccuvMyldb8hadHdK/qOI7aikvQWqulnEq2nCEYA== +"@typescript-eslint/scope-manager@7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.2.0.tgz#cfb437b09a84f95a0930a76b066e89e35d94e3da" + integrity sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg== dependencies: - "@typescript-eslint/types" "6.7.0" - "@typescript-eslint/visitor-keys" "6.7.0" + "@typescript-eslint/types" "7.2.0" + "@typescript-eslint/visitor-keys" "7.2.0" -"@typescript-eslint/type-utils@6.7.0": - version "6.7.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.7.0.tgz#21a013d4c7f96255f5e64ac59fb41301d1e052ba" - integrity sha512-f/QabJgDAlpSz3qduCyQT0Fw7hHpmhOzY/Rv6zO3yO+HVIdPfIWhrQoAyG+uZVtWAIS85zAyzgAFfyEr+MgBpg== +"@typescript-eslint/type-utils@7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.2.0.tgz#7be5c30e9b4d49971b79095a1181324ef6089a19" + integrity sha512-xHi51adBHo9O9330J8GQYQwrKBqbIPJGZZVQTHHmy200hvkLZFWJIFtAG/7IYTWUyun6DE6w5InDReePJYJlJA== dependencies: - "@typescript-eslint/typescript-estree" "6.7.0" - "@typescript-eslint/utils" "6.7.0" + "@typescript-eslint/typescript-estree" "7.2.0" + "@typescript-eslint/utils" "7.2.0" debug "^4.3.4" ts-api-utils "^1.0.1" -"@typescript-eslint/types@6.7.0": - version "6.7.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.7.0.tgz#8de8ba9cafadc38e89003fe303e219c9250089ae" - integrity sha512-ihPfvOp7pOcN/ysoj0RpBPOx3HQTJTrIN8UZK+WFd3/iDeFHHqeyYxa4hQk4rMhsz9H9mXpR61IzwlBVGXtl9Q== +"@typescript-eslint/types@7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.2.0.tgz#0feb685f16de320e8520f13cca30779c8b7c403f" + integrity sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA== -"@typescript-eslint/typescript-estree@6.7.0": - version "6.7.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.0.tgz#20ce2801733bd46f02cc0f141f5b63fbbf2afb63" - integrity sha512-dPvkXj3n6e9yd/0LfojNU8VMUGHWiLuBZvbM6V6QYD+2qxqInE7J+J/ieY2iGwR9ivf/R/haWGkIj04WVUeiSQ== +"@typescript-eslint/typescript-estree@7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.2.0.tgz#5beda2876c4137f8440c5a84b4f0370828682556" + integrity sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA== dependencies: - "@typescript-eslint/types" "6.7.0" - "@typescript-eslint/visitor-keys" "6.7.0" + "@typescript-eslint/types" "7.2.0" + "@typescript-eslint/visitor-keys" "7.2.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" + minimatch "9.0.3" semver "^7.5.4" ts-api-utils "^1.0.1" -"@typescript-eslint/utils@6.7.0": - version "6.7.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.7.0.tgz#61b6f1f1b82ad529abfcee074d21764e880886fb" - integrity sha512-MfCq3cM0vh2slSikQYqK2Gq52gvOhe57vD2RM3V4gQRZYX4rDPnKLu5p6cm89+LJiGlwEXU8hkYxhqqEC/V3qA== +"@typescript-eslint/utils@7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.2.0.tgz#fc8164be2f2a7068debb4556881acddbf0b7ce2a" + integrity sha512-YfHpnMAGb1Eekpm3XRK8hcMwGLGsnT6L+7b2XyRv6ouDuJU1tZir1GS2i0+VXRatMwSI1/UfcyPe53ADkU+IuA== dependencies: "@eslint-community/eslint-utils" "^4.4.0" "@types/json-schema" "^7.0.12" "@types/semver" "^7.5.0" - "@typescript-eslint/scope-manager" "6.7.0" - "@typescript-eslint/types" "6.7.0" - "@typescript-eslint/typescript-estree" "6.7.0" + "@typescript-eslint/scope-manager" "7.2.0" + "@typescript-eslint/types" "7.2.0" + "@typescript-eslint/typescript-estree" "7.2.0" semver "^7.5.4" -"@typescript-eslint/visitor-keys@6.7.0": - version "6.7.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.0.tgz#34140ac76dfb6316d17012e4469acf3366ad3f44" - integrity sha512-/C1RVgKFDmGMcVGeD8HjKv2bd72oI1KxQDeY8uc66gw9R0OK0eMq48cA+jv9/2Ag6cdrsUGySm1yzYmfz0hxwQ== +"@typescript-eslint/visitor-keys@7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.2.0.tgz#5035f177752538a5750cca1af6044b633610bf9e" + integrity sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A== dependencies: - "@typescript-eslint/types" "6.7.0" + "@typescript-eslint/types" "7.2.0" eslint-visitor-keys "^3.4.1" "@ungap/structured-clone@^1.2.0": @@ -557,26 +568,26 @@ resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.0.4.tgz#508d6a0f2440f86945835d903fcc0d95d1bb8a37" integrity sha512-WS3hevEszI6CEVEx28F8RjTX97k3KsrcY6kvTg7+Whm5y3oYvcqzVeGCU3hxSAn4uY2CLCkeokkGKpoctccilQ== -"@volar/language-core@1.11.1", "@volar/language-core@~1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-1.11.1.tgz#ecdf12ea8dc35fb8549e517991abcbf449a5ad4f" - integrity sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw== +"@volar/language-core@2.1.3", "@volar/language-core@~2.1.3": + version "2.1.3" + resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-2.1.3.tgz#ac6057ec73c5fcda1fc07677bf0d7be41e6c59b1" + integrity sha512-F93KYZYqcYltG7NihfnLt/omMZOtrQtsh2+wj+cgx3xolopU+TZvmwlZWOjw3ObZGFj3SKBb4jJn6VSfSch6RA== dependencies: - "@volar/source-map" "1.11.1" + "@volar/source-map" "2.1.3" -"@volar/source-map@1.11.1", "@volar/source-map@~1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@volar/source-map/-/source-map-1.11.1.tgz#535b0328d9e2b7a91dff846cab4058e191f4452f" - integrity sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg== +"@volar/source-map@2.1.3": + version "2.1.3" + resolved "https://registry.yarnpkg.com/@volar/source-map/-/source-map-2.1.3.tgz#8f3cb110019c45fa4cd47ad2f5fe5469bd54b9e3" + integrity sha512-j+R+NG/OlDgdNMttADxNuSM9Z26StT/Bjw0NgSydI05Vihngn9zvaP/xXwfWs5qQrRzbKVFxJebS2ks5m/URuA== dependencies: - muggle-string "^0.3.1" + muggle-string "^0.4.0" -"@volar/typescript@~1.11.1": - version "1.11.1" - resolved "https://registry.yarnpkg.com/@volar/typescript/-/typescript-1.11.1.tgz#ba86c6f326d88e249c7f5cfe4b765be3946fd627" - integrity sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ== +"@volar/typescript@~2.1.3": + version "2.1.3" + resolved "https://registry.yarnpkg.com/@volar/typescript/-/typescript-2.1.3.tgz#bfdc901afd44c2d05697967211aa55d53fb8bf69" + integrity sha512-ZZqLMih4mvu2eJAW3UCFm84OM/ojYMoA/BU/W1TctT5F2nVzNJmW4jxMWmP3wQzxCbATfTa5gLb1+BSI9NBMBg== dependencies: - "@volar/language-core" "1.11.1" + "@volar/language-core" "2.1.3" path-browserify "^1.0.1" "@vue/compiler-core@3.2.47": @@ -589,23 +600,13 @@ estree-walker "^2.0.2" source-map "^0.6.1" -"@vue/compiler-core@3.3.2": - version "3.3.2" - resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.3.2.tgz#39567bd15c7f97add97bfc4d44e814df36eb797b" - integrity sha512-CKZWo1dzsQYTNTft7whzjL0HsrEpMfiK7pjZ2WFE3bC1NA7caUjWioHSK+49y/LK7Bsm4poJZzAMnvZMQ7OTeg== - dependencies: - "@babel/parser" "^7.21.3" - "@vue/shared" "3.3.2" - estree-walker "^2.0.2" - source-map-js "^1.0.2" - -"@vue/compiler-core@3.4.19": - version "3.4.19" - resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.4.19.tgz#3161b1ede69da00f3ce8155dfab907a3eaa0515e" - integrity sha512-gj81785z0JNzRcU0Mq98E56e4ltO1yf8k5PQ+tV/7YHnbZkrM0fyFyuttnN8ngJZjbpofWE/m4qjKBiLl8Ju4w== +"@vue/compiler-core@3.4.21": + version "3.4.21" + resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.4.21.tgz#868b7085378fc24e58c9aed14c8d62110a62be1a" + integrity sha512-MjXawxZf2SbZszLPYxaFCjxfibYrzr3eYbKxwpLR9EQN+oaziSu3qKVbwBERj1IFIB8OLUewxB5m/BFzi613og== dependencies: "@babel/parser" "^7.23.9" - "@vue/shared" "3.4.19" + "@vue/shared" "3.4.21" entities "^4.5.0" estree-walker "^2.0.2" source-map-js "^1.0.2" @@ -618,35 +619,27 @@ "@vue/compiler-core" "3.2.47" "@vue/shared" "3.2.47" -"@vue/compiler-dom@3.4.19": - version "3.4.19" - resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.4.19.tgz#2457e57e978f431e3b5fd11fc50a3e92d5816f9a" - integrity sha512-vm6+cogWrshjqEHTzIDCp72DKtea8Ry/QVpQRYoyTIg9k7QZDX6D8+HGURjtmatfgM8xgCFtJJaOlCaRYRK3QA== - dependencies: - "@vue/compiler-core" "3.4.19" - "@vue/shared" "3.4.19" - -"@vue/compiler-dom@^3.3.0": - version "3.3.2" - resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.3.2.tgz#2012ef4879375a4ca4ee68012a9256398b848af2" - integrity sha512-6gS3auANuKXLw0XH6QxkWqyPYPunziS2xb6VRenM3JY7gVfZcJvkCBHkb5RuNY1FCbBO3lkIi0CdXUCW1c7SXw== +"@vue/compiler-dom@3.4.21", "@vue/compiler-dom@^3.4.0": + version "3.4.21" + resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.4.21.tgz#0077c355e2008207283a5a87d510330d22546803" + integrity sha512-IZC6FKowtT1sl0CR5DpXSiEB5ayw75oT2bma1BEhV7RRR1+cfwLrxc2Z8Zq/RGFzJ8w5r9QtCOvTjQgdn0IKmA== dependencies: - "@vue/compiler-core" "3.3.2" - "@vue/shared" "3.3.2" + "@vue/compiler-core" "3.4.21" + "@vue/shared" "3.4.21" -"@vue/compiler-sfc@3.4.19": - version "3.4.19" - resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.4.19.tgz#33b238ded6d63e51f6a7048b742626f6007df129" - integrity sha512-LQ3U4SN0DlvV0xhr1lUsgLCYlwQfUfetyPxkKYu7dkfvx7g3ojrGAkw0AERLOKYXuAGnqFsEuytkdcComei3Yg== +"@vue/compiler-sfc@3.4.21": + version "3.4.21" + resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.4.21.tgz#4af920dc31ab99e1ff5d152b5fe0ad12181145b2" + integrity sha512-me7epoTxYlY+2CUM7hy9PCDdpMPfIwrOvAXud2Upk10g4YLv9UBW7kL798TvMeDhPthkZ0CONNrK2GoeI1ODiQ== dependencies: "@babel/parser" "^7.23.9" - "@vue/compiler-core" "3.4.19" - "@vue/compiler-dom" "3.4.19" - "@vue/compiler-ssr" "3.4.19" - "@vue/shared" "3.4.19" + "@vue/compiler-core" "3.4.21" + "@vue/compiler-dom" "3.4.21" + "@vue/compiler-ssr" "3.4.21" + "@vue/shared" "3.4.21" estree-walker "^2.0.2" - magic-string "^0.30.6" - postcss "^8.4.33" + magic-string "^0.30.7" + postcss "^8.4.35" source-map-js "^1.0.2" "@vue/compiler-sfc@^3.2.47": @@ -673,40 +666,43 @@ "@vue/compiler-dom" "3.2.47" "@vue/shared" "3.2.47" -"@vue/compiler-ssr@3.4.19": - version "3.4.19" - resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.4.19.tgz#1f8ee06005ebbaa354f8783fad84e9f7ea4a69c2" - integrity sha512-P0PLKC4+u4OMJ8sinba/5Z/iDT84uMRRlrWzadgLA69opCpI1gG4N55qDSC+dedwq2fJtzmGald05LWR5TFfLw== +"@vue/compiler-ssr@3.4.21": + version "3.4.21" + resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.4.21.tgz#b84ae64fb9c265df21fc67f7624587673d324fef" + integrity sha512-M5+9nI2lPpAsgXOGQobnIueVqc9sisBFexh5yMIMRAPYLa7+5wEJs8iqOZc1WAa9WQbx9GR2twgznU8LTIiZ4Q== dependencies: - "@vue/compiler-dom" "3.4.19" - "@vue/shared" "3.4.19" + "@vue/compiler-dom" "3.4.21" + "@vue/shared" "3.4.21" "@vue/devtools-api@^6.5.0": version "6.5.0" resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.5.0.tgz#98b99425edee70b4c992692628fa1ea2c1e57d07" integrity sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q== -"@vue/eslint-config-typescript@^12.0.0": - version "12.0.0" - resolved "https://registry.yarnpkg.com/@vue/eslint-config-typescript/-/eslint-config-typescript-12.0.0.tgz#0ce22d97af5e4155f3f2e7b21a48cfde8a6f3365" - integrity sha512-StxLFet2Qe97T8+7L8pGlhYBBr8Eg05LPuTDVopQV6il+SK6qqom59BA/rcFipUef2jD8P2X44Vd8tMFytfvlg== +"@vue/devtools-api@^6.5.1": + version "6.6.1" + resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.6.1.tgz#7c14346383751d9f6ad4bea0963245b30220ef83" + integrity sha512-LgPscpE3Vs0x96PzSSB4IGVSZXZBZHpfxs+ZA1d+VEPwHdOXowy/Y2CsvCAIFrf+ssVU1pD1jidj505EpUnfbA== + +"@vue/eslint-config-typescript@^13.0.0": + version "13.0.0" + resolved "https://registry.yarnpkg.com/@vue/eslint-config-typescript/-/eslint-config-typescript-13.0.0.tgz#f5f3d986ace34a10f403921d5044831b89a1b679" + integrity sha512-MHh9SncG/sfqjVqjcuFLOLD6Ed4dRAis4HNt0dXASeAuLqIAx4YMB1/m2o4pUKK1vCt8fUvYG8KKX2Ot3BVZTg== dependencies: - "@typescript-eslint/eslint-plugin" "^6.7.0" - "@typescript-eslint/parser" "^6.7.0" + "@typescript-eslint/eslint-plugin" "^7.1.1" + "@typescript-eslint/parser" "^7.1.1" vue-eslint-parser "^9.3.1" -"@vue/language-core@1.8.27": - version "1.8.27" - resolved "https://registry.yarnpkg.com/@vue/language-core/-/language-core-1.8.27.tgz#2ca6892cb524e024a44e554e4c55d7a23e72263f" - integrity sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA== +"@vue/language-core@2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@vue/language-core/-/language-core-2.0.7.tgz#af12f752a93c4d2498626fca33f5d1ddc8c5ceb9" + integrity sha512-Vh1yZX3XmYjn9yYLkjU8DN6L0ceBtEcapqiyclHne8guG84IaTzqtvizZB1Yfxm3h6m7EIvjerLO5fvOZO6IIQ== dependencies: - "@volar/language-core" "~1.11.1" - "@volar/source-map" "~1.11.1" - "@vue/compiler-dom" "^3.3.0" - "@vue/shared" "^3.3.0" + "@volar/language-core" "~2.1.3" + "@vue/compiler-dom" "^3.4.0" + "@vue/shared" "^3.4.0" computeds "^0.0.1" minimatch "^9.0.3" - muggle-string "^0.3.1" path-browserify "^1.0.1" vue-template-compiler "^2.7.14" @@ -721,52 +717,47 @@ estree-walker "^2.0.2" magic-string "^0.25.7" -"@vue/reactivity@3.4.19": - version "3.4.19" - resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.4.19.tgz#8cf335d97d07881d8184cb23289289dc18b03f60" - integrity sha512-+VcwrQvLZgEclGZRHx4O2XhyEEcKaBi50WbxdVItEezUf4fqRh838Ix6amWTdX0CNb/b6t3Gkz3eOebfcSt+UA== +"@vue/reactivity@3.4.21": + version "3.4.21" + resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.4.21.tgz#affd3415115b8ebf4927c8d2a0d6a24bccfa9f02" + integrity sha512-UhenImdc0L0/4ahGCyEzc/pZNwVgcglGy9HVzJ1Bq2Mm9qXOpP8RyNTjookw/gOCUlXSEtuZ2fUg5nrHcoqJcw== dependencies: - "@vue/shared" "3.4.19" + "@vue/shared" "3.4.21" -"@vue/runtime-core@3.4.19": - version "3.4.19" - resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.4.19.tgz#ef10357fdf3afdf68523b55424541000105e2aeb" - integrity sha512-/Z3tFwOrerJB/oyutmJGoYbuoadphDcJAd5jOuJE86THNZji9pYjZroQ2NFsZkTxOq0GJbb+s2kxTYToDiyZzw== +"@vue/runtime-core@3.4.21": + version "3.4.21" + resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.4.21.tgz#3749c3f024a64c4c27ecd75aea4ca35634db0062" + integrity sha512-pQthsuYzE1XcGZznTKn73G0s14eCJcjaLvp3/DKeYWoFacD9glJoqlNBxt3W2c5S40t6CCcpPf+jG01N3ULyrA== dependencies: - "@vue/reactivity" "3.4.19" - "@vue/shared" "3.4.19" + "@vue/reactivity" "3.4.21" + "@vue/shared" "3.4.21" -"@vue/runtime-dom@3.4.19": - version "3.4.19" - resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.4.19.tgz#079141e31d9f47515b9595f29843d51011f88739" - integrity sha512-IyZzIDqfNCF0OyZOauL+F4yzjMPN2rPd8nhqPP2N1lBn3kYqJpPHHru+83Rkvo2lHz5mW+rEeIMEF9qY3PB94g== +"@vue/runtime-dom@3.4.21": + version "3.4.21" + resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.4.21.tgz#91f867ef64eff232cac45095ab28ebc93ac74588" + integrity sha512-gvf+C9cFpevsQxbkRBS1NpU8CqxKw0ebqMvLwcGQrNpx6gqRDodqKqA+A2VZZpQ9RpK2f9yfg8VbW/EpdFUOJw== dependencies: - "@vue/runtime-core" "3.4.19" - "@vue/shared" "3.4.19" + "@vue/runtime-core" "3.4.21" + "@vue/shared" "3.4.21" csstype "^3.1.3" -"@vue/server-renderer@3.4.19": - version "3.4.19" - resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.4.19.tgz#e6f8ff5268d0758766ca9835375218924d5f0eb6" - integrity sha512-eAj2p0c429RZyyhtMRnttjcSToch+kTWxFPHlzGMkR28ZbF1PDlTcmGmlDxccBuqNd9iOQ7xPRPAGgPVj+YpQw== +"@vue/server-renderer@3.4.21": + version "3.4.21" + resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.4.21.tgz#150751579d26661ee3ed26a28604667fa4222a97" + integrity sha512-aV1gXyKSN6Rz+6kZ6kr5+Ll14YzmIbeuWe7ryJl5muJ4uwSwY/aStXTixx76TwkZFJLm1aAlA/HSWEJ4EyiMkg== dependencies: - "@vue/compiler-ssr" "3.4.19" - "@vue/shared" "3.4.19" + "@vue/compiler-ssr" "3.4.21" + "@vue/shared" "3.4.21" "@vue/shared@3.2.47": version "3.2.47" resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.47.tgz#e597ef75086c6e896ff5478a6bfc0a7aa4bbd14c" integrity sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ== -"@vue/shared@3.3.2", "@vue/shared@^3.3.0": - version "3.3.2" - resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.3.2.tgz#774cd9b4635ce801b70a3fc3713779a5ef5d77c3" - integrity sha512-0rFu3h8JbclbnvvKrs7Fe5FNGV9/5X2rPD7KmOzhLSUAiQH5//Hq437Gv0fR5Mev3u/nbtvmLl8XgwCU20/ZfQ== - -"@vue/shared@3.4.19": - version "3.4.19" - resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.4.19.tgz#28105147811bcf1e6612bf1c9ab0c6d91ada019c" - integrity sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw== +"@vue/shared@3.4.21", "@vue/shared@^3.4.0": + version "3.4.21" + resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.4.21.tgz#de526a9059d0a599f0b429af7037cd0c3ed7d5a1" + integrity sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g== "@vue/tsconfig@^0.5.1": version "0.5.1" @@ -860,10 +851,10 @@ bootstrap-icons-vue@^1.11.3: resolved "https://registry.yarnpkg.com/bootstrap-icons-vue/-/bootstrap-icons-vue-1.11.3.tgz#717745c433b2043d6d1ec24260b9bbc9eea16c66" integrity sha512-Xba1GTDYon8KYSDTKiiAtiyfk4clhdKQYvCQPMkE58+F5loVwEmh0Wi+ECCfowNc9SGwpoSLpSkvg7rhgZBttw== -bootstrap@^5.3.2: - version "5.3.2" - resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.3.2.tgz#97226583f27aae93b2b28ab23f4c114757ff16ae" - integrity sha512-D32nmNWiQHo94BKHLmOrdjlL05q1c8oxbtBphQFb9Z5to6eGRDCm0QgeaZ4zFBHzfg2++rqa2JkqCcxDy0sH0g== +bootstrap@^5.3.3: + version "5.3.3" + resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.3.3.tgz#de35e1a765c897ac940021900fcbb831602bac38" + integrity sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg== brace-expansion@^1.1.7: version "1.1.11" @@ -1011,7 +1002,7 @@ de-indent@^1.0.2: resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" integrity sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg== -debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: +debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -1096,33 +1087,34 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" -esbuild@^0.19.3: - version "0.19.5" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.5.tgz#53a0e19dfbf61ba6c827d51a80813cf071239a8c" - integrity sha512-bUxalY7b1g8vNhQKdB24QDmHeY4V4tw/s6Ak5z+jJX9laP5MoQseTOMemAr0gxssjNcH0MCViG8ONI2kksvfFQ== +esbuild@^0.20.1: + version "0.20.2" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.20.2.tgz#9d6b2386561766ee6b5a55196c6d766d28c87ea1" + integrity sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g== optionalDependencies: - "@esbuild/android-arm" "0.19.5" - "@esbuild/android-arm64" "0.19.5" - "@esbuild/android-x64" "0.19.5" - "@esbuild/darwin-arm64" "0.19.5" - "@esbuild/darwin-x64" "0.19.5" - "@esbuild/freebsd-arm64" "0.19.5" - "@esbuild/freebsd-x64" "0.19.5" - "@esbuild/linux-arm" "0.19.5" - "@esbuild/linux-arm64" "0.19.5" - "@esbuild/linux-ia32" "0.19.5" - "@esbuild/linux-loong64" "0.19.5" - "@esbuild/linux-mips64el" "0.19.5" - "@esbuild/linux-ppc64" "0.19.5" - "@esbuild/linux-riscv64" "0.19.5" - "@esbuild/linux-s390x" "0.19.5" - "@esbuild/linux-x64" "0.19.5" - "@esbuild/netbsd-x64" "0.19.5" - "@esbuild/openbsd-x64" "0.19.5" - "@esbuild/sunos-x64" "0.19.5" - "@esbuild/win32-arm64" "0.19.5" - "@esbuild/win32-ia32" "0.19.5" - "@esbuild/win32-x64" "0.19.5" + "@esbuild/aix-ppc64" "0.20.2" + "@esbuild/android-arm" "0.20.2" + "@esbuild/android-arm64" "0.20.2" + "@esbuild/android-x64" "0.20.2" + "@esbuild/darwin-arm64" "0.20.2" + "@esbuild/darwin-x64" "0.20.2" + "@esbuild/freebsd-arm64" "0.20.2" + "@esbuild/freebsd-x64" "0.20.2" + "@esbuild/linux-arm" "0.20.2" + "@esbuild/linux-arm64" "0.20.2" + "@esbuild/linux-ia32" "0.20.2" + "@esbuild/linux-loong64" "0.20.2" + "@esbuild/linux-mips64el" "0.20.2" + "@esbuild/linux-ppc64" "0.20.2" + "@esbuild/linux-riscv64" "0.20.2" + "@esbuild/linux-s390x" "0.20.2" + "@esbuild/linux-x64" "0.20.2" + "@esbuild/netbsd-x64" "0.20.2" + "@esbuild/openbsd-x64" "0.20.2" + "@esbuild/sunos-x64" "0.20.2" + "@esbuild/win32-arm64" "0.20.2" + "@esbuild/win32-ia32" "0.20.2" + "@esbuild/win32-x64" "0.20.2" escape-string-regexp@^1.0.5: version "1.0.5" @@ -1146,16 +1138,16 @@ escodegen@^2.0.0: optionalDependencies: source-map "~0.6.1" -eslint-plugin-vue@^9.21.1: - version "9.21.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-9.21.1.tgz#da5629efa48527cec98278dca0daa90fada4caf7" - integrity sha512-XVtI7z39yOVBFJyi8Ljbn7kY9yHzznKXL02qQYn+ta63Iy4A9JFBw6o4OSB9hyD2++tVT+su9kQqetUyCCwhjw== +eslint-plugin-vue@^9.23.0: + version "9.23.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-9.23.0.tgz#1354a33b0cd21e0cb373557ff73c5d7a6698fbcd" + integrity sha512-Bqd/b7hGYGrlV+wP/g77tjyFmp81lh5TMw0be9093X02SyelxRRfCI6/IsGq/J7Um0YwB9s0Ry0wlFyjPdmtUw== dependencies: "@eslint-community/eslint-utils" "^4.4.0" natural-compare "^1.4.0" nth-check "^2.1.1" - postcss-selector-parser "^6.0.13" - semver "^7.5.4" + postcss-selector-parser "^6.0.15" + semver "^7.6.0" vue-eslint-parser "^9.4.2" xml-name-validator "^4.0.0" @@ -1190,16 +1182,16 @@ eslint-visitor-keys@^3.4.1: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994" integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA== -eslint@^8.56.0: - version "8.56.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.56.0.tgz#4957ce8da409dc0809f99ab07a1b94832ab74b15" - integrity sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ== +eslint@^8.57.0: + version "8.57.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668" + integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.6.1" "@eslint/eslintrc" "^2.1.4" - "@eslint/js" "8.56.0" - "@humanwhocodes/config-array" "^0.11.13" + "@eslint/js" "8.57.0" + "@humanwhocodes/config-array" "^0.11.14" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" "@ungap/structured-clone" "^1.2.0" @@ -1834,7 +1826,7 @@ magic-string@^0.30.0: dependencies: "@jridgewell/sourcemap-codec" "^1.4.13" -magic-string@^0.30.6: +magic-string@^0.30.7: version "0.30.7" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.7.tgz#0cecd0527d473298679da95a2d7aeb8c64048505" integrity sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA== @@ -1859,6 +1851,13 @@ micromatch@^4.0.4: braces "^3.0.2" picomatch "^2.3.1" +minimatch@9.0.3, minimatch@^9.0.3: + version "9.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" + integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== + dependencies: + brace-expansion "^2.0.1" + minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" @@ -1866,13 +1865,6 @@ minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimatch@^9.0.3: - version "9.0.3" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" - integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== - dependencies: - brace-expansion "^2.0.1" - mitt@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1" @@ -1893,10 +1885,10 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -muggle-string@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/muggle-string/-/muggle-string-0.3.1.tgz#e524312eb1728c63dd0b2ac49e3282e6ed85963a" - integrity sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg== +muggle-string@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/muggle-string/-/muggle-string-0.4.1.tgz#3b366bd43b32f809dc20659534dd30e7c8a0d328" + integrity sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ== nanoid@^3.3.4: version "3.3.4" @@ -2111,10 +2103,10 @@ pkg-types@^1.0.3: mlly "^1.2.0" pathe "^1.1.0" -postcss-selector-parser@^6.0.13: - version "6.0.13" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz#d05d8d76b1e8e173257ef9d60b706a8e5e99bf1b" - integrity sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ== +postcss-selector-parser@^6.0.15: + version "6.0.15" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz#11cc2b21eebc0b99ea374ffb9887174855a01535" + integrity sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw== dependencies: cssesc "^3.0.0" util-deprecate "^1.0.2" @@ -2128,15 +2120,6 @@ postcss@^8.1.10: picocolors "^1.0.0" source-map-js "^1.0.2" -postcss@^8.4.33: - version "8.4.33" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.33.tgz#1378e859c9f69bf6f638b990a0212f43e2aaa742" - integrity sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg== - dependencies: - nanoid "^3.3.7" - picocolors "^1.0.0" - source-map-js "^1.0.2" - postcss@^8.4.35: version "8.4.35" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.35.tgz#60997775689ce09011edf083a549cea44aabe2f7" @@ -2146,6 +2129,15 @@ postcss@^8.4.35: picocolors "^1.0.0" source-map-js "^1.0.2" +postcss@^8.4.36: + version "8.4.38" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" + integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== + dependencies: + nanoid "^3.3.7" + picocolors "^1.0.0" + source-map-js "^1.2.0" + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" @@ -2222,23 +2214,26 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" -rollup@^4.2.0: - version "4.4.1" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.4.1.tgz#2f85169f23d13dabb3d9b846d753965757353820" - integrity sha512-idZzrUpWSblPJX66i+GzrpjKE3vbYrlWirUHteoAbjKReZwa0cohAErOYA5efoMmNCdvG9yrJS+w9Kl6csaH4w== +rollup@^4.13.0: + version "4.13.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.13.0.tgz#dd2ae144b4cdc2ea25420477f68d4937a721237a" + integrity sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg== + dependencies: + "@types/estree" "1.0.5" optionalDependencies: - "@rollup/rollup-android-arm-eabi" "4.4.1" - "@rollup/rollup-android-arm64" "4.4.1" - "@rollup/rollup-darwin-arm64" "4.4.1" - "@rollup/rollup-darwin-x64" "4.4.1" - "@rollup/rollup-linux-arm-gnueabihf" "4.4.1" - "@rollup/rollup-linux-arm64-gnu" "4.4.1" - "@rollup/rollup-linux-arm64-musl" "4.4.1" - "@rollup/rollup-linux-x64-gnu" "4.4.1" - "@rollup/rollup-linux-x64-musl" "4.4.1" - "@rollup/rollup-win32-arm64-msvc" "4.4.1" - "@rollup/rollup-win32-ia32-msvc" "4.4.1" - "@rollup/rollup-win32-x64-msvc" "4.4.1" + "@rollup/rollup-android-arm-eabi" "4.13.0" + "@rollup/rollup-android-arm64" "4.13.0" + "@rollup/rollup-darwin-arm64" "4.13.0" + "@rollup/rollup-darwin-x64" "4.13.0" + "@rollup/rollup-linux-arm-gnueabihf" "4.13.0" + "@rollup/rollup-linux-arm64-gnu" "4.13.0" + "@rollup/rollup-linux-arm64-musl" "4.13.0" + "@rollup/rollup-linux-riscv64-gnu" "4.13.0" + "@rollup/rollup-linux-x64-gnu" "4.13.0" + "@rollup/rollup-linux-x64-musl" "4.13.0" + "@rollup/rollup-win32-arm64-msvc" "4.13.0" + "@rollup/rollup-win32-ia32-msvc" "4.13.0" + "@rollup/rollup-win32-x64-msvc" "4.13.0" fsevents "~2.3.2" run-parallel@^1.1.9: @@ -2257,10 +2252,10 @@ safe-regex-test@^1.0.0: get-intrinsic "^1.1.3" is-regex "^1.1.4" -sass@^1.71.0: - version "1.71.0" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.71.0.tgz#b3085759b9b2ab503a977aecb7e91153bf941117" - integrity sha512-HKKIKf49Vkxlrav3F/w6qRuPcmImGVbIXJ2I3Kg0VMA+3Bav+8yE9G5XmP5lMj6nl4OlqbPftGAscNaNu28b8w== +sass@^1.72.0: + version "1.72.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.72.0.tgz#5b9978943fcfb32b25a6a5acb102fc9dabbbf41c" + integrity sha512-Gpczt3WA56Ly0Mn8Sl21Vj94s1axi9hDIzDFn9Ph9x3C3p4nNyvsqJoQyVXKou6cBlfFWEgRW4rT8Tb4i3XnVA== dependencies: chokidar ">=3.0.0 <4.0.0" immutable "^4.0.0" @@ -2285,6 +2280,13 @@ semver@^7.3.6: dependencies: lru-cache "^6.0.0" +semver@^7.6.0: + version "7.6.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" + integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== + dependencies: + lru-cache "^6.0.0" + shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" @@ -2338,6 +2340,11 @@ sortablejs@^1.15.2: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== +source-map-js@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" + integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== + source-map-support@~0.5.20: version "0.5.21" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" @@ -2450,10 +2457,10 @@ 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.27.1: - version "5.27.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.27.1.tgz#b0092975ea1b379d166088a1a57e32f0839d84a2" - integrity sha512-29wAr6UU/oQpnTw5HoadwjUZnFQXGdOfj0LjZ4sVxzqwHh/QVkvr7m8y9WoR4iN3FRitVduTc6KdjcW38Npsug== +terser@^5.29.2: + version "5.29.2" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.29.2.tgz#c17d573ce1da1b30f21a877bffd5655dd86fdb35" + integrity sha512-ZiGkhUBIM+7LwkNjXYJq8svgkd+QK3UUr0wJqY4MieaezBSAIPgbSPZyIx0idM6XWK5CMzSWa8MJIzmRcB8Caw== dependencies: "@jridgewell/source-map" "^0.3.3" acorn "^8.8.2" @@ -2496,10 +2503,10 @@ type-fest@^0.20.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== -typescript@^5.3.3: - version "5.3.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" - integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== +typescript@^5.4.3: + version "5.4.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.3.tgz#5c6fedd4c87bee01cd7a528a30145521f8e0feff" + integrity sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg== ufo@^1.1.2: version "1.1.2" @@ -2565,19 +2572,19 @@ vite-plugin-compression@^0.5.1: debug "^4.3.3" fs-extra "^10.0.0" -vite-plugin-css-injected-by-js@^3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/vite-plugin-css-injected-by-js/-/vite-plugin-css-injected-by-js-3.4.0.tgz#b09a571ab50744623736a4b056ecc85d7516311a" - integrity sha512-wS5+UYtJXQ/vNornsqTQxOLBVO/UjXU54ZsYMeX0mj2OrbStMQ4GLgvneVDQGPwyGJcm/ntBPawc2lA7xx+Lpg== +vite-plugin-css-injected-by-js@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/vite-plugin-css-injected-by-js/-/vite-plugin-css-injected-by-js-3.5.0.tgz#784c0f42c2b42155eb4c726c6addfa24aba9f4fb" + integrity sha512-d0QaHH9kS93J25SwRqJNEfE29PSuQS5jn51y9N9i2Yoq0FRO7rjuTeLvjM5zwklZlRrIn6SUdtOEDKyHokgJZg== -vite@^5.1.3: - version "5.1.3" - resolved "https://registry.yarnpkg.com/vite/-/vite-5.1.3.tgz#dd072653a80225702265550a4700561740dfde55" - integrity sha512-UfmUD36DKkqhi/F75RrxvPpry+9+tTkrXfMNZD+SboZqBCMsxKtO52XeGzzuh7ioz+Eo/SYDBbdb0Z7vgcDJew== +vite@^5.2.3: + version "5.2.3" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.3.tgz#198efc2fd4d80eac813b146a68a4b0dbde884fc2" + integrity sha512-+i1oagbvkVIhEy9TnEV+fgXsng13nZM90JQbrcPrf6DvW2mXARlz+DK7DLiDP+qeKoD1FCVx/1SpFL1CLq9Mhw== dependencies: - esbuild "^0.19.3" - postcss "^8.4.35" - rollup "^4.2.0" + esbuild "^0.20.1" + postcss "^8.4.36" + rollup "^4.13.0" optionalDependencies: fsevents "~2.3.3" @@ -2607,21 +2614,21 @@ vue-eslint-parser@^9.4.2: lodash "^4.17.21" semver "^7.3.6" -vue-i18n@^9.9.1: - version "9.9.1" - resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-9.9.1.tgz#3c2fdf3c9db430572a1246439d541d01e2795c06" - integrity sha512-xyQ4VspLdNSPTKBFBPWa1tvtj+9HuockZwgFeD2OhxxXuC2CWeNvV4seu2o9+vbQOyQbhAM5Ez56oxUrrnTWdw== +vue-i18n@^9.10.2: + version "9.10.2" + resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-9.10.2.tgz#6f4b5d76bce649f1e18bb9b7767b72962b3e30a3" + integrity sha512-ECJ8RIFd+3c1d3m1pctQ6ywG5Yj8Efy1oYoAKQ9neRdkLbuKLVeW4gaY5HPkD/9ssf1pOnUrmIFjx2/gkGxmEw== dependencies: - "@intlify/core-base" "9.9.1" - "@intlify/shared" "9.9.1" + "@intlify/core-base" "9.10.2" + "@intlify/shared" "9.10.2" "@vue/devtools-api" "^6.5.0" -vue-router@^4.2.5: - version "4.2.5" - resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.2.5.tgz#b9e3e08f1bd9ea363fdd173032620bc50cf0e98a" - integrity sha512-DIUpKcyg4+PTQKfFPX88UWhlagBEBEfJ5A8XDXRJLUnZOvcpMF8o/dnL90vpVkGaPbjvXazV/rC1qBKrZlFugw== +vue-router@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.3.0.tgz#d5913f27bf68a0a178ee798c3c88be471811a235" + integrity sha512-dqUcs8tUeG+ssgWhcPbjHvazML16Oga5w34uCUmsk7i0BcnskoLGwjpa15fqMr2Fa5JgVBrdL2MEgqz6XZ/6IQ== dependencies: - "@vue/devtools-api" "^6.5.0" + "@vue/devtools-api" "^6.5.1" vue-template-compiler@^2.7.14: version "2.7.14" @@ -2631,25 +2638,25 @@ vue-template-compiler@^2.7.14: de-indent "^1.0.2" he "^1.2.0" -vue-tsc@^1.8.27: - version "1.8.27" - resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-1.8.27.tgz#feb2bb1eef9be28017bb9e95e2bbd1ebdd48481c" - integrity sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg== +vue-tsc@^2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/vue-tsc/-/vue-tsc-2.0.7.tgz#3177a2fe720bfa7355d3717929ee8c8d132bc5d0" + integrity sha512-LYa0nInkfcDBB7y8jQ9FQ4riJTRNTdh98zK/hzt4gEpBZQmf30dPhP+odzCa+cedGz6B/guvJEd0BavZaRptjg== dependencies: - "@volar/typescript" "~1.11.1" - "@vue/language-core" "1.8.27" + "@volar/typescript" "~2.1.3" + "@vue/language-core" "2.0.7" semver "^7.5.4" -vue@^3.4.19: - version "3.4.19" - resolved "https://registry.yarnpkg.com/vue/-/vue-3.4.19.tgz#f9ae0a44db86628548736ff04152830726a97263" - integrity sha512-W/7Fc9KUkajFU8dBeDluM4sRGc/aa4YJnOYck8dkjgZoXtVsn3OeTGni66FV1l3+nvPA7VBFYtPioaGKUmEADw== +vue@^3.4.21: + version "3.4.21" + resolved "https://registry.yarnpkg.com/vue/-/vue-3.4.21.tgz#69ec30e267d358ee3a0ce16612ba89e00aaeb731" + integrity sha512-5hjyV/jLEIKD/jYl4cavMcnzKwjMKohureP8ejn3hhEjwhWIhWeuzL2kJAjzl/WyVsgPY56Sy4Z40C3lVshxXA== dependencies: - "@vue/compiler-dom" "3.4.19" - "@vue/compiler-sfc" "3.4.19" - "@vue/runtime-dom" "3.4.19" - "@vue/server-renderer" "3.4.19" - "@vue/shared" "3.4.19" + "@vue/compiler-dom" "3.4.21" + "@vue/compiler-sfc" "3.4.21" + "@vue/runtime-dom" "3.4.21" + "@vue/server-renderer" "3.4.21" + "@vue/shared" "3.4.21" webpack-sources@^3.2.3: version "3.2.3" diff --git a/webapp_dist/index.html.gz b/webapp_dist/index.html.gz index be84fc7db..3de27210e 100644 Binary files a/webapp_dist/index.html.gz and b/webapp_dist/index.html.gz differ diff --git a/webapp_dist/js/app.js.gz b/webapp_dist/js/app.js.gz index 65b77d681..995da1ff2 100644 Binary files a/webapp_dist/js/app.js.gz and b/webapp_dist/js/app.js.gz differ diff --git a/webapp_dist/zones.json.gz b/webapp_dist/zones.json.gz index 02f82db68..890a1279e 100644 Binary files a/webapp_dist/zones.json.gz and b/webapp_dist/zones.json.gz differ