diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dd88f54c5..83f9bab23 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,14 +23,14 @@ jobs: - uses: actions/checkout@v4 - name: Cache pip - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} restore-keys: | ${{ runner.os }}-pip- - - uses: actions/setup-python@v4 + - uses: actions/setup-python@v5 with: python-version: "3.x" @@ -60,8 +60,17 @@ jobs: - name: Get tags run: git fetch --force --tags origin + - name: Create and switch to a meaningful branch for pull-requests + if: github.event_name == 'pull_request' + run: | + OWNER=${{ github.repository_owner }} + NAME=${{ github.event.repository.name }} + ID=${{ github.event.pull_request.number }} + DATE=$(date +'%Y%m%d%H%M') + git switch -c ${OWNER}/${NAME}/pr${ID}-${DATE} + - name: Cache pip - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} @@ -69,13 +78,13 @@ jobs: ${{ runner.os }}-pip- - name: Cache PlatformIO - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.platformio key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.x" diff --git a/.github/workflows/cpplint.yml b/.github/workflows/cpplint.yml index af5e8b79f..4ee4b4a82 100644 --- a/.github/workflows/cpplint.yml +++ b/.github/workflows/cpplint.yml @@ -9,7 +9,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.x" - name: Install dependencies diff --git a/docs/DeviceProfiles/AhoyDTU-ESP32.json b/docs/DeviceProfiles/AhoyDTU-ESP32.json new file mode 100644 index 000000000..5de694463 --- /dev/null +++ b/docs/DeviceProfiles/AhoyDTU-ESP32.json @@ -0,0 +1,76 @@ +[ + { + "name": "AhoyDTU ESP32 Display LED", + "links": [ + {"name": "Information", "url": "https://ahoydtu.de/getting_started/"} + ], + "nrf24": { + "miso": 19, + "mosi": 23, + "clk": 18, + "irq": 16, + "en": 4, + "cs": 5 + }, + "led": { + "led0": 25, + "led1": 26 + }, + "display": { + "type": 2, + "data": 21, + "clk": 22 + } + }, + { + "name": "AhoyDTU ESP32 Display", + "links": [ + {"name": "Information", "url": "https://ahoydtu.de/getting_started/"} + ], + "nrf24": { + "miso": 19, + "mosi": 23, + "clk": 18, + "irq": 16, + "en": 4, + "cs": 5 + }, + "display": { + "type": 2, + "data": 21, + "clk": 22 + } + }, + { + "name": "AhoyDTU ESP32 LED", + "links": [ + {"name": "Information", "url": "https://ahoydtu.de/getting_started/"} + ], + "nrf24": { + "miso": 19, + "mosi": 23, + "clk": 18, + "irq": 16, + "en": 4, + "cs": 5 + }, + "led": { + "led0": 25, + "led1": 26 + } + }, + { + "name": "AhoyDTU ESP32", + "links": [ + {"name": "Information", "url": "https://ahoydtu.de/getting_started/"} + ], + "nrf24": { + "miso": 19, + "mosi": 23, + "clk": 18, + "irq": 16, + "en": 4, + "cs": 5 + } + } +] \ No newline at end of file diff --git a/docs/DeviceProfiles/liligo_t-eth-lite_poe.json b/docs/DeviceProfiles/liligo_t-eth-lite_poe.json new file mode 100644 index 000000000..be91f95ba --- /dev/null +++ b/docs/DeviceProfiles/liligo_t-eth-lite_poe.json @@ -0,0 +1,74 @@ +[ + { + "name": "LILYGO T-ETH-Lite-POE CMT", + "links": [ + {"name": "Datasheet", "url": "https://www.lilygo.cc/products/t-eth-lite"} + ], + "eth": { + "enabled": true, + "phy_addr": 0, + "power": 12, + "mdc": 23, + "mdio": 18, + "type": 2, + "clk_mode": 0 + }, + "cmt": { + "clk": 15, + "cs": 32, + "fcs": 33, + "sdio": 4 + } + }, + { + "name": "LILYGO T-ETH-Lite-POE NRF24", + "links": [ + {"name": "Datasheet", "url": "https://www.lilygo.cc/products/t-eth-lite"} + ], + "eth": { + "enabled": true, + "phy_addr": 0, + "power": 12, + "mdc": 23, + "mdio": 18, + "type": 2, + "clk_mode": 0 + }, + "nrf24": { + "miso": 34, + "mosi": 13, + "clk": 14, + "irq": 35, + "en": 4, + "cs": 2 + } + }, + { + "name": "LILYGO T-ETH-Lite-POE NRF24 + Display", + "links": [ + {"name": "Datasheet", "url": "https://www.lilygo.cc/products/t-eth-lite"} + ], + "eth": { + "enabled": true, + "phy_addr": 0, + "power": 12, + "mdc": 23, + "mdio": 18, + "type": 2, + "clk_mode": 0 + }, + "nrf24": { + "miso": 34, + "mosi": 13, + "clk": 14, + "irq": 35, + "en": 4, + "cs": 2 + }, + "display": { + "type": 3, + "data": 32, + "clk": 33 + } + } +] diff --git a/include/BatteryStats.h b/include/BatteryStats.h index 36eed06a3..c11f45f95 100644 --- a/include/BatteryStats.h +++ b/include/BatteryStats.h @@ -15,11 +15,14 @@ class BatteryStats { // the last time *any* datum was updated uint32_t getAgeSeconds() const { return (millis() - _lastUpdate) / 1000; } - bool updateAvailable(uint32_t since) const { return _lastUpdate > since; } + bool updateAvailable(uint32_t since) const; - uint8_t getSoC() const { return _SoC; } + uint8_t getSoC() const { return _soc; } uint32_t getSoCAgeSeconds() const { return (millis() - _lastUpdateSoC) / 1000; } + float getVoltage() const { return _voltage; } + uint32_t getVoltageAgeSeconds() const { return (millis() - _lastUpdateVoltage) / 1000; } + // convert stats to JSON for web application live view virtual void getLiveViewData(JsonVariant& root) const; @@ -29,18 +32,33 @@ class BatteryStats { // if they did not change. used to calculate Home Assistent expiration. virtual uint32_t getMqttFullPublishIntervalMs() const; - bool isValid() const { return _lastUpdateSoC > 0 && _lastUpdate > 0; } + bool isSoCValid() const { return _lastUpdateSoC > 0; } + bool isVoltageValid() const { return _lastUpdateVoltage > 0; } protected: virtual void mqttPublish() const; + void setSoC(float soc, uint8_t precision, uint32_t timestamp) { + _soc = soc; + _socPrecision = precision; + _lastUpdateSoC = timestamp; + } + + void setVoltage(float voltage, uint32_t timestamp) { + _voltage = voltage; + _lastUpdateVoltage = timestamp; + } + String _manufacturer = "unknown"; - uint8_t _SoC = 0; - uint32_t _lastUpdateSoC = 0; uint32_t _lastUpdate = 0; private: uint32_t _lastMqttPublish = 0; + float _soc = 0; + uint8_t _socPrecision = 0; // decimal places + uint32_t _lastUpdateSoC = 0; + float _voltage = 0; // total battery pack voltage + uint32_t _lastUpdateVoltage = 0; }; class PylontechBatteryStats : public BatteryStats { @@ -52,14 +70,12 @@ class PylontechBatteryStats : public BatteryStats { private: void setManufacturer(String&& m) { _manufacturer = std::move(m); } - void setSoC(uint8_t SoC) { _SoC = SoC; _lastUpdateSoC = millis(); } void setLastUpdate(uint32_t ts) { _lastUpdate = ts; } float _chargeVoltage; float _chargeCurrentLimitation; float _dischargeCurrentLimitation; uint16_t _stateOfHealth; - float _voltage; // total voltage of the battery pack // total current into (positive) or from (negative) // the battery, i.e., the charging current float _current; @@ -123,7 +139,6 @@ class VictronSmartShuntStats : public BatteryStats { void updateFrom(VeDirectShuntController::veShuntStruct const& shuntData); private: - float _voltage; float _current; float _temperature; bool _tempPresent; @@ -141,14 +156,14 @@ class VictronSmartShuntStats : public BatteryStats { }; class MqttBatteryStats : public BatteryStats { + friend class MqttBattery; + public: // since the source of information was MQTT in the first place, // we do NOT publish the same data under a different topic. void mqttPublish() const final { } - // the SoC is the only interesting value in this case, which is already - // displayed at the top of the live view. do not generate a card. + // if the voltage is subscribed to at all, it alone does not warrant a + // card in the live view, since the SoC is already displayed at the top void getLiveViewData(JsonVariant& root) const final { } - - void setSoC(uint8_t SoC) { _SoC = SoC; _lastUpdateSoC = _lastUpdate = millis(); } }; diff --git a/include/Configuration.h b/include/Configuration.h index cc3456434..14a056670 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -207,12 +207,14 @@ struct CONFIG_T { uint8_t BatteryDrainStategy; uint32_t Interval; bool IsInverterBehindPowerMeter; + bool IsInverterSolarPowered; uint8_t InverterId; uint8_t InverterChannelId; int32_t TargetPowerConsumption; int32_t TargetPowerConsumptionHysteresis; int32_t LowerPowerLimit; int32_t UpperPowerLimit; + bool IgnoreSoc; uint32_t BatterySocStartThreshold; uint32_t BatterySocStopThreshold; float VoltageStartThreshold; @@ -230,7 +232,8 @@ struct CONFIG_T { uint8_t Provider; uint8_t JkBmsInterface; uint8_t JkBmsPollingInterval; - char MqttTopic[MQTT_MAX_TOPIC_STRLEN + 1]; + char MqttSocTopic[MQTT_MAX_TOPIC_STRLEN + 1]; + char MqttVoltageTopic[MQTT_MAX_TOPIC_STRLEN + 1]; } Battery; struct { diff --git a/include/MqttBattery.h b/include/MqttBattery.h index 83ff412d3..61df04500 100644 --- a/include/MqttBattery.h +++ b/include/MqttBattery.h @@ -1,5 +1,6 @@ #pragma once +#include #include "Battery.h" #include @@ -15,8 +16,12 @@ class MqttBattery : public BatteryProvider { private: bool _verboseLogging = false; String _socTopic; + String _voltageTopic; std::shared_ptr _stats = std::make_shared(); - void onMqttMessage(espMqttClientTypes::MessageProperties const& properties, + 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/PowerLimiter.h b/include/PowerLimiter.h index af6ed28f5..0a8dfab04 100644 --- a/include/PowerLimiter.h +++ b/include/PowerLimiter.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -15,10 +16,6 @@ #define PL_UI_STATE_USE_SOLAR_ONLY 2 #define PL_UI_STATE_USE_SOLAR_AND_BATTERY 3 -#define PL_MODE_ENABLE_NORMAL_OP 0 -#define PL_MODE_FULL_DISABLE 1 -#define PL_MODE_SOLAR_PT_ONLY 2 - typedef enum { EMPTY_WHEN_FULL= 0, EMPTY_AT_NIGHT @@ -43,15 +40,18 @@ class PowerLimiterClass { InverterPowerCmdPending, InverterDevInfoPending, InverterStatsPending, + CalculatedLimitBelowMinLimit, UnconditionalSolarPassthrough, NoVeDirect, + NoEnergy, + HuaweiPsu, Settling, Stable, }; void init(Scheduler& scheduler); uint8_t getPowerLimiterState(); - int32_t getLastRequestedPowerLimit(); + int32_t getLastRequestedPowerLimit() { return _lastRequestedPowerLimit; } enum class Mode : unsigned { Normal = 0, @@ -69,8 +69,10 @@ class PowerLimiterClass { Task _loopTask; int32_t _lastRequestedPowerLimit = 0; - uint32_t _lastPowerLimitMillis = 0; - uint32_t _shutdownTimeout = 0; + bool _shutdownPending = false; + std::optional _oUpdateStartMillis = std::nullopt; + std::optional _oTargetPowerLimitWatts = std::nullopt; + std::optional _oTargetPowerState = std::nullopt; Status _lastStatus = Status::Initializing; uint32_t _lastStatusPrinted = 0; uint32_t _lastCalculation = 0; @@ -88,13 +90,14 @@ class PowerLimiterClass { void announceStatus(Status status); bool shutdown(Status status); bool shutdown() { return shutdown(_lastStatus); } + float getBatteryVoltage(bool log = false); 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); - void commitPowerLimit(std::shared_ptr inverter, int32_t limit, bool enablePowerProduction); + 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/Utils.h b/include/Utils.h index 4d4bfee37..fddc2ab97 100644 --- a/include/Utils.h +++ b/include/Utils.h @@ -11,4 +11,5 @@ class Utils { static int getTimezoneOffset(); static void restartDtu(); static bool checkJsonAlloc(const DynamicJsonDocument& doc, const char* function, const uint16_t line); + static void removeAllFiles(); }; diff --git a/include/VictronMppt.h b/include/VictronMppt.h index 091ddb005..12d6bdf75 100644 --- a/include/VictronMppt.h +++ b/include/VictronMppt.h @@ -35,6 +35,9 @@ class VictronMpptClass { // sum of today's yield of all MPPT charge controllers in kWh double getYieldDay() const; + // minimum of all MPPT charge controllers' output voltages in V + double getOutputVoltage() const; + private: void loop(); VictronMpptClass(VictronMpptClass const& other) = delete; diff --git a/include/WebApi_config.h b/include/WebApi_config.h index 91243c18c..f29dc8fcf 100644 --- a/include/WebApi_config.h +++ b/include/WebApi_config.h @@ -14,6 +14,4 @@ class WebApiConfigClass { void onConfigListGet(AsyncWebServerRequest* request); void onConfigUploadFinish(AsyncWebServerRequest* request); void onConfigUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final); - - AsyncWebServer* _server; }; diff --git a/include/WebApi_device.h b/include/WebApi_device.h index f74315e34..48976bce6 100644 --- a/include/WebApi_device.h +++ b/include/WebApi_device.h @@ -11,6 +11,4 @@ class WebApiDeviceClass { private: void onDeviceAdminGet(AsyncWebServerRequest* request); void onDeviceAdminPost(AsyncWebServerRequest* request); - - AsyncWebServer* _server; }; diff --git a/include/WebApi_devinfo.h b/include/WebApi_devinfo.h index d1924ecc0..e312ecdf0 100644 --- a/include/WebApi_devinfo.h +++ b/include/WebApi_devinfo.h @@ -10,6 +10,4 @@ class WebApiDevInfoClass { private: void onDevInfoStatus(AsyncWebServerRequest* request); - - AsyncWebServer* _server; }; diff --git a/include/WebApi_dtu.h b/include/WebApi_dtu.h index 3acf4494d..20f5274e6 100644 --- a/include/WebApi_dtu.h +++ b/include/WebApi_dtu.h @@ -13,8 +13,6 @@ class WebApiDtuClass { void onDtuAdminGet(AsyncWebServerRequest* request); void onDtuAdminPost(AsyncWebServerRequest* request); - AsyncWebServer* _server; - Task _applyDataTask; void applyDataTaskCb(); }; diff --git a/include/WebApi_eventlog.h b/include/WebApi_eventlog.h index 3cba7e5e5..e7fe9874a 100644 --- a/include/WebApi_eventlog.h +++ b/include/WebApi_eventlog.h @@ -10,6 +10,4 @@ class WebApiEventlogClass { private: void onEventlogStatus(AsyncWebServerRequest* request); - - AsyncWebServer* _server; }; diff --git a/include/WebApi_firmware.h b/include/WebApi_firmware.h index fd9a96428..990a5e064 100644 --- a/include/WebApi_firmware.h +++ b/include/WebApi_firmware.h @@ -11,6 +11,4 @@ class WebApiFirmwareClass { private: void onFirmwareUpdateFinish(AsyncWebServerRequest* request); void onFirmwareUpdateUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final); - - AsyncWebServer* _server; }; diff --git a/include/WebApi_gridprofile.h b/include/WebApi_gridprofile.h index 73c156540..cff4ddb86 100644 --- a/include/WebApi_gridprofile.h +++ b/include/WebApi_gridprofile.h @@ -11,6 +11,4 @@ class WebApiGridProfileClass { private: void onGridProfileStatus(AsyncWebServerRequest* request); void onGridProfileRawdata(AsyncWebServerRequest* request); - - AsyncWebServer* _server; }; diff --git a/include/WebApi_inverter.h b/include/WebApi_inverter.h index b8f054256..c316622e5 100644 --- a/include/WebApi_inverter.h +++ b/include/WebApi_inverter.h @@ -14,6 +14,4 @@ class WebApiInverterClass { void onInverterEdit(AsyncWebServerRequest* request); void onInverterDelete(AsyncWebServerRequest* request); void onInverterOrder(AsyncWebServerRequest* request); - - AsyncWebServer* _server; }; diff --git a/include/WebApi_limit.h b/include/WebApi_limit.h index 84d48d3a3..285be27cc 100644 --- a/include/WebApi_limit.h +++ b/include/WebApi_limit.h @@ -11,6 +11,4 @@ class WebApiLimitClass { private: void onLimitStatus(AsyncWebServerRequest* request); void onLimitPost(AsyncWebServerRequest* request); - - AsyncWebServer* _server; }; diff --git a/include/WebApi_maintenance.h b/include/WebApi_maintenance.h index 02dc4702f..5a00bbab8 100644 --- a/include/WebApi_maintenance.h +++ b/include/WebApi_maintenance.h @@ -10,6 +10,4 @@ class WebApiMaintenanceClass { private: void onRebootPost(AsyncWebServerRequest* request); - - AsyncWebServer* _server; }; diff --git a/include/WebApi_mqtt.h b/include/WebApi_mqtt.h index 6ec971c93..b259752b1 100644 --- a/include/WebApi_mqtt.h +++ b/include/WebApi_mqtt.h @@ -15,6 +15,4 @@ class WebApiMqttClass { void onMqttAdminGet(AsyncWebServerRequest* request); void onMqttAdminPost(AsyncWebServerRequest* request); String getTlsCertInfo(const char* cert); - - AsyncWebServer* _server; }; diff --git a/include/WebApi_network.h b/include/WebApi_network.h index 7587bbbd3..179fa4920 100644 --- a/include/WebApi_network.h +++ b/include/WebApi_network.h @@ -12,6 +12,4 @@ class WebApiNetworkClass { void onNetworkStatus(AsyncWebServerRequest* request); void onNetworkAdminGet(AsyncWebServerRequest* request); void onNetworkAdminPost(AsyncWebServerRequest* request); - - AsyncWebServer* _server; }; diff --git a/include/WebApi_ntp.h b/include/WebApi_ntp.h index 75e02c549..5ce040ede 100644 --- a/include/WebApi_ntp.h +++ b/include/WebApi_ntp.h @@ -14,6 +14,4 @@ class WebApiNtpClass { void onNtpAdminPost(AsyncWebServerRequest* request); void onNtpTimeGet(AsyncWebServerRequest* request); void onNtpTimePost(AsyncWebServerRequest* request); - - AsyncWebServer* _server; }; diff --git a/include/WebApi_power.h b/include/WebApi_power.h index 7d186eb4d..aed11b0ef 100644 --- a/include/WebApi_power.h +++ b/include/WebApi_power.h @@ -11,6 +11,4 @@ class WebApiPowerClass { private: void onPowerStatus(AsyncWebServerRequest* request); void onPowerPost(AsyncWebServerRequest* request); - - AsyncWebServer* _server; }; diff --git a/include/WebApi_prometheus.h b/include/WebApi_prometheus.h index 08e2221df..b3ee6a18c 100644 --- a/include/WebApi_prometheus.h +++ b/include/WebApi_prometheus.h @@ -17,8 +17,6 @@ class WebApiPrometheusClass { void addPanelInfo(AsyncResponseStream* stream, const String& serial, const uint8_t idx, std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel); - AsyncWebServer* _server; - enum MetricType_t { NONE = 0, GAUGE, diff --git a/include/WebApi_security.h b/include/WebApi_security.h index b5981e3d4..ac76522a5 100644 --- a/include/WebApi_security.h +++ b/include/WebApi_security.h @@ -13,6 +13,4 @@ class WebApiSecurityClass { void onSecurityPost(AsyncWebServerRequest* request); void onAuthenticateGet(AsyncWebServerRequest* request); - - AsyncWebServer* _server; }; diff --git a/include/WebApi_sysstatus.h b/include/WebApi_sysstatus.h index 32bdc7e3e..c754ac0df 100644 --- a/include/WebApi_sysstatus.h +++ b/include/WebApi_sysstatus.h @@ -10,6 +10,4 @@ class WebApiSysstatusClass { private: void onSystemStatus(AsyncWebServerRequest* request); - - AsyncWebServer* _server; }; diff --git a/include/WebApi_webapp.h b/include/WebApi_webapp.h index 401408c61..5330fbdf7 100644 --- a/include/WebApi_webapp.h +++ b/include/WebApi_webapp.h @@ -9,5 +9,5 @@ class WebApiWebappClass { void init(AsyncWebServer& server, Scheduler& scheduler); private: - AsyncWebServer* _server; + void responseBinaryDataWithETagCache(AsyncWebServerRequest* request, const String &contentType, const String &contentEncoding, const uint8_t *content, size_t len); }; diff --git a/include/WebApi_ws_console.h b/include/WebApi_ws_console.h index 4289afd0f..cf7beecce 100644 --- a/include/WebApi_ws_console.h +++ b/include/WebApi_ws_console.h @@ -10,7 +10,6 @@ class WebApiWsConsoleClass { void init(AsyncWebServer& server, Scheduler& scheduler); private: - AsyncWebServer* _server; AsyncWebSocket _ws; Task _wsCleanupTask; diff --git a/include/WebApi_ws_live.h b/include/WebApi_ws_live.h index 392ca8696..4a29fff5b 100644 --- a/include/WebApi_ws_live.h +++ b/include/WebApi_ws_live.h @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once +#include "Configuration.h" #include #include #include @@ -12,17 +13,28 @@ class WebApiWsLiveClass { void init(AsyncWebServer& server, Scheduler& scheduler); private: - void generateJsonResponse(JsonVariant& root); - void addField(JsonObject& root, std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId, String topic = ""); - void addTotalField(JsonObject& root, const String& name, const float value, const String& unit, const uint8_t digits); + static void generateInverterCommonJsonResponse(JsonObject& root, std::shared_ptr inv); + static void generateInverterChannelJsonResponse(JsonObject& root, std::shared_ptr inv); + static void generateCommonJsonResponse(JsonVariant& root); + + void generateOnBatteryJsonResponse(JsonVariant& root, bool all); + void sendOnBatteryStats(); + + static void addField(JsonObject& root, std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId, String topic = ""); + static void addTotalField(JsonObject& root, const String& name, const float value, const String& unit, const uint8_t digits); + void onLivedataStatus(AsyncWebServerRequest* request); void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len); - AsyncWebServer* _server; AsyncWebSocket _ws; - uint32_t _lastWsPublish = 0; - uint32_t _newestInverterTimestamp = 0; + uint32_t _lastPublishOnBatteryFull = 0; + uint32_t _lastPublishVictron = 0; + uint32_t _lastPublishHuawei = 0; + uint32_t _lastPublishBattery = 0; + uint32_t _lastPublishPowerMeter = 0; + + uint32_t _lastPublishStats[INV_MAX_COUNT] = { 0 }; std::mutex _mutex; diff --git a/include/defaults.h b/include/defaults.h index 7e1d7a0c1..c30cb32e2 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -122,12 +122,14 @@ #define POWERLIMITER_BATTERY_DRAIN_STRATEGY 0 #define POWERLIMITER_INTERVAL 10 #define POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER true +#define POWERLIMITER_IS_INVERTER_SOLAR_POWERED false #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 diff --git a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp index d112fd6f2..035e52f46 100644 --- a/lib/Hoymiles/src/HoymilesRadio_CMT.cpp +++ b/lib/Hoymiles/src/HoymilesRadio_CMT.cpp @@ -239,8 +239,11 @@ CountryModeId_t HoymilesRadio_CMT::getCountryMode() const void HoymilesRadio_CMT::setCountryMode(const CountryModeId_t mode) { - _radio->setFrequencyBand(countryDefinition.at(mode).Band); _countryMode = mode; + if (!_isInitialized) { + return; + } + _radio->setFrequencyBand(countryDefinition.at(mode).Band); } uint32_t HoymilesRadio_CMT::getInvBootFrequency() const diff --git a/lib/Hoymiles/src/inverters/HMS_1CH.cpp b/lib/Hoymiles/src/inverters/HMS_1CH.cpp index 312cf6302..5d906e58f 100644 --- a/lib/Hoymiles/src/inverters/HMS_1CH.cpp +++ b/lib/Hoymiles/src/inverters/HMS_1CH.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2023 Thomas Basler and others + * Copyright (C) 2023-2024 Thomas Basler and others */ #include "HMS_1CH.h" @@ -10,7 +10,7 @@ static const byteAssign_t byteAssignment[] = { { TYPE_DC, CH0, FLD_PDC, UNIT_W, 6, 2, 10, false, 1 }, { TYPE_DC, CH0, FLD_YD, UNIT_WH, 12, 2, 1, false, 0 }, { TYPE_DC, CH0, FLD_YT, UNIT_KWH, 8, 4, 1000, false, 3 }, - { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH0, CMD_CALC, false, 3 }, + { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH0, CMD_CALC, false, 3 }, { TYPE_AC, CH0, FLD_UAC, UNIT_V, 14, 2, 10, false, 1 }, { TYPE_AC, CH0, FLD_IAC, UNIT_A, 22, 2, 100, false, 2 }, @@ -22,10 +22,10 @@ static const byteAssign_t byteAssignment[] = { { TYPE_INV, CH0, FLD_T, UNIT_C, 26, 2, 10, true, 1 }, { TYPE_INV, CH0, FLD_EVT_LOG, UNIT_NONE, 28, 2, 1, false, 0 }, - { TYPE_AC, CH0, FLD_YD, UNIT_WH, CALC_YD_CH0, 0, CMD_CALC, false, 0 }, - { TYPE_AC, CH0, FLD_YT, UNIT_KWH, CALC_YT_CH0, 0, CMD_CALC, false, 3 }, - { TYPE_AC, CH0, FLD_PDC, UNIT_W, CALC_PDC_CH0, 0, CMD_CALC, false, 1 }, - { TYPE_AC, CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } + { 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 } }; HMS_1CH::HMS_1CH(HoymilesRadio* radio, const uint64_t serial) @@ -51,4 +51,4 @@ const byteAssign_t* HMS_1CH::getByteAssignment() const uint8_t HMS_1CH::getByteAssignmentSize() const { return sizeof(byteAssignment) / sizeof(byteAssignment[0]); -} \ No newline at end of file +} diff --git a/lib/Hoymiles/src/inverters/HMS_1CHv2.cpp b/lib/Hoymiles/src/inverters/HMS_1CHv2.cpp index b6a9d93e0..2cfaa28b0 100644 --- a/lib/Hoymiles/src/inverters/HMS_1CHv2.cpp +++ b/lib/Hoymiles/src/inverters/HMS_1CHv2.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2023 Thomas Basler and others + * Copyright (C) 2023-2024 Thomas Basler and others */ #include "HMS_1CHv2.h" @@ -10,7 +10,7 @@ static const byteAssign_t byteAssignment[] = { { 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_IRR_CH, CH0, CMD_CALC, false, 3 }, + { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH0, 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 }, @@ -22,10 +22,10 @@ static const byteAssign_t byteAssignment[] = { { TYPE_INV, CH0, FLD_T, UNIT_C, 38, 2, 10, true, 1 }, { TYPE_INV, CH0, FLD_EVT_LOG, UNIT_NONE, 18, 2, 1, false, 0 }, - { TYPE_AC, CH0, FLD_YD, UNIT_WH, CALC_YD_CH0, 0, CMD_CALC, false, 0 }, - { TYPE_AC, CH0, FLD_YT, UNIT_KWH, CALC_YT_CH0, 0, CMD_CALC, false, 3 }, - { TYPE_AC, CH0, FLD_PDC, UNIT_W, CALC_PDC_CH0, 0, CMD_CALC, false, 1 }, - { TYPE_AC, CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } + { 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 } }; HMS_1CHv2::HMS_1CHv2(HoymilesRadio* radio, const uint64_t serial) @@ -51,4 +51,4 @@ const byteAssign_t* HMS_1CHv2::getByteAssignment() const uint8_t HMS_1CHv2::getByteAssignmentSize() const { return sizeof(byteAssignment) / sizeof(byteAssignment[0]); -} \ No newline at end of file +} diff --git a/lib/Hoymiles/src/inverters/HMS_2CH.cpp b/lib/Hoymiles/src/inverters/HMS_2CH.cpp index d038e7727..56c7fc69b 100644 --- a/lib/Hoymiles/src/inverters/HMS_2CH.cpp +++ b/lib/Hoymiles/src/inverters/HMS_2CH.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2023 Thomas Basler and others + * Copyright (C) 2023-2024 Thomas Basler and others */ #include "HMS_2CH.h" @@ -10,14 +10,14 @@ static const byteAssign_t byteAssignment[] = { { TYPE_DC, CH0, FLD_PDC, UNIT_W, 10, 2, 10, false, 1 }, { TYPE_DC, CH0, FLD_YT, UNIT_KWH, 14, 4, 1000, false, 3 }, { TYPE_DC, CH0, FLD_YD, UNIT_WH, 22, 2, 1, false, 0 }, - { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH0, CMD_CALC, 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_YT, UNIT_KWH, 18, 4, 1000, false, 3 }, { TYPE_DC, CH1, FLD_YD, UNIT_WH, 24, 2, 1, false, 0 }, - { TYPE_DC, CH1, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH1, CMD_CALC, 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 }, @@ -29,10 +29,10 @@ static const byteAssign_t byteAssignment[] = { { 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_AC, CH0, FLD_YD, UNIT_WH, CALC_YD_CH0, 0, CMD_CALC, false, 0 }, - { TYPE_AC, CH0, FLD_YT, UNIT_KWH, CALC_YT_CH0, 0, CMD_CALC, false, 3 }, - { TYPE_AC, CH0, FLD_PDC, UNIT_W, CALC_PDC_CH0, 0, CMD_CALC, false, 1 }, - { TYPE_AC, CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } + { 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 } }; HMS_2CH::HMS_2CH(HoymilesRadio* radio, const uint64_t serial) @@ -58,4 +58,4 @@ const byteAssign_t* HMS_2CH::getByteAssignment() const uint8_t HMS_2CH::getByteAssignmentSize() const { return sizeof(byteAssignment) / sizeof(byteAssignment[0]); -} \ No newline at end of file +} diff --git a/lib/Hoymiles/src/inverters/HMS_4CH.cpp b/lib/Hoymiles/src/inverters/HMS_4CH.cpp index eff44abc5..9aeaf1065 100644 --- a/lib/Hoymiles/src/inverters/HMS_4CH.cpp +++ b/lib/Hoymiles/src/inverters/HMS_4CH.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2023 Thomas Basler and others + * Copyright (C) 2023-2024 Thomas Basler and others */ #include "HMS_4CH.h" @@ -10,28 +10,28 @@ static const byteAssign_t byteAssignment[] = { { 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_IRR_CH, CH0, CMD_CALC, 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_IRR_CH, CH1, CMD_CALC, false, 3 }, + { TYPE_DC, CH1, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH1, CMD_CALC, false, 3 }, { TYPE_DC, CH2, FLD_UDC, UNIT_V, 26, 2, 10, false, 1 }, { TYPE_DC, CH2, FLD_IDC, UNIT_A, 30, 2, 100, false, 2 }, { TYPE_DC, CH2, FLD_PDC, UNIT_W, 34, 2, 10, false, 1 }, { TYPE_DC, CH2, FLD_YD, UNIT_WH, 46, 2, 1, false, 0 }, { TYPE_DC, CH2, FLD_YT, UNIT_KWH, 38, 4, 1000, false, 3 }, - { TYPE_DC, CH2, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH2, CMD_CALC, false, 3 }, + { TYPE_DC, CH2, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH2, CMD_CALC, false, 3 }, { TYPE_DC, CH3, FLD_UDC, UNIT_V, 28, 2, 10, false, 1 }, { TYPE_DC, CH3, FLD_IDC, UNIT_A, 32, 2, 100, false, 2 }, { TYPE_DC, CH3, FLD_PDC, UNIT_W, 36, 2, 10, false, 1 }, { TYPE_DC, CH3, FLD_YD, UNIT_WH, 48, 2, 1, false, 0 }, { TYPE_DC, CH3, FLD_YT, UNIT_KWH, 42, 4, 1000, false, 3 }, - { TYPE_DC, CH3, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH3, CMD_CALC, false, 3 }, + { TYPE_DC, CH3, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH3, CMD_CALC, false, 3 }, { TYPE_AC, CH0, FLD_UAC, UNIT_V, 50, 2, 10, false, 1 }, { TYPE_AC, CH0, FLD_IAC, UNIT_A, 58, 2, 100, false, 2 }, @@ -43,10 +43,10 @@ static const byteAssign_t byteAssignment[] = { { TYPE_INV, CH0, FLD_T, UNIT_C, 62, 2, 10, true, 1 }, { TYPE_INV, CH0, FLD_EVT_LOG, UNIT_NONE, 64, 2, 1, false, 0 }, - { TYPE_AC, CH0, FLD_YD, UNIT_WH, CALC_YD_CH0, 0, CMD_CALC, false, 0 }, - { TYPE_AC, CH0, FLD_YT, UNIT_KWH, CALC_YT_CH0, 0, CMD_CALC, false, 3 }, - { TYPE_AC, CH0, FLD_PDC, UNIT_W, CALC_PDC_CH0, 0, CMD_CALC, false, 1 }, - { TYPE_AC, CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } + { 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 } }; HMS_4CH::HMS_4CH(HoymilesRadio* radio, const uint64_t serial) @@ -72,4 +72,4 @@ const byteAssign_t* HMS_4CH::getByteAssignment() const uint8_t HMS_4CH::getByteAssignmentSize() const { return sizeof(byteAssignment) / sizeof(byteAssignment[0]); -} \ No newline at end of file +} diff --git a/lib/Hoymiles/src/inverters/HMT_4CH.cpp b/lib/Hoymiles/src/inverters/HMT_4CH.cpp index 717099b77..d92a510f4 100644 --- a/lib/Hoymiles/src/inverters/HMT_4CH.cpp +++ b/lib/Hoymiles/src/inverters/HMT_4CH.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2023 Thomas Basler and others + * Copyright (C) 2023-2024 Thomas Basler and others */ #include "HMT_4CH.h" @@ -10,28 +10,28 @@ static const byteAssign_t byteAssignment[] = { { TYPE_DC, CH0, FLD_PDC, UNIT_W, 8, 2, 10, false, 1 }, { TYPE_DC, CH0, FLD_YT, UNIT_KWH, 12, 4, 1000, false, 3 }, { TYPE_DC, CH0, FLD_YD, UNIT_WH, 20, 2, 1, false, 0 }, - { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH0, CMD_CALC, false, 3 }, + { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH0, CMD_CALC, false, 3 }, { TYPE_DC, CH1, FLD_UDC, UNIT_V, 2, 2, 10, false, 1 }, { TYPE_DC, CH1, FLD_IDC, UNIT_A, 6, 2, 100, false, 2 }, { TYPE_DC, CH1, FLD_PDC, UNIT_W, 10, 2, 10, false, 1 }, { TYPE_DC, CH1, FLD_YT, UNIT_KWH, 16, 4, 1000, false, 3 }, { TYPE_DC, CH1, FLD_YD, UNIT_WH, 22, 2, 1, false, 0 }, - { TYPE_DC, CH1, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH1, CMD_CALC, false, 3 }, + { TYPE_DC, CH1, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH1, CMD_CALC, false, 3 }, { TYPE_DC, CH2, FLD_UDC, UNIT_V, 24, 2, 10, false, 1 }, { TYPE_DC, CH2, FLD_IDC, UNIT_A, 26, 2, 100, false, 2 }, { TYPE_DC, CH2, FLD_PDC, UNIT_W, 30, 2, 10, false, 1 }, { TYPE_DC, CH2, FLD_YT, UNIT_KWH, 34, 4, 1000, false, 3 }, { TYPE_DC, CH2, FLD_YD, UNIT_WH, 42, 2, 1, false, 0 }, - { TYPE_DC, CH2, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH2, CMD_CALC, false, 3 }, + { TYPE_DC, CH2, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH2, CMD_CALC, false, 3 }, { TYPE_DC, CH3, FLD_UDC, UNIT_V, 24, 2, 10, false, 1 }, { TYPE_DC, CH3, FLD_IDC, UNIT_A, 28, 2, 100, false, 2 }, { TYPE_DC, CH3, FLD_PDC, UNIT_W, 32, 2, 10, false, 1 }, { TYPE_DC, CH3, FLD_YT, UNIT_KWH, 38, 4, 1000, false, 3 }, { TYPE_DC, CH3, FLD_YD, UNIT_WH, 44, 2, 1, false, 0 }, - { TYPE_DC, CH3, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH3, CMD_CALC, false, 3 }, + { TYPE_DC, CH3, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH3, CMD_CALC, false, 3 }, { TYPE_AC, CH0, FLD_UAC, UNIT_V, 74, 2, 10, false, 1 }, // dummy { TYPE_AC, CH0, FLD_UAC_1N, UNIT_V, 68, 2, 10, false, 1 }, @@ -52,10 +52,10 @@ static const byteAssign_t byteAssignment[] = { { TYPE_INV, CH0, FLD_T, UNIT_C, 94, 2, 10, true, 1 }, { TYPE_INV, CH0, FLD_EVT_LOG, UNIT_NONE, 96, 2, 1, false, 0 }, - { TYPE_AC, CH0, FLD_YD, UNIT_WH, CALC_YD_CH0, 0, CMD_CALC, false, 0 }, - { TYPE_AC, CH0, FLD_YT, UNIT_KWH, CALC_YT_CH0, 0, CMD_CALC, false, 3 }, - { TYPE_AC, CH0, FLD_PDC, UNIT_W, CALC_PDC_CH0, 0, CMD_CALC, false, 1 }, - { TYPE_AC, CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } + { 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 } }; HMT_4CH::HMT_4CH(HoymilesRadio* radio, const uint64_t serial) diff --git a/lib/Hoymiles/src/inverters/HMT_6CH.cpp b/lib/Hoymiles/src/inverters/HMT_6CH.cpp index 6cbd20971..757cf91de 100644 --- a/lib/Hoymiles/src/inverters/HMT_6CH.cpp +++ b/lib/Hoymiles/src/inverters/HMT_6CH.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2023 Thomas Basler and others + * Copyright (C) 2023-2024 Thomas Basler and others */ #include "HMT_6CH.h" @@ -10,42 +10,42 @@ static const byteAssign_t byteAssignment[] = { { TYPE_DC, CH0, FLD_PDC, UNIT_W, 8, 2, 10, false, 1 }, { TYPE_DC, CH0, FLD_YT, UNIT_KWH, 12, 4, 1000, false, 3 }, { TYPE_DC, CH0, FLD_YD, UNIT_WH, 20, 2, 1, false, 0 }, - { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH0, CMD_CALC, false, 3 }, + { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH0, CMD_CALC, false, 3 }, { TYPE_DC, CH1, FLD_UDC, UNIT_V, 2, 2, 10, false, 1 }, { TYPE_DC, CH1, FLD_IDC, UNIT_A, 6, 2, 100, false, 2 }, { TYPE_DC, CH1, FLD_PDC, UNIT_W, 10, 2, 10, false, 1 }, { TYPE_DC, CH1, FLD_YT, UNIT_KWH, 16, 4, 1000, false, 3 }, { TYPE_DC, CH1, FLD_YD, UNIT_WH, 22, 2, 1, false, 0 }, - { TYPE_DC, CH1, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH1, CMD_CALC, false, 3 }, + { TYPE_DC, CH1, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH1, CMD_CALC, false, 3 }, { TYPE_DC, CH2, FLD_UDC, UNIT_V, 24, 2, 10, false, 1 }, { TYPE_DC, CH2, FLD_IDC, UNIT_A, 26, 2, 100, false, 2 }, { TYPE_DC, CH2, FLD_PDC, UNIT_W, 30, 2, 10, false, 1 }, { TYPE_DC, CH2, FLD_YT, UNIT_KWH, 34, 4, 1000, false, 3 }, { TYPE_DC, CH2, FLD_YD, UNIT_WH, 42, 2, 1, false, 0 }, - { TYPE_DC, CH2, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH2, CMD_CALC, false, 3 }, + { TYPE_DC, CH2, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH2, CMD_CALC, false, 3 }, { TYPE_DC, CH3, FLD_UDC, UNIT_V, 24, 2, 10, false, 1 }, { TYPE_DC, CH3, FLD_IDC, UNIT_A, 28, 2, 100, false, 2 }, { TYPE_DC, CH3, FLD_PDC, UNIT_W, 32, 2, 10, false, 1 }, { TYPE_DC, CH3, FLD_YT, UNIT_KWH, 38, 4, 1000, false, 3 }, { TYPE_DC, CH3, FLD_YD, UNIT_WH, 44, 2, 1, false, 0 }, - { TYPE_DC, CH3, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH3, CMD_CALC, false, 3 }, + { TYPE_DC, CH3, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH3, CMD_CALC, false, 3 }, { TYPE_DC, CH4, FLD_UDC, UNIT_V, 46, 2, 10, false, 1 }, { TYPE_DC, CH4, FLD_IDC, UNIT_A, 48, 2, 100, false, 2 }, { TYPE_DC, CH4, FLD_PDC, UNIT_W, 52, 2, 10, false, 1 }, { TYPE_DC, CH4, FLD_YT, UNIT_KWH, 56, 4, 1000, false, 3 }, { TYPE_DC, CH4, FLD_YD, UNIT_WH, 64, 2, 1, false, 0 }, - { TYPE_DC, CH4, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH4, CMD_CALC, false, 3 }, + { TYPE_DC, CH4, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH4, CMD_CALC, false, 3 }, { TYPE_DC, CH5, FLD_UDC, UNIT_V, 46, 2, 10, false, 1 }, { TYPE_DC, CH5, FLD_IDC, UNIT_A, 50, 2, 100, false, 2 }, { TYPE_DC, CH5, FLD_PDC, UNIT_W, 54, 2, 10, false, 1 }, { TYPE_DC, CH5, FLD_YT, UNIT_KWH, 60, 4, 1000, false, 3 }, { TYPE_DC, CH5, FLD_YD, UNIT_WH, 66, 2, 1, false, 0 }, - { TYPE_DC, CH5, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH5, CMD_CALC, false, 3 }, + { TYPE_DC, CH5, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH5, CMD_CALC, false, 3 }, { TYPE_AC, CH0, FLD_UAC, UNIT_V, 74, 2, 10, false, 1 }, // dummy { TYPE_AC, CH0, FLD_UAC_1N, UNIT_V, 68, 2, 10, false, 1 }, @@ -57,7 +57,7 @@ static const byteAssign_t byteAssignment[] = { { TYPE_AC, CH0, FLD_F, UNIT_HZ, 80, 2, 100, false, 2 }, { TYPE_AC, CH0, FLD_PAC, UNIT_W, 82, 2, 10, false, 1 }, { TYPE_AC, CH0, FLD_Q, UNIT_VAR, 84, 2, 10, true, 1 }, - { TYPE_AC, CH0, FLD_IAC, UNIT_A, 86, 2, 100, false, 2 }, // dummy + { TYPE_AC, CH0, FLD_IAC, UNIT_A, CALC_TOTAL_IAC, 0, CMD_CALC, false, 2 }, { TYPE_AC, CH0, FLD_IAC_1, UNIT_A, 86, 2, 100, false, 2 }, { TYPE_AC, CH0, FLD_IAC_2, UNIT_A, 88, 2, 100, false, 2 }, { TYPE_AC, CH0, FLD_IAC_3, UNIT_A, 90, 2, 100, false, 2 }, @@ -66,10 +66,10 @@ static const byteAssign_t byteAssignment[] = { { TYPE_INV, CH0, FLD_T, UNIT_C, 94, 2, 10, true, 1 }, { TYPE_INV, CH0, FLD_EVT_LOG, UNIT_NONE, 96, 2, 1, false, 0 }, - { TYPE_AC, CH0, FLD_YD, UNIT_WH, CALC_YD_CH0, 0, CMD_CALC, false, 0 }, - { TYPE_AC, CH0, FLD_YT, UNIT_KWH, CALC_YT_CH0, 0, CMD_CALC, false, 3 }, - { TYPE_AC, CH0, FLD_PDC, UNIT_W, CALC_PDC_CH0, 0, CMD_CALC, false, 1 }, - { TYPE_AC, CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } + { 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 } }; HMT_6CH::HMT_6CH(HoymilesRadio* radio, const uint64_t serial) diff --git a/lib/Hoymiles/src/inverters/HM_1CH.cpp b/lib/Hoymiles/src/inverters/HM_1CH.cpp index 7b23207da..670b7dbe2 100644 --- a/lib/Hoymiles/src/inverters/HM_1CH.cpp +++ b/lib/Hoymiles/src/inverters/HM_1CH.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ #include "HM_1CH.h" @@ -10,7 +10,7 @@ static const byteAssign_t byteAssignment[] = { { TYPE_DC, CH0, FLD_PDC, UNIT_W, 6, 2, 10, false, 1 }, { TYPE_DC, CH0, FLD_YD, UNIT_WH, 12, 2, 1, false, 0 }, { TYPE_DC, CH0, FLD_YT, UNIT_KWH, 8, 4, 1000, false, 3 }, - { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH0, CMD_CALC, false, 3 }, + { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH0, CMD_CALC, false, 3 }, { TYPE_AC, CH0, FLD_UAC, UNIT_V, 14, 2, 10, false, 1 }, { TYPE_AC, CH0, FLD_IAC, UNIT_A, 22, 2, 100, false, 2 }, @@ -22,10 +22,10 @@ static const byteAssign_t byteAssignment[] = { { TYPE_INV, CH0, FLD_T, UNIT_C, 26, 2, 10, true, 1 }, { TYPE_INV, CH0, FLD_EVT_LOG, UNIT_NONE, 28, 2, 1, false, 0 }, - { TYPE_AC, CH0, FLD_YD, UNIT_WH, CALC_YD_CH0, 0, CMD_CALC, false, 0 }, - { TYPE_AC, CH0, FLD_YT, UNIT_KWH, CALC_YT_CH0, 0, CMD_CALC, false, 3 }, - { TYPE_AC, CH0, FLD_PDC, UNIT_W, CALC_PDC_CH0, 0, CMD_CALC, false, 1 }, - { TYPE_AC, CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } + { 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 } }; HM_1CH::HM_1CH(HoymilesRadio* radio, const uint64_t serial) @@ -64,4 +64,4 @@ const byteAssign_t* HM_1CH::getByteAssignment() const uint8_t HM_1CH::getByteAssignmentSize() const { return sizeof(byteAssignment) / sizeof(byteAssignment[0]); -} \ No newline at end of file +} diff --git a/lib/Hoymiles/src/inverters/HM_2CH.cpp b/lib/Hoymiles/src/inverters/HM_2CH.cpp index 2f56ec3e6..6d9b7ca91 100644 --- a/lib/Hoymiles/src/inverters/HM_2CH.cpp +++ b/lib/Hoymiles/src/inverters/HM_2CH.cpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ #include "HM_2CH.h" @@ -11,14 +11,14 @@ static const byteAssign_t byteAssignment[] = { { TYPE_DC, CH0, FLD_PDC, UNIT_W, 6, 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_IRR_CH, CH0, CMD_CALC, false, 3 }, + { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH0, CMD_CALC, false, 3 }, { TYPE_DC, CH1, FLD_UDC, UNIT_V, 8, 2, 10, false, 1 }, { TYPE_DC, CH1, FLD_IDC, UNIT_A, 10, 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_IRR_CH, CH1, CMD_CALC, 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 }, @@ -30,10 +30,10 @@ static const byteAssign_t byteAssignment[] = { { 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_AC, CH0, FLD_YD, UNIT_WH, CALC_YD_CH0, 0, CMD_CALC, false, 0 }, - { TYPE_AC, CH0, FLD_YT, UNIT_KWH, CALC_YT_CH0, 0, CMD_CALC, false, 3 }, - { TYPE_AC, CH0, FLD_PDC, UNIT_W, CALC_PDC_CH0, 0, CMD_CALC, false, 1 }, - { TYPE_AC, CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } + { 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 } }; HM_2CH::HM_2CH(HoymilesRadio* radio, const uint64_t serial) @@ -72,4 +72,4 @@ const byteAssign_t* HM_2CH::getByteAssignment() const uint8_t HM_2CH::getByteAssignmentSize() const { return sizeof(byteAssignment) / sizeof(byteAssignment[0]); -} \ No newline at end of file +} diff --git a/lib/Hoymiles/src/inverters/HM_4CH.cpp b/lib/Hoymiles/src/inverters/HM_4CH.cpp index bcad2536f..13ca061a9 100644 --- a/lib/Hoymiles/src/inverters/HM_4CH.cpp +++ b/lib/Hoymiles/src/inverters/HM_4CH.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ #include "HM_4CH.h" @@ -10,28 +10,28 @@ static const byteAssign_t byteAssignment[] = { { TYPE_DC, CH0, FLD_PDC, UNIT_W, 8, 2, 10, false, 1 }, { TYPE_DC, CH0, FLD_YD, UNIT_WH, 20, 2, 1, false, 0 }, { TYPE_DC, CH0, FLD_YT, UNIT_KWH, 12, 4, 1000, false, 3 }, - { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH0, CMD_CALC, false, 3 }, + { TYPE_DC, CH0, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH0, CMD_CALC, false, 3 }, - { TYPE_DC, CH1, FLD_UDC, UNIT_V, CALC_UDC_CH, CH0, CMD_CALC, false, 1 }, + { TYPE_DC, CH1, FLD_UDC, UNIT_V, CALC_CH_UDC, CH0, CMD_CALC, false, 1 }, { TYPE_DC, CH1, FLD_IDC, UNIT_A, 6, 2, 100, false, 2 }, { TYPE_DC, CH1, FLD_PDC, UNIT_W, 10, 2, 10, false, 1 }, { TYPE_DC, CH1, FLD_YD, UNIT_WH, 22, 2, 1, false, 0 }, { TYPE_DC, CH1, FLD_YT, UNIT_KWH, 16, 4, 1000, false, 3 }, - { TYPE_DC, CH1, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH1, CMD_CALC, false, 3 }, + { TYPE_DC, CH1, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH1, CMD_CALC, false, 3 }, { TYPE_DC, CH2, FLD_UDC, UNIT_V, 24, 2, 10, false, 1 }, { TYPE_DC, CH2, FLD_IDC, UNIT_A, 26, 2, 100, false, 2 }, { TYPE_DC, CH2, FLD_PDC, UNIT_W, 30, 2, 10, false, 1 }, { TYPE_DC, CH2, FLD_YD, UNIT_WH, 42, 2, 1, false, 0 }, { TYPE_DC, CH2, FLD_YT, UNIT_KWH, 34, 4, 1000, false, 3 }, - { TYPE_DC, CH2, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH2, CMD_CALC, false, 3 }, + { TYPE_DC, CH2, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH2, CMD_CALC, false, 3 }, - { TYPE_DC, CH3, FLD_UDC, UNIT_V, CALC_UDC_CH, CH2, CMD_CALC, false, 1 }, + { TYPE_DC, CH3, FLD_UDC, UNIT_V, CALC_CH_UDC, CH2, CMD_CALC, false, 1 }, { TYPE_DC, CH3, FLD_IDC, UNIT_A, 28, 2, 100, false, 2 }, { TYPE_DC, CH3, FLD_PDC, UNIT_W, 32, 2, 10, false, 1 }, { TYPE_DC, CH3, FLD_YD, UNIT_WH, 44, 2, 1, false, 0 }, { TYPE_DC, CH3, FLD_YT, UNIT_KWH, 38, 4, 1000, false, 3 }, - { TYPE_DC, CH3, FLD_IRR, UNIT_PCT, CALC_IRR_CH, CH3, CMD_CALC, false, 3 }, + { TYPE_DC, CH3, FLD_IRR, UNIT_PCT, CALC_CH_IRR, CH3, CMD_CALC, false, 3 }, { TYPE_AC, CH0, FLD_UAC, UNIT_V, 46, 2, 10, false, 1 }, { TYPE_AC, CH0, FLD_IAC, UNIT_A, 54, 2, 100, false, 2 }, @@ -43,10 +43,10 @@ static const byteAssign_t byteAssignment[] = { { TYPE_INV, CH0, FLD_T, UNIT_C, 58, 2, 10, true, 1 }, { TYPE_INV, CH0, FLD_EVT_LOG, UNIT_NONE, 60, 2, 1, false, 0 }, - { TYPE_AC, CH0, FLD_YD, UNIT_WH, CALC_YD_CH0, 0, CMD_CALC, false, 0 }, - { TYPE_AC, CH0, FLD_YT, UNIT_KWH, CALC_YT_CH0, 0, CMD_CALC, false, 3 }, - { TYPE_AC, CH0, FLD_PDC, UNIT_W, CALC_PDC_CH0, 0, CMD_CALC, false, 1 }, - { TYPE_AC, CH0, FLD_EFF, UNIT_PCT, CALC_EFF_CH0, 0, CMD_CALC, false, 3 } + { 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 } }; HM_4CH::HM_4CH(HoymilesRadio* radio, const uint64_t serial) @@ -85,4 +85,4 @@ const byteAssign_t* HM_4CH::getByteAssignment() const uint8_t HM_4CH::getByteAssignmentSize() const { return sizeof(byteAssignment) / sizeof(byteAssignment[0]); -} \ No newline at end of file +} diff --git a/lib/Hoymiles/src/parser/DevInfoParser.cpp b/lib/Hoymiles/src/parser/DevInfoParser.cpp index fd6ed5cc7..b2b30a2e8 100644 --- a/lib/Hoymiles/src/parser/DevInfoParser.cpp +++ b/lib/Hoymiles/src/parser/DevInfoParser.cpp @@ -28,9 +28,11 @@ const devInfo_t devInfo[] = { { { 0x10, 0x12, 0x30, ALL }, 1500, "HM-1500-4T" }, { { 0x10, 0x10, 0x10, 0x15 }, static_cast(300 * 0.7), "HM-300-1T" }, // HM-300 factory limitted to 70% + { { 0x10, 0x20, 0x11, ALL }, 300, "HMS-300-1T" }, // 00 { { 0x10, 0x20, 0x21, ALL }, 350, "HMS-350-1T" }, // 00 { { 0x10, 0x20, 0x41, ALL }, 400, "HMS-400-1T" }, // 00 { { 0x10, 0x10, 0x51, ALL }, 450, "HMS-450-1T" }, // 01 + { { 0x10, 0x20, 0x51, ALL }, 450, "HMS-450-1T" }, // 03 { { 0x10, 0x10, 0x71, ALL }, 500, "HMS-500-1T" }, // 02 { { 0x10, 0x20, 0x71, ALL }, 500, "HMS-500-1T v2" }, // 02 { { 0x10, 0x21, 0x11, ALL }, 600, "HMS-600-2T" }, // 01 @@ -47,6 +49,7 @@ const devInfo_t devInfo[] = { { { 0x10, 0x32, 0x41, ALL }, 1600, "HMT-1600-4T" }, // 00 { { 0x10, 0x32, 0x51, ALL }, 1800, "HMT-1800-4T" }, // 00 + { { 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 diff --git a/lib/Hoymiles/src/parser/StatisticsParser.cpp b/lib/Hoymiles/src/parser/StatisticsParser.cpp index 831c1ad1f..bd4056113 100644 --- a/lib/Hoymiles/src/parser/StatisticsParser.cpp +++ b/lib/Hoymiles/src/parser/StatisticsParser.cpp @@ -5,12 +5,13 @@ #include "StatisticsParser.h" #include "../Hoymiles.h" -static float calcYieldTotalCh0(StatisticsParser* iv, uint8_t arg0); -static float calcYieldDayCh0(StatisticsParser* iv, uint8_t arg0); -static float calcUdcCh(StatisticsParser* iv, uint8_t arg0); -static float calcPowerDcCh0(StatisticsParser* iv, uint8_t arg0); -static float calcEffiencyCh0(StatisticsParser* iv, uint8_t arg0); -static float calcIrradiation(StatisticsParser* iv, uint8_t arg0); +static float calcTotalYieldTotal(StatisticsParser* iv, uint8_t arg0); +static float calcTotalYieldDay(StatisticsParser* iv, uint8_t arg0); +static float calcChUdc(StatisticsParser* iv, uint8_t arg0); +static float calcTotalPowerDc(StatisticsParser* iv, uint8_t arg0); +static float calcTotalEffiency(StatisticsParser* iv, uint8_t arg0); +static float calcChIrradiation(StatisticsParser* iv, uint8_t arg0); +static float calcTotalCurrentAc(StatisticsParser* iv, uint8_t arg0); using func_t = float(StatisticsParser*, uint8_t); @@ -20,12 +21,13 @@ struct calcFunc_t { }; const calcFunc_t calcFunctions[] = { - { CALC_YT_CH0, &calcYieldTotalCh0 }, - { CALC_YD_CH0, &calcYieldDayCh0 }, - { CALC_UDC_CH, &calcUdcCh }, - { CALC_PDC_CH0, &calcPowerDcCh0 }, - { CALC_EFF_CH0, &calcEffiencyCh0 }, - { CALC_IRR_CH, &calcIrradiation } + { CALC_TOTAL_YT, &calcTotalYieldTotal }, + { CALC_TOTAL_YD, &calcTotalYieldDay }, + { CALC_CH_UDC, &calcChUdc }, + { CALC_TOTAL_PDC, &calcTotalPowerDc }, + { CALC_TOTAL_EFF, &calcTotalEffiency }, + { CALC_CH_IRR, &calcChIrradiation }, + { CALC_TOTAL_IAC, &calcTotalCurrentAc } }; const FieldId_t runtimeFields[] = { @@ -386,7 +388,7 @@ void StatisticsParser::resetYieldDayCorrection() } } -static float calcYieldTotalCh0(StatisticsParser* iv, uint8_t arg0) +static float calcTotalYieldTotal(StatisticsParser* iv, uint8_t arg0) { float yield = 0; for (auto& channel : iv->getChannelsByType(TYPE_DC)) { @@ -395,7 +397,7 @@ static float calcYieldTotalCh0(StatisticsParser* iv, uint8_t arg0) return yield; } -static float calcYieldDayCh0(StatisticsParser* iv, uint8_t arg0) +static float calcTotalYieldDay(StatisticsParser* iv, uint8_t arg0) { float yield = 0; for (auto& channel : iv->getChannelsByType(TYPE_DC)) { @@ -405,12 +407,12 @@ static float calcYieldDayCh0(StatisticsParser* iv, uint8_t arg0) } // arg0 = channel of source -static float calcUdcCh(StatisticsParser* iv, uint8_t arg0) +static float calcChUdc(StatisticsParser* iv, uint8_t arg0) { return iv->getChannelFieldValue(TYPE_DC, static_cast(arg0), FLD_UDC); } -static float calcPowerDcCh0(StatisticsParser* iv, uint8_t arg0) +static float calcTotalPowerDc(StatisticsParser* iv, uint8_t arg0) { float dcPower = 0; for (auto& channel : iv->getChannelsByType(TYPE_DC)) { @@ -419,8 +421,7 @@ static float calcPowerDcCh0(StatisticsParser* iv, uint8_t arg0) return dcPower; } -// arg0 = channel -static float calcEffiencyCh0(StatisticsParser* iv, uint8_t arg0) +static float calcTotalEffiency(StatisticsParser* iv, uint8_t arg0) { float acPower = 0; for (auto& channel : iv->getChannelsByType(TYPE_AC)) { @@ -439,7 +440,7 @@ static float calcEffiencyCh0(StatisticsParser* iv, uint8_t arg0) } // arg0 = channel -static float calcIrradiation(StatisticsParser* iv, uint8_t arg0) +static float calcChIrradiation(StatisticsParser* iv, uint8_t arg0) { if (nullptr != iv) { if (iv->getStringMaxPower(arg0) > 0) @@ -447,3 +448,12 @@ static float calcIrradiation(StatisticsParser* iv, uint8_t arg0) } return 0.0; } + +static float calcTotalCurrentAc(StatisticsParser* iv, uint8_t arg0) +{ + float acCurrent = 0; + acCurrent += iv->getChannelFieldValue(TYPE_AC, CH0, FLD_IAC_1); + acCurrent += iv->getChannelFieldValue(TYPE_AC, CH0, FLD_IAC_2); + acCurrent += iv->getChannelFieldValue(TYPE_AC, CH0, FLD_IAC_3); + return acCurrent; +} diff --git a/lib/Hoymiles/src/parser/StatisticsParser.h b/lib/Hoymiles/src/parser/StatisticsParser.h index 10f06e04b..90b9a5a9f 100644 --- a/lib/Hoymiles/src/parser/StatisticsParser.h +++ b/lib/Hoymiles/src/parser/StatisticsParser.h @@ -55,12 +55,13 @@ const char* const fields[] = { "Voltage", "Current", "Power", "YieldDay", "Yield // indices to calculation functions, defined in hmInverter.h enum { - CALC_YT_CH0 = 0, - CALC_YD_CH0, - CALC_UDC_CH, - CALC_PDC_CH0, - CALC_EFF_CH0, - CALC_IRR_CH + CALC_TOTAL_YT = 0, + CALC_TOTAL_YD, + CALC_CH_UDC, + CALC_TOTAL_PDC, + CALC_TOTAL_EFF, + CALC_CH_IRR, + CALC_TOTAL_IAC }; enum { CMD_CALC = 0xffff }; @@ -169,4 +170,4 @@ class StatisticsParser : public Parser { bool _enableYieldDayCorrection = false; float _lastYieldDay[CH_CNT] = {}; -}; \ No newline at end of file +}; diff --git a/partitions_custom_16mb.csv b/partitions_custom_16mb.csv new file mode 100644 index 000000000..1c48e6bbe --- /dev/null +++ b/partitions_custom_16mb.csv @@ -0,0 +1,6 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000 +otadata, data, ota, 0xE000, 0x2000 +app0, app, ota_0, 0x10000, 0x7E0000 +app1, app, ota_1, 0x7F0000, 0x7E0000 +spiffs, data, spiffs, 0xFD0000, 0x30000 diff --git a/partitions_custom.csv b/partitions_custom_4mb.csv similarity index 100% rename from partitions_custom.csv rename to partitions_custom_4mb.csv diff --git a/platformio.ini b/platformio.ini index e145e14ee..adf7eb879 100644 --- a/platformio.ini +++ b/platformio.ini @@ -36,9 +36,9 @@ build_unflags = -std=gnu++11 lib_deps = - https://github.com/yubox-node-org/ESPAsyncWebServer + mathieucarbou/ESP Async WebServer @ 2.7.0 bblanchon/ArduinoJson @ ^6.21.5 - https://github.com/bertmelis/espMqttClient.git#v1.5.0 + https://github.com/bertmelis/espMqttClient.git#v1.6.0 nrf24/RF24 @ ^1.4.8 olikraus/U8g2 @ ^2.35.9 buelowp/sunset @ ^1.1.7 @@ -54,7 +54,7 @@ extra_scripts = pre:pio-scripts/patch_apply.py post:pio-scripts/create_factory_bin.py -board_build.partitions = partitions_custom.csv +board_build.partitions = partitions_custom_4mb.csv board_build.filesystem = littlefs board_build.embed_files = webapp_dist/index.html.gz @@ -80,6 +80,16 @@ 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 diff --git a/src/BatteryStats.cpp b/src/BatteryStats.cpp index 606a372fd..48d089165 100644 --- a/src/BatteryStats.cpp +++ b/src/BatteryStats.cpp @@ -51,12 +51,19 @@ static void addLiveViewAlarm(JsonVariant& root, std::string const& name, root["issues"][name] = 2; } +bool BatteryStats::updateAvailable(uint32_t since) const +{ + auto constexpr halfOfAllMillis = std::numeric_limits::max() / 2; + return (_lastUpdate - since) < halfOfAllMillis; +} + void BatteryStats::getLiveViewData(JsonVariant& root) const { root[F("manufacturer")] = _manufacturer; root[F("data_age")] = getAgeSeconds(); - addLiveViewValue(root, "SoC", _SoC, "%", 0); + addLiveViewValue(root, "SoC", _soc, "%", _socPrecision); + addLiveViewValue(root, "voltage", _voltage, "V", 2); } void PylontechBatteryStats::getLiveViewData(JsonVariant& root) const @@ -68,7 +75,6 @@ void PylontechBatteryStats::getLiveViewData(JsonVariant& root) const addLiveViewValue(root, "chargeCurrentLimitation", _chargeCurrentLimitation, "A", 1); addLiveViewValue(root, "dischargeCurrentLimitation", _dischargeCurrentLimitation, "A", 1); addLiveViewValue(root, "stateOfHealth", _stateOfHealth, "%", 0); - addLiveViewValue(root, "voltage", _voltage, "V", 2); addLiveViewValue(root, "current", _current, "A", 1); addLiveViewValue(root, "temperature", _temperature, "°C", 1); @@ -105,18 +111,13 @@ void JkBmsBatteryStats::getJsonData(JsonVariant& root, bool verbose) const using Label = JkBms::DataPointLabel; - auto oVoltage = _dataPoints.get(); - if (oVoltage.has_value()) { - addLiveViewValue(root, "voltage", - static_cast(*oVoltage) / 1000, "V", 2); - } - auto oCurrent = _dataPoints.get(); if (oCurrent.has_value()) { addLiveViewValue(root, "current", static_cast(*oCurrent) / 1000, "A", 2); } + auto oVoltage = _dataPoints.get(); if (oVoltage.has_value() && oCurrent.has_value()) { auto current = static_cast(*oCurrent) / 1000; auto voltage = static_cast(*oVoltage) / 1000; @@ -217,7 +218,8 @@ void BatteryStats::mqttPublish() const { MqttSettings.publish(F("battery/manufacturer"), _manufacturer); MqttSettings.publish(F("battery/dataAge"), String(getAgeSeconds())); - MqttSettings.publish(F("battery/stateOfCharge"), String(_SoC)); + MqttSettings.publish(F("battery/stateOfCharge"), String(_soc)); + MqttSettings.publish(F("battery/voltage"), String(_voltage)); } void PylontechBatteryStats::mqttPublish() const @@ -228,7 +230,6 @@ void PylontechBatteryStats::mqttPublish() const MqttSettings.publish(F("battery/settings/chargeCurrentLimitation"), String(_chargeCurrentLimitation)); MqttSettings.publish(F("battery/settings/dischargeCurrentLimitation"), String(_dischargeCurrentLimitation)); MqttSettings.publish(F("battery/stateOfHealth"), String(_stateOfHealth)); - MqttSettings.publish(F("battery/voltage"), String(_voltage)); MqttSettings.publish(F("battery/current"), String(_current)); MqttSettings.publish(F("battery/temperature"), String(_temperature)); MqttSettings.publish(F("battery/alarm/overCurrentDischarge"), String(_alarmOverCurrentDischarge)); @@ -260,6 +261,10 @@ void JkBmsBatteryStats::mqttPublish() const Label::CellsMilliVolt, // complex data format Label::ModificationPassword, // sensitive data Label::BatterySoCPercent // already published by base class + // NOTE that voltage is also published by the base class, however, we + // previously published it only from here using the respective topic. + // to avoid a breaking change, we publish the value again using the + // "old" topic. }; // regularly publish all topics regardless of whether or not their value changed @@ -335,9 +340,16 @@ void JkBmsBatteryStats::updateFrom(JkBms::DataPointContainer const& dp) auto oSoCValue = dp.get(); if (oSoCValue.has_value()) { - _SoC = *oSoCValue; auto oSoCDataPoint = dp.getDataPointFor(); - _lastUpdateSoC = oSoCDataPoint->getTimestamp(); + BatteryStats::setSoC(*oSoCValue, 0/*precision*/, + oSoCDataPoint->getTimestamp()); + } + + auto oVoltage = dp.get(); + if (oVoltage.has_value()) { + auto oVoltageDataPoint = dp.getDataPointFor(); + BatteryStats::setVoltage(static_cast(*oVoltage) / 1000, + oVoltageDataPoint->getTimestamp()); } _dataPoints.updateFrom(dp); @@ -360,8 +372,9 @@ void JkBmsBatteryStats::updateFrom(JkBms::DataPointContainer const& dp) } void VictronSmartShuntStats::updateFrom(VeDirectShuntController::veShuntStruct const& shuntData) { - _SoC = shuntData.SOC / 10; - _voltage = shuntData.V; + BatteryStats::setVoltage(shuntData.V, millis()); + BatteryStats::setSoC(static_cast(shuntData.SOC) / 10, 1/*precision*/, millis()); + _current = shuntData.I; _modelName = shuntData.getPidAsString().data(); _chargeCycles = shuntData.H4; @@ -380,14 +393,12 @@ void VictronSmartShuntStats::updateFrom(VeDirectShuntController::veShuntStruct c _alarmHighTemperature = shuntData.AR & 64; _lastUpdate = VeDirectShunt.getLastUpdate(); - _lastUpdateSoC = VeDirectShunt.getLastUpdate(); } void VictronSmartShuntStats::getLiveViewData(JsonVariant& root) const { BatteryStats::getLiveViewData(root); // values go into the "Status" card of the web application - addLiveViewValue(root, "voltage", _voltage, "V", 2); addLiveViewValue(root, "current", _current, "A", 1); addLiveViewValue(root, "chargeCycles", _chargeCycles, "", 0); addLiveViewValue(root, "chargedEnergy", _chargedEnergy, "KWh", 1); @@ -406,7 +417,6 @@ void VictronSmartShuntStats::getLiveViewData(JsonVariant& root) const { void VictronSmartShuntStats::mqttPublish() const { BatteryStats::mqttPublish(); - MqttSettings.publish(F("battery/voltage"), String(_voltage)); MqttSettings.publish(F("battery/current"), String(_current)); MqttSettings.publish(F("battery/chargeCycles"), String(_chargeCycles)); MqttSettings.publish(F("battery/chargedEnergy"), String(_chargedEnergy)); diff --git a/src/Configuration.cpp b/src/Configuration.cpp index 5004b3cbe..a1764f1e7 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -185,12 +185,14 @@ bool ConfigurationClass::write() powerlimiter["battery_drain_strategy"] = config.PowerLimiter.BatteryDrainStategy; 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; powerlimiter["target_power_consumption_hysteresis"] = config.PowerLimiter.TargetPowerConsumptionHysteresis; powerlimiter["lower_power_limit"] = config.PowerLimiter.LowerPowerLimit; powerlimiter["upper_power_limit"] = config.PowerLimiter.UpperPowerLimit; + powerlimiter["ignore_soc"] = config.PowerLimiter.IgnoreSoc; powerlimiter["battery_soc_start_threshold"] = config.PowerLimiter.BatterySocStartThreshold; powerlimiter["battery_soc_stop_threshold"] = config.PowerLimiter.BatterySocStopThreshold; powerlimiter["voltage_start_threshold"] = config.PowerLimiter.VoltageStartThreshold; @@ -207,7 +209,8 @@ bool ConfigurationClass::write() battery["provider"] = config.Battery.Provider; battery["jkbms_interface"] = config.Battery.JkBmsInterface; battery["jkbms_polling_interval"] = config.Battery.JkBmsPollingInterval; - battery["mqtt_topic"] = config.Battery.MqttTopic; + battery["mqtt_topic"] = config.Battery.MqttSocTopic; + battery["mqtt_voltage_topic"] = config.Battery.MqttVoltageTopic; JsonObject huawei = doc.createNestedObject("huawei"); huawei["enabled"] = config.Huawei.Enabled; @@ -429,12 +432,14 @@ bool ConfigurationClass::read() config.PowerLimiter.BatteryDrainStategy = powerlimiter["battery_drain_strategy"] | POWERLIMITER_BATTERY_DRAIN_STRATEGY; 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; config.PowerLimiter.TargetPowerConsumptionHysteresis = powerlimiter["target_power_consumption_hysteresis"] | POWERLIMITER_TARGET_POWER_CONSUMPTION_HYSTERESIS; config.PowerLimiter.LowerPowerLimit = powerlimiter["lower_power_limit"] | POWERLIMITER_LOWER_POWER_LIMIT; config.PowerLimiter.UpperPowerLimit = powerlimiter["upper_power_limit"] | POWERLIMITER_UPPER_POWER_LIMIT; + config.PowerLimiter.IgnoreSoc = powerlimiter["ignore_soc"] | POWERLIMITER_IGNORE_SOC; config.PowerLimiter.BatterySocStartThreshold = powerlimiter["battery_soc_start_threshold"] | POWERLIMITER_BATTERY_SOC_START_THRESHOLD; config.PowerLimiter.BatterySocStopThreshold = powerlimiter["battery_soc_stop_threshold"] | POWERLIMITER_BATTERY_SOC_STOP_THRESHOLD; config.PowerLimiter.VoltageStartThreshold = powerlimiter["voltage_start_threshold"] | POWERLIMITER_VOLTAGE_START_THRESHOLD; @@ -451,7 +456,8 @@ bool ConfigurationClass::read() config.Battery.Provider = battery["provider"] | BATTERY_PROVIDER; config.Battery.JkBmsInterface = battery["jkbms_interface"] | BATTERY_JKBMS_INTERFACE; config.Battery.JkBmsPollingInterval = battery["jkbms_polling_interval"] | BATTERY_JKBMS_POLLING_INTERVAL; - strlcpy(config.Battery.MqttTopic, battery["mqtt_topic"] | "", sizeof(config.Battery.MqttTopic)); + strlcpy(config.Battery.MqttSocTopic, battery["mqtt_topic"] | "", sizeof(config.Battery.MqttSocTopic)); + strlcpy(config.Battery.MqttVoltageTopic, battery["mqtt_voltage_topic"] | "", sizeof(config.Battery.MqttVoltageTopic)); JsonObject huawei = doc["huawei"]; config.Huawei.Enabled = huawei["enabled"] | HUAWEI_ENABLED; diff --git a/src/Datastore.cpp b/src/Datastore.cpp index 5bfbb98ed..15a6dad0f 100644 --- a/src/Datastore.cpp +++ b/src/Datastore.cpp @@ -81,14 +81,17 @@ void DatastoreClass::loop() } } - for (auto& c : inv->Statistics()->getChannelsByType(TYPE_AC)) { + for (auto& c : inv->Statistics()->getChannelsByType(TYPE_INV)) { if (cfg->Poll_Enable) { - _totalAcYieldTotalEnabled += inv->Statistics()->getChannelFieldValue(TYPE_AC, c, FLD_YT); - _totalAcYieldDayEnabled += inv->Statistics()->getChannelFieldValue(TYPE_AC, c, FLD_YD); + _totalAcYieldTotalEnabled += inv->Statistics()->getChannelFieldValue(TYPE_INV, c, FLD_YT); + _totalAcYieldDayEnabled += inv->Statistics()->getChannelFieldValue(TYPE_INV, c, FLD_YD); - _totalAcYieldTotalDigits = max(_totalAcYieldTotalDigits, inv->Statistics()->getChannelFieldDigits(TYPE_AC, c, FLD_YT)); - _totalAcYieldDayDigits = max(_totalAcYieldDayDigits, inv->Statistics()->getChannelFieldDigits(TYPE_AC, c, FLD_YD)); + _totalAcYieldTotalDigits = max(_totalAcYieldTotalDigits, inv->Statistics()->getChannelFieldDigits(TYPE_INV, c, FLD_YT)); + _totalAcYieldDayDigits = max(_totalAcYieldDayDigits, inv->Statistics()->getChannelFieldDigits(TYPE_INV, c, FLD_YD)); } + } + + for (auto& c : inv->Statistics()->getChannelsByType(TYPE_AC)) { if (inv->getEnablePolling()) { _totalAcPowerEnabled += inv->Statistics()->getChannelFieldValue(TYPE_AC, c, FLD_PAC); _totalAcPowerDigits = max(_totalAcPowerDigits, inv->Statistics()->getChannelFieldDigits(TYPE_AC, c, FLD_PAC)); diff --git a/src/Display_Graphic.cpp b/src/Display_Graphic.cpp index 12b2aa56e..98d46eea1 100644 --- a/src/Display_Graphic.cpp +++ b/src/Display_Graphic.cpp @@ -4,6 +4,8 @@ */ #include "Display_Graphic.h" #include "Datastore.h" +#include "PowerMeter.h" +#include "Configuration.h" #include #include #include @@ -31,8 +33,11 @@ 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_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() @@ -67,11 +72,19 @@ void DisplayGraphicClass::init(Scheduler& scheduler, const DisplayType_t type, c void DisplayGraphicClass::calcLineHeights() { - uint8_t yOff = 0; + bool diagram = (_isLarge && _diagram_mode == DiagramMode_t::Small); + // the diagram needs space. we need to keep + // away from the y-axis label in particular. + uint8_t yOff = (diagram ? 7 : 0); for (uint8_t i = 0; i < 4; i++) { setFont(i); - yOff += (_display->getMaxCharHeight()); + yOff += _display->getAscent(); _lineOffsets[i] = yOff; + yOff += ((!_isLarge || diagram) ? 2 : 3); + // the descent is a negative value and moves the *next* line's + // baseline. the first line never uses a letter with descent and + // we need that space when showing the small diagram. + yOff -= ((i == 0 && diagram) ? 0 : _display->getDescent()); } } @@ -103,27 +116,23 @@ void DisplayGraphicClass::printText(const char* text, const uint8_t line) if (!_isLarge) { dispX = (line == 0) ? 5 : 0; } else { - switch (line) { - case 0: - if (_diagram_mode == DiagramMode_t::Small) { - // Center between left border and diagram - dispX = (CHART_POSX - _display->getStrWidth(text)) / 2; - } else { - // Center on screen - dispX = (_display->getDisplayWidth() - _display->getStrWidth(text)) / 2; - } - break; - case 3: + if (line == 0 && _diagram_mode == DiagramMode_t::Small) { + // Center between left border and diagram + dispX = (CHART_POSX - _display->getStrWidth(text)) / 2; + } else { // Center on screen dispX = (_display->getDisplayWidth() - _display->getStrWidth(text)) / 2; - break; - default: - dispX = 5; - break; } } - dispX += enableScreensaver ? (_mExtra % 7) : 0; + if (enableScreensaver) { + unsigned maxOffset = (_isLarge ? 8 : 6); + unsigned period = 2 * maxOffset; + unsigned step = _mExtra % period; + int offset = (step <= maxOffset) ? step : (period - step); + offset -= (_isLarge ? 5 : 0); // oscillate around center on large screens + dispX += offset; + } _display->drawStr(dispX, _lineOffsets[line], text); } @@ -236,7 +245,9 @@ void DisplayGraphicClass::loop() snprintf(_fmtText, sizeof(_fmtText), i18n_yield_today_wh[_display_language], Datastore.getTotalAcYieldDayEnabled()); printText(_fmtText, 1); - snprintf(_fmtText, sizeof(_fmtText), i18n_yield_total_kwh[_display_language], Datastore.getTotalAcYieldTotalEnabled()); + 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); printText(_fmtText, 2); //<======================= @@ -252,6 +263,32 @@ void DisplayGraphicClass::loop() } } + // the IP and time info in the third line use three-second slots. the + // timing for the power meter is chosen such that every third of those + // three-second slots is used to NOT overwrite the total inverter energy. + bool timing = (_mExtra % 9) >= 3; + + if (showText && Configuration.get().PowerMeter.Enabled && timing && !displayPowerSave) { + // erase the third line and print the power meter value instead. + // we do it this way to touch as least upstream code as possible + // to make maintenance easier. + setFont(2); + auto lineHeight = _display->getAscent() - _display->getDescent(); + auto y = _lineOffsets[2] - _display->getAscent(); + _display->setDrawColor(0); + _display->drawBox(0, y, _display->getDisplayWidth(), lineHeight); + _display->setDrawColor(1); + + auto acPower = PowerMeter.getPowerTotal(false); + if (acPower > 999) { + snprintf(_fmtText, sizeof(_fmtText), i18n_meter_power_kw[_display_language], (acPower / 1000)); + } else { + snprintf(_fmtText, sizeof(_fmtText), i18n_meter_power_w[_display_language], acPower); + } + + printText(_fmtText, 2); + } + _display->sendBuffer(); _mExtra++; diff --git a/src/MqttBattery.cpp b/src/MqttBattery.cpp index 9e1992429..03e141e2f 100644 --- a/src/MqttBattery.cpp +++ b/src/MqttBattery.cpp @@ -10,20 +10,35 @@ bool MqttBattery::init(bool verboseLogging) _verboseLogging = verboseLogging; auto const& config = Configuration.get(); - _socTopic = config.Battery.MqttTopic; - if (_socTopic.isEmpty()) { return false; } + _socTopic = config.Battery.MqttSocTopic; + if (!_socTopic.isEmpty()) { + MqttSettings.subscribe(_socTopic, 0/*QoS*/, + std::bind(&MqttBattery::onMqttMessageSoC, + this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4, + std::placeholders::_5, std::placeholders::_6) + ); - MqttSettings.subscribe(_socTopic, 0/*QoS*/, - std::bind(&MqttBattery::onMqttMessage, - this, std::placeholders::_1, std::placeholders::_2, - std::placeholders::_3, std::placeholders::_4, - std::placeholders::_5, std::placeholders::_6) - ); + if (_verboseLogging) { + MessageOutput.printf("MqttBattery: Subscribed to '%s' for SoC readings\r\n", + _socTopic.c_str()); + } + } - if (_verboseLogging) { - MessageOutput.printf("MqttBattery: Subscribed to '%s'\r\n", - _socTopic.c_str()); + _voltageTopic = config.Battery.MqttVoltageTopic; + if (!_voltageTopic.isEmpty()) { + MqttSettings.subscribe(_voltageTopic, 0/*QoS*/, + std::bind(&MqttBattery::onMqttMessageVoltage, + this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4, + std::placeholders::_5, std::placeholders::_6) + ); + + if (_verboseLogging) { + MessageOutput.printf("MqttBattery: Subscribed to '%s' for voltage readings\r\n", + _voltageTopic.c_str()); + } } return true; @@ -31,35 +46,69 @@ bool MqttBattery::init(bool verboseLogging) void MqttBattery::deinit() { - if (_socTopic.isEmpty()) { return; } - MqttSettings.unsubscribe(_socTopic); + if (!_voltageTopic.isEmpty()) { + MqttSettings.unsubscribe(_voltageTopic); + } + + if (!_socTopic.isEmpty()) { + MqttSettings.unsubscribe(_socTopic); + } } -void MqttBattery::onMqttMessage(espMqttClientTypes::MessageProperties const& properties, - char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total) -{ - float soc = 0; - std::string value(reinterpret_cast(payload), len); +std::optional MqttBattery::getFloat(std::string const& src, char const* topic) { + float res = 0; try { - soc = std::stof(value); + res = std::stof(src); } catch(std::invalid_argument const& e) { MessageOutput.printf("MqttBattery: Cannot parse payload '%s' in topic '%s' as float\r\n", - value.c_str(), topic); - return; + src.c_str(), topic); + return std::nullopt; } - if (soc < 0 || soc > 100) { + return res; +} + +void MqttBattery::onMqttMessageSoC(espMqttClientTypes::MessageProperties const& properties, + char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total) +{ + auto soc = getFloat(std::string(reinterpret_cast(payload), len), topic); + if (!soc.has_value()) { return; } + + if (*soc < 0 || *soc > 100) { MessageOutput.printf("MqttBattery: Implausible SoC '%.2f' in topic '%s'\r\n", - soc, topic); + *soc, topic); return; } - _stats->setSoC(static_cast(soc)); + _stats->setSoC(*soc, 0/*precision*/, millis()); if (_verboseLogging) { MessageOutput.printf("MqttBattery: Updated SoC to %d from '%s'\r\n", - static_cast(soc), topic); + static_cast(*soc), topic); + } +} + +void MqttBattery::onMqttMessageVoltage(espMqttClientTypes::MessageProperties const& properties, + char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total) +{ + auto voltage = getFloat(std::string(reinterpret_cast(payload), len), topic); + if (!voltage.has_value()) { return; } + + // since this project is revolving around Hoymiles microinverters, which can + // only handle up to 65V of input voltage at best, it is safe to assume that + // an even higher voltage is implausible. + if (*voltage < 0 || *voltage > 65) { + MessageOutput.printf("MqttBattery: Implausible voltage '%.2f' in topic '%s'\r\n", + *voltage, topic); + return; + } + + _stats->setVoltage(*voltage, millis()); + + if (_verboseLogging) { + MessageOutput.printf("MqttBattery: Updated voltage to %.2f from '%s'\r\n", + *voltage, topic); } } diff --git a/src/MqttHandleHass.cpp b/src/MqttHandleHass.cpp index 1df7237e6..21ff0fa2b 100644 --- a/src/MqttHandleHass.cpp +++ b/src/MqttHandleHass.cpp @@ -107,7 +107,7 @@ void MqttHandleHassClass::publishInverterField(std::shared_ptr const String serial = inv->serialString(); String fieldName; - if (type == TYPE_AC && fieldType.fieldId == FLD_PDC) { + if (type == TYPE_INV && fieldType.fieldId == FLD_PDC) { fieldName = "PowerDC"; } else { fieldName = inv->Statistics()->getChannelFieldName(type, channel, fieldType.fieldId); diff --git a/src/MqttHandleInverter.cpp b/src/MqttHandleInverter.cpp index 53cf490b2..de2778d10 100644 --- a/src/MqttHandleInverter.cpp +++ b/src/MqttHandleInverter.cpp @@ -141,7 +141,7 @@ String MqttHandleInverterClass::getTopic(std::shared_ptr inv, } String chanName; - if (type == TYPE_AC && fieldId == FLD_PDC) { + if (type == TYPE_INV && fieldId == FLD_PDC) { chanName = "powerdc"; } else { chanName = inv->Statistics()->getChannelFieldName(type, channel, fieldId); diff --git a/src/MqttSettings.cpp b/src/MqttSettings.cpp index a0b236862..c2b94662a 100644 --- a/src/MqttSettings.cpp +++ b/src/MqttSettings.cpp @@ -128,7 +128,7 @@ void MqttSettingsClass::performConnect() } else { static_cast(_mqttClient)->setCredentials(config.Mqtt.Username, config.Mqtt.Password); } - static_cast(_mqttClient)->setWill(willTopic.c_str(), 2, config.Mqtt.Retain, config.Mqtt.Lwt.Value_Offline); + static_cast(_mqttClient)->setWill(willTopic.c_str(), config.Mqtt.Lwt.Qos, config.Mqtt.Retain, config.Mqtt.Lwt.Value_Offline); static_cast(_mqttClient)->setClientId(clientId.c_str()); static_cast(_mqttClient)->setCleanSession(config.Mqtt.CleanSession); static_cast(_mqttClient)->onConnect(std::bind(&MqttSettingsClass::onMqttConnect, this, _1)); @@ -226,4 +226,4 @@ void MqttSettingsClass::createMqttClientObject() } } -MqttSettingsClass MqttSettings; \ No newline at end of file +MqttSettingsClass MqttSettings; diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index e7f2ea2d6..e069e2e4e 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -12,6 +12,7 @@ #include "Huawei_can.h" #include #include "MessageOutput.h" +#include "inverters/HMS_4CH.h" #include #include #include @@ -30,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" }, @@ -46,9 +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::Settling, "waiting for the system to settle" }, + { 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" }, }; @@ -79,36 +82,26 @@ void PowerLimiterClass::announceStatus(PowerLimiterClass::Status status) /** * returns true if the inverter state was changed or is about to change, i.e., * if it is actually in need of a shutdown. returns false otherwise, i.e., the - * inverter is already (assumed to be) shut down. + * inverter is already shut down and the inverter limit is set to the configured + * lower power limit. */ bool PowerLimiterClass::shutdown(PowerLimiterClass::Status status) { announceStatus(status); - if (_inverter == nullptr || !_inverter->isProducing() || - (_shutdownTimeout > 0 && _shutdownTimeout < millis()) ) { - // we are actually (already) done with shutting down the inverter, - // or a shutdown attempt was initiated but it timed out. - _inverter = nullptr; - _shutdownTimeout = 0; - return false; - } - - if (!_inverter->isReachable()) { return true; } // retry later (until timeout) - - // retry shutdown for a maximum amount of time before giving up - if (_shutdownTimeout == 0) { _shutdownTimeout = millis() + 10 * 1000; } + _shutdownPending = true; - auto lastLimitCommandState = _inverter->SystemConfigPara()->getLastLimitCommandSuccess(); - if (CMD_PENDING == lastLimitCommandState) { return true; } + _oTargetPowerState = false; - auto lastPowerCommandState = _inverter->PowerCommand()->getLastPowerCommandSuccess(); - if (CMD_PENDING == lastPowerCommandState) { return true; } - - CONFIG_T& config = Configuration.get(); - commitPowerLimit(_inverter, config.PowerLimiter.LowerPowerLimit, false); + auto const& config = Configuration.get(); + if ( (Status::PowerMeterTimeout == status || + Status::CalculatedLimitBelowMinLimit == status) + && config.PowerLimiter.IsInverterSolarPowered) { + _oTargetPowerState = true; + } - return true; + _oTargetPowerLimitWatts = config.PowerLimiter.LowerPowerLimit; + return updateInverter(); } void PowerLimiterClass::loop() @@ -124,12 +117,13 @@ void PowerLimiterClass::loop() return announceStatus(Status::WaitingForValidTimestamp); } - if (_shutdownTimeout > 0) { - // we transition from SHUTDOWN to OFF when we know the inverter was - // shut down. until then, we retry shutting it down. in this case we - // preserve the original status that lead to the decision to shut down. - shutdown(); - return; + // take care that the last requested power + // limit and power state are actually reached + if (updateInverter()) { return; } + + if (_shutdownPending) { + _shutdownPending = false; + _inverter = nullptr; } if (!config.PowerLimiter.Enabled) { @@ -172,18 +166,6 @@ void PowerLimiterClass::loop() return announceStatus(Status::InverterCommandsDisabled); } - // concerns active power commands (power limits) only (also from web app or MQTT) - auto lastLimitCommandState = _inverter->SystemConfigPara()->getLastLimitCommandSuccess(); - if (CMD_PENDING == lastLimitCommandState) { - return announceStatus(Status::InverterLimitPending); - } - - // concerns power commands (start, stop, restart) only (also from web app or MQTT) - auto lastPowerCommandState = _inverter->PowerCommand()->getLastPowerCommandSuccess(); - if (CMD_PENDING == lastPowerCommandState) { - return announceStatus(Status::InverterPowerCmdPending); - } - // a calculated power limit will always be limited to the reported // device's max power. that upper limit is only known after the first // DevInfoSimpleCommand succeeded. @@ -214,16 +196,11 @@ void PowerLimiterClass::loop() _inverter->SystemConfigPara()->getLastUpdateCommand(), _inverter->PowerCommand()->getLastUpdateCommand()); - // wait for power meter and inverter stat updates after a settling phase - auto settlingEnd = lastUpdateCmd + 3 * 1000; - - if (millis() < settlingEnd) { return announceStatus(Status::Settling); } - - if (_inverter->Statistics()->getLastUpdate() <= settlingEnd) { + if (_inverter->Statistics()->getLastUpdate() <= lastUpdateCmd) { return announceStatus(Status::InverterStatsPending); } - if (PowerMeter.getLastPowerMeterUpdate() <= settlingEnd) { + if (PowerMeter.getLastPowerMeterUpdate() <= lastUpdateCmd) { return announceStatus(Status::PowerMeterPending); } @@ -258,46 +235,48 @@ 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 drain strategy, 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.BatteryDrainStategy == EMPTY_AT_NIGHT) { + 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) { - MessageOutput.printf("[DPL::loop] battery interface %s, SoC: %d %%, StartTH: %d %%, StopTH: %d %%, SoC age: %d s\r\n", + _batteryDischargeEnabled = getBatteryPower(); + + auto logging = [this,&config]() -> void { + MessageOutput.printf("[DPL::loop] PowerMeter: %d W, target consumption: %d W, solar power: %d W\r\n", + static_cast(round(PowerMeter.getPowerTotal())), + config.PowerLimiter.TargetPowerConsumption, + getSolarPower()); + + if (config.PowerLimiter.IsInverterSolarPowered) { return; } + + 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(), config.PowerLimiter.BatterySocStartThreshold, config.PowerLimiter.BatterySocStopThreshold, - Battery.getStats()->getSoCAgeSeconds()); + Battery.getStats()->getSoCAgeSeconds(), + (config.PowerLimiter.IgnoreSoc?"yes":"no")); - float dcVoltage = _inverter->Statistics()->getChannelFieldValue(TYPE_DC, (ChannelNum_t)config.PowerLimiter.InverterChannelId, FLD_UDC); + auto dcVoltage = getBatteryVoltage(true/*log voltages only once per DPL loop*/); MessageOutput.printf("[DPL::loop] dcVoltage: %.2f V, loadCorrectedVoltage: %.2f V, StartTH: %.2f V, StopTH: %.2f V\r\n", dcVoltage, getLoadCorrectedVoltage(), config.PowerLimiter.VoltageStartThreshold, @@ -308,25 +287,16 @@ void PowerLimiterClass::loop() (isStopThresholdReached()?"yes":"no"), (_inverter->isProducing()?"is":"is NOT")); - MessageOutput.printf("[DPL::loop] SolarPT %s, Drain Strategy: %i, canUseDirectSolarPower: %s\r\n", + MessageOutput.printf("[DPL::loop] battery discharging %s, SolarPT %s, Drain Strategy: %i\r\n", + (_batteryDischargeEnabled?"allowed":"prevented"), (config.PowerLimiter.SolarPassThroughEnabled?"enabled":"disabled"), - config.PowerLimiter.BatteryDrainStategy, (canUseDirectSolarPower()?"yes":"no")); + config.PowerLimiter.BatteryDrainStategy); + }; - 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); - } + if (_verboseLogging) { logging(); } // Calculate and set Power Limit (NOTE: might reset _inverter to nullptr!) - int32_t newPowerLimit = calcPowerLimit(_inverter, canUseDirectSolarPower(), _batteryDischargeEnabled); - bool limitUpdated = setNewPowerLimit(_inverter, newPowerLimit); - - if (_verboseLogging) { - MessageOutput.printf("[DPL::loop] ******************* Leaving PL, calculated limit: %d W, requested limit: %d W (%s)\r\n", - newPowerLimit, _lastRequestedPowerLimit, - (limitUpdated?"updated from calculated":"kept last requested")); - } + bool limitUpdated = calcPowerLimit(_inverter, getSolarPower(), _batteryDischargeEnabled); _lastCalculation = millis(); @@ -339,6 +309,46 @@ void PowerLimiterClass::loop() _calculationBackoffMs = _calculationBackoffMsDefault; } +/** + * determines the battery's voltage, trying multiple data providers. the most + * accurate data is expected to be delivered by a BMS, if it's available. more + * accurate and more recent than the inverter's voltage reading is the volage + * at the charge controller's output, if it's available. only as a fallback + * the voltage reported by the inverter is used. + */ +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)"); + return 0.0; + } + + auto const& config = Configuration.get(); + auto channel = static_cast(config.PowerLimiter.InverterChannelId); + float inverterVoltage = _inverter->Statistics()->getChannelFieldValue(TYPE_DC, channel, FLD_UDC); + float res = inverterVoltage; + + float chargeControllerVoltage = -1; + if (VictronMppt.isDataValid()) { + res = chargeControllerVoltage = static_cast(VictronMppt.getOutputVoltage()); + } + + float bmsVoltage = -1; + auto stats = Battery.getStats(); + if (config.Battery.Enabled + && stats->isVoltageValid() + && stats->getVoltageAgeSeconds() < 60) { + res = bmsVoltage = stats->getVoltage(); + } + + if (log) { + MessageOutput.printf("[DPL::getBatteryVoltage] BMS: %.2f V, MPPT: %.2f V, inverter: %.2f V, returning: %.2fV\r\n", + bmsVoltage, chargeControllerVoltage, inverterVoltage, res); + } + + return res; +} + /** * calculate the AC output power (limit) to set, such that the inverter uses * the given power on its DC side, i.e., adjust the power for the inverter's @@ -400,50 +410,39 @@ uint8_t PowerLimiterClass::getPowerLimiterState() { return PL_UI_STATE_INACTIVE; } -int32_t PowerLimiterClass::getLastRequestedPowerLimit() { - return _lastRequestedPowerLimit; -} - -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 (!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); + } -// 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()); - if (!solarPowerEnabled && !batteryDischargeEnabled) { - // Case 1 - No energy sources available - return 0; - } + auto const& config = Configuration.get(); 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)); + auto acPower = inverter->Statistics()->getChannelFieldValue(TYPE_AC, CH0, FLD_PAC); newPowerLimit += acPower; } @@ -452,134 +451,271 @@ 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()); - - // 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; - } - } + int32_t solarPowerAC = inverterPowerDcToAc(inverter, solarPowerDC); - // We should use Victron solar power only (corrected by efficiency factor) - if (solarPowerEnabled && !batteryDischargeEnabled) { - // Case 2 - Limit power to solar power only + if (!batteryPower) { + // 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::loop] Consuming Solar Power Only -> adjustedVictronChargePower: %d W, newPowerLimit: %d W\r\n", - adjustedVictronChargePower, newPowerLimit); + MessageOutput.printf("[DPL::loop] Consuming Solar Power Only -> solarPowerAC: %d W, newPowerLimit: %d W\r\n", + solarPowerAC, newPowerLimit); } - newPowerLimit = std::min(newPowerLimit, adjustedVictronChargePower); + return setNewPowerLimit(inverter, std::min(newPowerLimit, solarPowerAC)); } - return newPowerLimit; + // convert all solar power if full solar-passthrough is active + if (useFullSolarPassthrough()) { + return setNewPowerLimit(inverter, std::max(newPowerLimit, solarPowerAC)); + } + + return setNewPowerLimit(inverter, newPowerLimit); } -void PowerLimiterClass::commitPowerLimit(std::shared_ptr inverter, int32_t limit, bool enablePowerProduction) +/** + * updates the inverter state (power production and limit). returns true if a + * change to its state was requested or is pending. this function only requests + * one change (limit value or production on/off) at a time. + */ +bool PowerLimiterClass::updateInverter() { - // disable power production as soon as possible. - // setting the power limit is less important. - if (!enablePowerProduction && inverter->isProducing()) { - MessageOutput.println("[DPL::commitPowerLimit] Stopping inverter..."); - inverter->sendPowerControlRequest(false); + auto reset = [this]() -> bool { + _oTargetPowerState = std::nullopt; + _oTargetPowerLimitWatts = std::nullopt; + _oUpdateStartMillis = std::nullopt; + return false; + }; + + if (nullptr == _inverter) { return reset(); } + + if (!_oUpdateStartMillis.has_value()) { + _oUpdateStartMillis = millis(); } - inverter->sendActivePowerControlRequest(static_cast(limit), - PowerLimitControlType::AbsolutNonPersistent); + if ((millis() - *_oUpdateStartMillis) > 30 * 1000) { + MessageOutput.printf("[DPL::updateInverter] timeout, " + "state transition pending: %s, limit pending: %s\r\n", + (_oTargetPowerState.has_value()?"yes":"no"), + (_oTargetPowerLimitWatts.has_value()?"yes":"no")); + return reset(); + } - _lastRequestedPowerLimit = limit; - _lastPowerLimitMillis = millis(); + auto constexpr halfOfAllMillis = std::numeric_limits::max() / 2; + + auto switchPowerState = [this](bool transitionOn) -> bool { + // no power state transition requested at all + if (!_oTargetPowerState.has_value()) { return false; } + + // the transition that may be started is not the one which is requested + if (transitionOn != *_oTargetPowerState) { return false; } + + // wait for pending power command(s) to complete + auto lastPowerCommandState = _inverter->PowerCommand()->getLastPowerCommandSuccess(); + if (CMD_PENDING == lastPowerCommandState) { + announceStatus(Status::InverterPowerCmdPending); + return true; + } + + // we need to wait for statistics that are more recent than the last + // power update command to reliably use _inverter->isProducing() + auto lastPowerCommandMillis = _inverter->PowerCommand()->getLastUpdateCommand(); + auto lastStatisticsMillis = _inverter->Statistics()->getLastUpdate(); + if ((lastStatisticsMillis - lastPowerCommandMillis) > halfOfAllMillis) { return true; } + + if (_inverter->isProducing() != *_oTargetPowerState) { + MessageOutput.printf("[DPL::updateInverter] %s inverter...\r\n", + ((*_oTargetPowerState)?"Starting":"Stopping")); + _inverter->sendPowerControlRequest(*_oTargetPowerState); + return true; + } + + _oTargetPowerState = std::nullopt; // target power state reached + return false; + }; - // enable power production only after setting the desired limit, - // such that an older, greater limit will not cause power spikes. - if (enablePowerProduction && !inverter->isProducing()) { - MessageOutput.println("[DPL::commitPowerLimit] Starting up inverter..."); - inverter->sendPowerControlRequest(true); + // we use a lambda function here to be able to use return statements, + // which allows to avoid if-else-indentions and improves code readability + auto updateLimit = [this]() -> bool { + // no limit update requested at all + if (!_oTargetPowerLimitWatts.has_value()) { return false; } + + // wait for pending limit command(s) to complete + auto lastLimitCommandState = _inverter->SystemConfigPara()->getLastLimitCommandSuccess(); + if (CMD_PENDING == lastLimitCommandState) { + announceStatus(Status::InverterLimitPending); + return true; + } + + auto maxPower = _inverter->DevInfo()->getMaxPower(); + auto newRelativeLimit = static_cast(*_oTargetPowerLimitWatts * 100) / maxPower; + + // if no limit command is pending, the SystemConfigPara does report the + // current limit, as the answer by the inverter to a limit command is + // the canonical source that updates the known current limit. + auto currentRelativeLimit = _inverter->SystemConfigPara()->getLimitPercent(); + + // we assume having exclusive control over the inverter. if the last + // limit command was successful and sent after we started the last + // update cycle, we should assume *our* requested limit was set. + uint32_t lastLimitCommandMillis = _inverter->SystemConfigPara()->getLastUpdateCommand(); + if ((lastLimitCommandMillis - *_oUpdateStartMillis) < halfOfAllMillis && + CMD_OK == lastLimitCommandState) { + MessageOutput.printf("[DPL:updateInverter] actual limit is %.1f %% " + "(%.0f W respectively), effective %d ms after update started, " + "requested were %.1f %%\r\n", + currentRelativeLimit, + (currentRelativeLimit * maxPower / 100), + (lastLimitCommandMillis - *_oUpdateStartMillis), + newRelativeLimit); + + if (std::abs(newRelativeLimit - currentRelativeLimit) > 2.0) { + 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); + } + + _oTargetPowerLimitWatts = std::nullopt; + return false; + } + + MessageOutput.printf("[DPL::updateInverter] sending limit of %.1f %% " + "(%.0f W respectively), max output is %d W\r\n", + newRelativeLimit, (newRelativeLimit * maxPower / 100), maxPower); + + _inverter->sendActivePowerControlRequest(static_cast(newRelativeLimit), + PowerLimitControlType::RelativNonPersistent); + + _lastRequestedPowerLimit = *_oTargetPowerLimitWatts; + return true; + }; + + // disable power production as soon as possible. + // setting the power limit is less important once the inverter is off. + if (switchPowerState(false)) { return true; } + + if (updateLimit()) { return true; } + + // enable power production only after setting the desired limit + if (switchPowerState(true)) { return true; } + + return reset(); +} + +/** + * scale the desired inverter limit such that the actual inverter AC output is + * close to the desired power limit, even if some input channels are producing + * less than the limit allows. this happens because the inverter seems to split + * the total power limit equally among all MPPTs (not inputs; some inputs share + * the same MPPT on some models). + * + * TODO(schlimmchen): the current implementation is broken and is in need of + * refactoring. currently it only works for inverters that provide one MPPT for + * each input. it also does not work as expected if any input produces *some* + * energy, but is limited by its respective solar input. + */ +static int32_t scalePowerLimit(std::shared_ptr inverter, int32_t newLimit, int32_t currentLimitWatts) +{ + // prevent scaling if inverter is not producing, as input channels are not + // producing energy and hence are detected as not-producing, causing + // unreasonable scaling. + if (!inverter->isProducing()) { return newLimit; } + + std::list dcChnls = inverter->Statistics()->getChannelsByType(TYPE_DC); + size_t dcTotalChnls = dcChnls.size(); + + // according to the upstream projects README (table with supported devs), + // every 2 channel inverter has 2 MPPTs. then there are the HM*S* 4 channel + // models which have 4 MPPTs. all others have a different number of MPPTs + // than inputs. those are not supported by the current scaling mechanism. + bool supported = dcTotalChnls == 2; + supported |= dcTotalChnls == 4 && HMS_4CH::isValidSerial(inverter->serial()); + if (!supported) { return newLimit; } + + // test for a reasonable power limit that allows us to assume that an input + // channel with little energy is actually not producing, rather than + // producing very little due to the very low limit. + if (currentLimitWatts < dcTotalChnls * 10) { return newLimit; } + + size_t dcProdChnls = 0; + for (auto& c : dcChnls) { + if (inverter->Statistics()->getChannelFieldValue(TYPE_DC, c, FLD_PDC) > 2.0) { + dcProdChnls++; + } } + + 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); } /** - * enforces limits and a hystersis on the requested power limit, after scaling - * the power limit to the ratio of total and producing inverter channels. - * commits the sanitized power limit. returns true if a limit update was - * committed, false otherwise. + * enforces limits on the requested power limit, after scaling the power limit + * to the ratio of total and producing inverter channels. commits the sanitized + * power limit. returns true if an inverter update was committed, false + * otherwise. */ bool PowerLimiterClass::setNewPowerLimit(std::shared_ptr inverter, int32_t newPowerLimit) { - CONFIG_T& config = Configuration.get(); + auto const& config = Configuration.get(); - // 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(); + return shutdown(Status::CalculatedLimitBelowMinLimit); } // enforce configured upper power limit int32_t effPowerLimit = std::min(newPowerLimit, config.PowerLimiter.UpperPowerLimit); - // scale the power limit by the amount of all inverter channels devided by - // the amount of producing inverter channels. the inverters limit each of - // the n channels to 1/n of the total power limit. scaling the power limit - // ensures the total inverter output is what we are asking for. - std::list dcChnls = inverter->Statistics()->getChannelsByType(TYPE_DC); - int dcProdChnls = 0, dcTotalChnls = dcChnls.size(); - for (auto& c : dcChnls) { - if (inverter->Statistics()->getChannelFieldValue(TYPE_DC, c, FLD_PDC) > 2.0) { - dcProdChnls++; - } - } - if ((dcProdChnls > 0) && (dcProdChnls != dcTotalChnls)) { - MessageOutput.printf("[DPL::setNewPowerLimit] %d channels total, %d producing channels, scaling power limit\r\n", - dcTotalChnls, dcProdChnls); - effPowerLimit = round(effPowerLimit * static_cast(dcTotalChnls) / dcProdChnls); - } + // early in the loop we make it a pre-requisite that this + // value is non-zero, so we can assume it to be valid. + auto maxPower = inverter->DevInfo()->getMaxPower(); - effPowerLimit = std::min(effPowerLimit, inverter->DevInfo()->getMaxPower()); + float currentLimitPercent = inverter->SystemConfigPara()->getLimitPercent(); + auto currentLimitAbs = static_cast(currentLimitPercent * maxPower / 100); - // Check if the new value is within the limits of the hysteresis - auto diff = std::abs(effPowerLimit - _lastRequestedPowerLimit); - auto hysteresis = config.PowerLimiter.TargetPowerConsumptionHysteresis; + effPowerLimit = scalePowerLimit(inverter, effPowerLimit, currentLimitAbs); - // (re-)send power limit in case the last was sent a long time ago. avoids - // staleness in case a power limit update was not received by the inverter. - auto ageMillis = millis() - _lastPowerLimitMillis; + effPowerLimit = std::min(effPowerLimit, maxPower); - if (diff < hysteresis && ageMillis < 60 * 1000) { - if (_verboseLogging) { - MessageOutput.printf("[DPL::setNewPowerLimit] requested: %d W, last limit: %d W, diff: %d W, hysteresis: %d W, age: %ld ms\r\n", - newPowerLimit, _lastRequestedPowerLimit, diff, hysteresis, ageMillis); - } - return false; - } + auto diff = std::abs(currentLimitAbs - effPowerLimit); + auto hysteresis = config.PowerLimiter.TargetPowerConsumptionHysteresis; if (_verboseLogging) { - MessageOutput.printf("[DPL::setNewPowerLimit] requested: %d W, (re-)sending limit: %d W\r\n", - newPowerLimit, effPowerLimit); + 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); } - commitPowerLimit(inverter, effPowerLimit, true); - return true; + if (diff > hysteresis) { + _oTargetPowerLimitWatts = effPowerLimit; + } + + _oTargetPowerState = true; + 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() @@ -592,9 +728,8 @@ float PowerLimiterClass::getLoadCorrectedVoltage() CONFIG_T& config = Configuration.get(); - auto channel = static_cast(config.PowerLimiter.InverterChannelId); float acPower = _inverter->Statistics()->getChannelFieldValue(TYPE_AC, CH0, FLD_PAC); - float dcVoltage = _inverter->Statistics()->getChannelFieldValue(TYPE_DC, channel, FLD_UDC); + float dcVoltage = getBatteryVoltage(); if (dcVoltage <= 0.0) { return 0.0; @@ -608,11 +743,14 @@ bool PowerLimiterClass::testThreshold(float socThreshold, float voltThreshold, { CONFIG_T& config = Configuration.get(); - // prefer SoC provided through battery interface - if (config.Battery.Enabled && socThreshold > 0.0 - && Battery.getStats()->isValid() - && Battery.getStats()->getSoCAgeSeconds() < 60) { - return compare(Battery.getStats()->getSoC(), socThreshold); + // prefer SoC provided through battery interface, unless disabled by user + auto stats = Battery.getStats(); + if (!config.PowerLimiter.IgnoreSoc + && config.Battery.Enabled + && socThreshold > 0.0 + && stats->isSoCValid() + && stats->getSoCAgeSeconds() < 60) { + return compare(stats->getSoC(), socThreshold); } // use voltage threshold as fallback @@ -696,12 +834,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/PylontechCanReceiver.cpp b/src/PylontechCanReceiver.cpp index c1b26176b..e19cff599 100644 --- a/src/PylontechCanReceiver.cpp +++ b/src/PylontechCanReceiver.cpp @@ -136,7 +136,7 @@ void PylontechCanReceiver::loop() } case 0x355: { - _stats->setSoC(static_cast(this->readUnsignedInt16(rx_message.data))); + _stats->setSoC(static_cast(this->readUnsignedInt16(rx_message.data)), 0/*precision*/, millis()); _stats->_stateOfHealth = this->readUnsignedInt16(rx_message.data + 2); if (_verboseLogging) { @@ -147,13 +147,13 @@ void PylontechCanReceiver::loop() } case 0x356: { - _stats->_voltage = this->scaleValue(this->readSignedInt16(rx_message.data), 0.01); + _stats->setVoltage(this->scaleValue(this->readSignedInt16(rx_message.data), 0.01), millis()); _stats->_current = this->scaleValue(this->readSignedInt16(rx_message.data + 2), 0.1); _stats->_temperature = this->scaleValue(this->readSignedInt16(rx_message.data + 4), 0.1); if (_verboseLogging) { MessageOutput.printf("[Pylontech] voltage: %f current: %f temperature: %f\n", - _stats->_voltage, _stats->_current, _stats->_temperature); + _stats->getVoltage(), _stats->_current, _stats->_temperature); } break; } @@ -282,12 +282,12 @@ void PylontechCanReceiver::dummyData() }; _stats->setManufacturer("Pylontech US3000C"); - _stats->setSoC(42); + _stats->setSoC(42, 0/*precision*/, millis()); _stats->_chargeVoltage = dummyFloat(50); _stats->_chargeCurrentLimitation = dummyFloat(33); _stats->_dischargeCurrentLimitation = dummyFloat(12); _stats->_stateOfHealth = 99; - _stats->_voltage = 48.67; + _stats->setVoltage(48.67, millis()); _stats->_current = dummyFloat(-1); _stats->_temperature = dummyFloat(20); diff --git a/src/Utils.cpp b/src/Utils.cpp index 386e0ed1f..7ad072938 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -6,7 +6,9 @@ #include "Display_Graphic.h" #include "Led_Single.h" #include "MessageOutput.h" +#include "PinMapping.h" #include +#include uint32_t Utils::getChipId() { @@ -76,3 +78,17 @@ bool Utils::checkJsonAlloc(const DynamicJsonDocument& doc, const char* function, return true; } + +/// @brief Remove all files but the PINMAPPING_FILENAME +void Utils::removeAllFiles() +{ + auto root = LittleFS.open("/"); + auto file = root.getNextFileName(); + + while (file != "") { + if (file != PINMAPPING_FILENAME) { + LittleFS.remove(file); + } + file = root.getNextFileName(); + } +} diff --git a/src/VictronMppt.cpp b/src/VictronMppt.cpp index fd1073a78..c4dd0bd5a 100644 --- a/src/VictronMppt.cpp +++ b/src/VictronMppt.cpp @@ -137,3 +137,16 @@ double VictronMpptClass::getYieldDay() const return sum; } + +double VictronMpptClass::getOutputVoltage() const +{ + double min = -1; + + for (const auto& upController : _controllers) { + double volts = upController->getData()->V; + if (min == -1) { min = volts; } + min = std::min(min, volts); + } + + return min; +} diff --git a/src/WebApi_battery.cpp b/src/WebApi_battery.cpp index b96eb2cf6..9e2230c4e 100644 --- a/src/WebApi_battery.cpp +++ b/src/WebApi_battery.cpp @@ -39,7 +39,8 @@ void WebApiBatteryClass::onStatus(AsyncWebServerRequest* request) root["provider"] = config.Battery.Provider; root["jkbms_interface"] = config.Battery.JkBmsInterface; root["jkbms_polling_interval"] = config.Battery.JkBmsPollingInterval; - root["mqtt_topic"] = config.Battery.MqttTopic; + root["mqtt_soc_topic"] = config.Battery.MqttSocTopic; + root["mqtt_voltage_topic"] = config.Battery.MqttVoltageTopic; response->setLength(); request->send(response); @@ -103,8 +104,9 @@ void WebApiBatteryClass::onAdminPost(AsyncWebServerRequest* request) config.Battery.Provider = root["provider"].as(); config.Battery.JkBmsInterface = root["jkbms_interface"].as(); config.Battery.JkBmsPollingInterval = root["jkbms_polling_interval"].as(); - strlcpy(config.Battery.MqttTopic, root["mqtt_topic"].as().c_str(), sizeof(config.Battery.MqttTopic)); - + strlcpy(config.Battery.MqttSocTopic, root["mqtt_soc_topic"].as().c_str(), sizeof(config.Battery.MqttSocTopic)); + strlcpy(config.Battery.MqttVoltageTopic, root["mqtt_voltage_topic"].as().c_str(), sizeof(config.Battery.MqttVoltageTopic)); + WebApi.writeConfig(retMsg); response->setLength(); diff --git a/src/WebApi_config.cpp b/src/WebApi_config.cpp index 3372e4f45..29f353192 100644 --- a/src/WebApi_config.cpp +++ b/src/WebApi_config.cpp @@ -19,12 +19,10 @@ void WebApiConfigClass::init(AsyncWebServer& server, Scheduler& scheduler) using std::placeholders::_5; using std::placeholders::_6; - _server = &server; - - _server->on("/api/config/get", HTTP_GET, std::bind(&WebApiConfigClass::onConfigGet, this, _1)); - _server->on("/api/config/delete", HTTP_POST, std::bind(&WebApiConfigClass::onConfigDelete, this, _1)); - _server->on("/api/config/list", HTTP_GET, std::bind(&WebApiConfigClass::onConfigListGet, this, _1)); - _server->on("/api/config/upload", HTTP_POST, + server.on("/api/config/get", HTTP_GET, std::bind(&WebApiConfigClass::onConfigGet, this, _1)); + server.on("/api/config/delete", HTTP_POST, std::bind(&WebApiConfigClass::onConfigDelete, this, _1)); + server.on("/api/config/list", HTTP_GET, std::bind(&WebApiConfigClass::onConfigListGet, this, _1)); + server.on("/api/config/upload", HTTP_POST, std::bind(&WebApiConfigClass::onConfigUploadFinish, this, _1), std::bind(&WebApiConfigClass::onConfigUpload, this, _1, _2, _3, _4, _5, _6)); } @@ -110,7 +108,7 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request) response->setLength(); request->send(response); - LittleFS.remove(CONFIG_FILENAME); + Utils::removeAllFiles(); Utils::restartDtu(); } diff --git a/src/WebApi_device.cpp b/src/WebApi_device.cpp index 010a539f9..cc08dfaab 100644 --- a/src/WebApi_device.cpp +++ b/src/WebApi_device.cpp @@ -16,10 +16,8 @@ void WebApiDeviceClass::init(AsyncWebServer& server, Scheduler& scheduler) { using std::placeholders::_1; - _server = &server; - - _server->on("/api/device/config", HTTP_GET, std::bind(&WebApiDeviceClass::onDeviceAdminGet, this, _1)); - _server->on("/api/device/config", HTTP_POST, std::bind(&WebApiDeviceClass::onDeviceAdminPost, this, _1)); + server.on("/api/device/config", HTTP_GET, std::bind(&WebApiDeviceClass::onDeviceAdminGet, this, _1)); + server.on("/api/device/config", HTTP_POST, std::bind(&WebApiDeviceClass::onDeviceAdminPost, this, _1)); } void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request) @@ -184,12 +182,12 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request) config.Led_Single[i].Brightness = min(100, config.Led_Single[i].Brightness); } + Display.setDiagramMode(static_cast(config.Display.Diagram.Mode)); Display.setOrientation(config.Display.Rotation); Display.enablePowerSafe = config.Display.PowerSafe; Display.enableScreensaver = config.Display.ScreenSaver; Display.setContrast(config.Display.Contrast); Display.setLanguage(config.Display.Language); - Display.setDiagramMode(static_cast(config.Display.Diagram.Mode)); Display.Diagram().updatePeriod(); WebApi.writeConfig(retMsg); diff --git a/src/WebApi_devinfo.cpp b/src/WebApi_devinfo.cpp index a27cb31ee..212a7f7d5 100644 --- a/src/WebApi_devinfo.cpp +++ b/src/WebApi_devinfo.cpp @@ -12,9 +12,7 @@ void WebApiDevInfoClass::init(AsyncWebServer& server, Scheduler& scheduler) { using std::placeholders::_1; - _server = &server; - - _server->on("/api/devinfo/status", HTTP_GET, std::bind(&WebApiDevInfoClass::onDevInfoStatus, this, _1)); + server.on("/api/devinfo/status", HTTP_GET, std::bind(&WebApiDevInfoClass::onDevInfoStatus, this, _1)); } void WebApiDevInfoClass::onDevInfoStatus(AsyncWebServerRequest* request) diff --git a/src/WebApi_dtu.cpp b/src/WebApi_dtu.cpp index adfd411c4..bbdfd0708 100644 --- a/src/WebApi_dtu.cpp +++ b/src/WebApi_dtu.cpp @@ -18,10 +18,8 @@ void WebApiDtuClass::init(AsyncWebServer& server, Scheduler& scheduler) { using std::placeholders::_1; - _server = &server; - - _server->on("/api/dtu/config", HTTP_GET, std::bind(&WebApiDtuClass::onDtuAdminGet, this, _1)); - _server->on("/api/dtu/config", HTTP_POST, std::bind(&WebApiDtuClass::onDtuAdminPost, this, _1)); + server.on("/api/dtu/config", HTTP_GET, std::bind(&WebApiDtuClass::onDtuAdminGet, this, _1)); + server.on("/api/dtu/config", HTTP_POST, std::bind(&WebApiDtuClass::onDtuAdminPost, this, _1)); scheduler.addTask(_applyDataTask); } diff --git a/src/WebApi_eventlog.cpp b/src/WebApi_eventlog.cpp index a92e515e7..51e85affa 100644 --- a/src/WebApi_eventlog.cpp +++ b/src/WebApi_eventlog.cpp @@ -11,9 +11,7 @@ void WebApiEventlogClass::init(AsyncWebServer& server, Scheduler& scheduler) { using std::placeholders::_1; - _server = &server; - - _server->on("/api/eventlog/status", HTTP_GET, std::bind(&WebApiEventlogClass::onEventlogStatus, this, _1)); + server.on("/api/eventlog/status", HTTP_GET, std::bind(&WebApiEventlogClass::onEventlogStatus, this, _1)); } void WebApiEventlogClass::onEventlogStatus(AsyncWebServerRequest* request) diff --git a/src/WebApi_firmware.cpp b/src/WebApi_firmware.cpp index 617fca067..9491f935d 100644 --- a/src/WebApi_firmware.cpp +++ b/src/WebApi_firmware.cpp @@ -19,9 +19,7 @@ void WebApiFirmwareClass::init(AsyncWebServer& server, Scheduler& scheduler) using std::placeholders::_5; using std::placeholders::_6; - _server = &server; - - _server->on("/api/firmware/update", HTTP_POST, + server.on("/api/firmware/update", HTTP_POST, std::bind(&WebApiFirmwareClass::onFirmwareUpdateFinish, this, _1), std::bind(&WebApiFirmwareClass::onFirmwareUpdateUpload, this, _1, _2, _3, _4, _5, _6)); } diff --git a/src/WebApi_gridprofile.cpp b/src/WebApi_gridprofile.cpp index 587f46400..60c340fa0 100644 --- a/src/WebApi_gridprofile.cpp +++ b/src/WebApi_gridprofile.cpp @@ -11,10 +11,8 @@ void WebApiGridProfileClass::init(AsyncWebServer& server, Scheduler& scheduler) { using std::placeholders::_1; - _server = &server; - - _server->on("/api/gridprofile/status", HTTP_GET, std::bind(&WebApiGridProfileClass::onGridProfileStatus, this, _1)); - _server->on("/api/gridprofile/rawdata", HTTP_GET, std::bind(&WebApiGridProfileClass::onGridProfileRawdata, this, _1)); + server.on("/api/gridprofile/status", HTTP_GET, std::bind(&WebApiGridProfileClass::onGridProfileStatus, this, _1)); + server.on("/api/gridprofile/rawdata", HTTP_GET, std::bind(&WebApiGridProfileClass::onGridProfileRawdata, this, _1)); } void WebApiGridProfileClass::onGridProfileStatus(AsyncWebServerRequest* request) diff --git a/src/WebApi_inverter.cpp b/src/WebApi_inverter.cpp index f1bfc2aa5..32a472350 100644 --- a/src/WebApi_inverter.cpp +++ b/src/WebApi_inverter.cpp @@ -16,13 +16,11 @@ void WebApiInverterClass::init(AsyncWebServer& server, Scheduler& scheduler) { using std::placeholders::_1; - _server = &server; - - _server->on("/api/inverter/list", HTTP_GET, std::bind(&WebApiInverterClass::onInverterList, this, _1)); - _server->on("/api/inverter/add", HTTP_POST, std::bind(&WebApiInverterClass::onInverterAdd, this, _1)); - _server->on("/api/inverter/edit", HTTP_POST, std::bind(&WebApiInverterClass::onInverterEdit, this, _1)); - _server->on("/api/inverter/del", HTTP_POST, std::bind(&WebApiInverterClass::onInverterDelete, this, _1)); - _server->on("/api/inverter/order", HTTP_POST, std::bind(&WebApiInverterClass::onInverterOrder, this, _1)); + server.on("/api/inverter/list", HTTP_GET, std::bind(&WebApiInverterClass::onInverterList, this, _1)); + server.on("/api/inverter/add", HTTP_POST, std::bind(&WebApiInverterClass::onInverterAdd, this, _1)); + server.on("/api/inverter/edit", HTTP_POST, std::bind(&WebApiInverterClass::onInverterEdit, this, _1)); + server.on("/api/inverter/del", HTTP_POST, std::bind(&WebApiInverterClass::onInverterDelete, this, _1)); + server.on("/api/inverter/order", HTTP_POST, std::bind(&WebApiInverterClass::onInverterOrder, this, _1)); } void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request) diff --git a/src/WebApi_limit.cpp b/src/WebApi_limit.cpp index be8e1202f..1d9c111a5 100644 --- a/src/WebApi_limit.cpp +++ b/src/WebApi_limit.cpp @@ -14,10 +14,8 @@ void WebApiLimitClass::init(AsyncWebServer& server, Scheduler& scheduler) { using std::placeholders::_1; - _server = &server; - - _server->on("/api/limit/status", HTTP_GET, std::bind(&WebApiLimitClass::onLimitStatus, this, _1)); - _server->on("/api/limit/config", HTTP_POST, std::bind(&WebApiLimitClass::onLimitPost, this, _1)); + server.on("/api/limit/status", HTTP_GET, std::bind(&WebApiLimitClass::onLimitStatus, this, _1)); + server.on("/api/limit/config", HTTP_POST, std::bind(&WebApiLimitClass::onLimitPost, this, _1)); } void WebApiLimitClass::onLimitStatus(AsyncWebServerRequest* request) diff --git a/src/WebApi_maintenance.cpp b/src/WebApi_maintenance.cpp index 922b0ba09..ba257efa8 100644 --- a/src/WebApi_maintenance.cpp +++ b/src/WebApi_maintenance.cpp @@ -13,9 +13,7 @@ void WebApiMaintenanceClass::init(AsyncWebServer& server, Scheduler& scheduler) { using std::placeholders::_1; - _server = &server; - - _server->on("/api/maintenance/reboot", HTTP_POST, std::bind(&WebApiMaintenanceClass::onRebootPost, this, _1)); + server.on("/api/maintenance/reboot", HTTP_POST, std::bind(&WebApiMaintenanceClass::onRebootPost, this, _1)); } void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request) diff --git a/src/WebApi_mqtt.cpp b/src/WebApi_mqtt.cpp index bdb19ef01..9e7411bfe 100644 --- a/src/WebApi_mqtt.cpp +++ b/src/WebApi_mqtt.cpp @@ -19,11 +19,9 @@ void WebApiMqttClass::init(AsyncWebServer& server, Scheduler& scheduler) { using std::placeholders::_1; - _server = &server; - - _server->on("/api/mqtt/status", HTTP_GET, std::bind(&WebApiMqttClass::onMqttStatus, this, _1)); - _server->on("/api/mqtt/config", HTTP_GET, std::bind(&WebApiMqttClass::onMqttAdminGet, this, _1)); - _server->on("/api/mqtt/config", HTTP_POST, std::bind(&WebApiMqttClass::onMqttAdminPost, this, _1)); + server.on("/api/mqtt/status", HTTP_GET, std::bind(&WebApiMqttClass::onMqttStatus, this, _1)); + server.on("/api/mqtt/config", HTTP_GET, std::bind(&WebApiMqttClass::onMqttAdminGet, this, _1)); + server.on("/api/mqtt/config", HTTP_POST, std::bind(&WebApiMqttClass::onMqttAdminPost, this, _1)); } void WebApiMqttClass::onMqttStatus(AsyncWebServerRequest* request) @@ -85,7 +83,7 @@ void WebApiMqttClass::onMqttAdminGet(AsyncWebServerRequest* request) root["mqtt_client_cert"] = config.Mqtt.Tls.ClientCert; root["mqtt_client_key"] = config.Mqtt.Tls.ClientKey; root["mqtt_lwt_topic"] = config.Mqtt.Lwt.Topic; - root["mqtt_lwt_online"] = config.Mqtt.CleanSession; + root["mqtt_lwt_online"] = config.Mqtt.Lwt.Value_Online;; root["mqtt_lwt_offline"] = config.Mqtt.Lwt.Value_Offline; root["mqtt_lwt_qos"] = config.Mqtt.Lwt.Qos; root["mqtt_publish_interval"] = config.Mqtt.PublishInterval; diff --git a/src/WebApi_network.cpp b/src/WebApi_network.cpp index ba9980539..12f637adc 100644 --- a/src/WebApi_network.cpp +++ b/src/WebApi_network.cpp @@ -14,11 +14,9 @@ void WebApiNetworkClass::init(AsyncWebServer& server, Scheduler& scheduler) { using std::placeholders::_1; - _server = &server; - - _server->on("/api/network/status", HTTP_GET, std::bind(&WebApiNetworkClass::onNetworkStatus, this, _1)); - _server->on("/api/network/config", HTTP_GET, std::bind(&WebApiNetworkClass::onNetworkAdminGet, this, _1)); - _server->on("/api/network/config", HTTP_POST, std::bind(&WebApiNetworkClass::onNetworkAdminPost, this, _1)); + server.on("/api/network/status", HTTP_GET, std::bind(&WebApiNetworkClass::onNetworkStatus, this, _1)); + server.on("/api/network/config", HTTP_GET, std::bind(&WebApiNetworkClass::onNetworkAdminGet, this, _1)); + server.on("/api/network/config", HTTP_POST, std::bind(&WebApiNetworkClass::onNetworkAdminPost, this, _1)); } void WebApiNetworkClass::onNetworkStatus(AsyncWebServerRequest* request) diff --git a/src/WebApi_ntp.cpp b/src/WebApi_ntp.cpp index e0bcd6992..02bbfb105 100644 --- a/src/WebApi_ntp.cpp +++ b/src/WebApi_ntp.cpp @@ -15,13 +15,11 @@ void WebApiNtpClass::init(AsyncWebServer& server, Scheduler& scheduler) { using std::placeholders::_1; - _server = &server; - - _server->on("/api/ntp/status", HTTP_GET, std::bind(&WebApiNtpClass::onNtpStatus, this, _1)); - _server->on("/api/ntp/config", HTTP_GET, std::bind(&WebApiNtpClass::onNtpAdminGet, this, _1)); - _server->on("/api/ntp/config", HTTP_POST, std::bind(&WebApiNtpClass::onNtpAdminPost, this, _1)); - _server->on("/api/ntp/time", HTTP_GET, std::bind(&WebApiNtpClass::onNtpTimeGet, this, _1)); - _server->on("/api/ntp/time", HTTP_POST, std::bind(&WebApiNtpClass::onNtpTimePost, this, _1)); + server.on("/api/ntp/status", HTTP_GET, std::bind(&WebApiNtpClass::onNtpStatus, this, _1)); + server.on("/api/ntp/config", HTTP_GET, std::bind(&WebApiNtpClass::onNtpAdminGet, this, _1)); + server.on("/api/ntp/config", HTTP_POST, std::bind(&WebApiNtpClass::onNtpAdminPost, this, _1)); + server.on("/api/ntp/time", HTTP_GET, std::bind(&WebApiNtpClass::onNtpTimeGet, this, _1)); + server.on("/api/ntp/time", HTTP_POST, std::bind(&WebApiNtpClass::onNtpTimePost, this, _1)); } void WebApiNtpClass::onNtpStatus(AsyncWebServerRequest* request) diff --git a/src/WebApi_power.cpp b/src/WebApi_power.cpp index 54fc664ef..b51967894 100644 --- a/src/WebApi_power.cpp +++ b/src/WebApi_power.cpp @@ -12,10 +12,8 @@ void WebApiPowerClass::init(AsyncWebServer& server, Scheduler& scheduler) { using std::placeholders::_1; - _server = &server; - - _server->on("/api/power/status", HTTP_GET, std::bind(&WebApiPowerClass::onPowerStatus, this, _1)); - _server->on("/api/power/config", HTTP_POST, std::bind(&WebApiPowerClass::onPowerPost, this, _1)); + server.on("/api/power/status", HTTP_GET, std::bind(&WebApiPowerClass::onPowerStatus, this, _1)); + server.on("/api/power/config", HTTP_POST, std::bind(&WebApiPowerClass::onPowerPost, this, _1)); } void WebApiPowerClass::onPowerStatus(AsyncWebServerRequest* request) diff --git a/src/WebApi_powerlimiter.cpp b/src/WebApi_powerlimiter.cpp index 25cb42e22..3a7d827a2 100644 --- a/src/WebApi_powerlimiter.cpp +++ b/src/WebApi_powerlimiter.cpp @@ -39,12 +39,14 @@ void WebApiPowerLimiterClass::onStatus(AsyncWebServerRequest* request) root["solar_passthrough_losses"] = config.PowerLimiter.SolarPassThroughLosses; root["battery_drain_strategy"] = config.PowerLimiter.BatteryDrainStategy; root["is_inverter_behind_powermeter"] = config.PowerLimiter.IsInverterBehindPowerMeter; + root["is_inverter_solar_powered"] = config.PowerLimiter.IsInverterSolarPowered; root["inverter_id"] = 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; root["lower_power_limit"] = config.PowerLimiter.LowerPowerLimit; root["upper_power_limit"] = config.PowerLimiter.UpperPowerLimit; + root["ignore_soc"] = config.PowerLimiter.IgnoreSoc; root["battery_soc_start_threshold"] = config.PowerLimiter.BatterySocStartThreshold; root["battery_soc_stop_threshold"] = config.PowerLimiter.BatterySocStopThreshold; root["voltage_start_threshold"] = static_cast(config.PowerLimiter.VoltageStartThreshold * 100 +0.5) / 100.0; @@ -127,12 +129,14 @@ void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request) config.PowerLimiter.SolarPassThroughLosses = root["solar_passthrough_losses"].as(); config.PowerLimiter.BatteryDrainStategy= root["battery_drain_strategy"].as(); config.PowerLimiter.IsInverterBehindPowerMeter = root["is_inverter_behind_powermeter"].as(); + config.PowerLimiter.IsInverterSolarPowered = root["is_inverter_solar_powered"].as(); config.PowerLimiter.InverterId = root["inverter_id"].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(); config.PowerLimiter.VoltageStartThreshold = root["voltage_start_threshold"].as(); diff --git a/src/WebApi_prometheus.cpp b/src/WebApi_prometheus.cpp index 275e568b9..8e6be8c90 100644 --- a/src/WebApi_prometheus.cpp +++ b/src/WebApi_prometheus.cpp @@ -15,9 +15,7 @@ void WebApiPrometheusClass::init(AsyncWebServer& server, Scheduler& scheduler) { using std::placeholders::_1; - _server = &server; - - _server->on("/api/prometheus/metrics", HTTP_GET, std::bind(&WebApiPrometheusClass::onPrometheusMetricsGet, this, _1)); + server.on("/api/prometheus/metrics", HTTP_GET, std::bind(&WebApiPrometheusClass::onPrometheusMetricsGet, this, _1)); } void WebApiPrometheusClass::onPrometheusMetricsGet(AsyncWebServerRequest* request) @@ -100,7 +98,7 @@ void WebApiPrometheusClass::onPrometheusMetricsGet(AsyncWebServerRequest* reques for (auto& c : inv->Statistics()->getChannelsByType(t)) { addPanelInfo(stream, serial, i, inv, t, c); for (uint8_t f = 0; f < sizeof(_publishFields) / sizeof(_publishFields[0]); f++) { - if (t == TYPE_AC && _publishFields[f].field == FLD_PDC) { + if (t == TYPE_INV && _publishFields[f].field == FLD_PDC) { addField(stream, serial, i, inv, t, c, _publishFields[f].field, _metricTypes[_publishFields[f].type], "PowerDC"); } else { addField(stream, serial, i, inv, t, c, _publishFields[f].field, _metricTypes[_publishFields[f].type]); diff --git a/src/WebApi_security.cpp b/src/WebApi_security.cpp index 205196812..b95ebb299 100644 --- a/src/WebApi_security.cpp +++ b/src/WebApi_security.cpp @@ -13,11 +13,9 @@ void WebApiSecurityClass::init(AsyncWebServer& server, Scheduler& scheduler) { using std::placeholders::_1; - _server = &server; - - _server->on("/api/security/config", HTTP_GET, std::bind(&WebApiSecurityClass::onSecurityGet, this, _1)); - _server->on("/api/security/config", HTTP_POST, std::bind(&WebApiSecurityClass::onSecurityPost, this, _1)); - _server->on("/api/security/authenticate", HTTP_GET, std::bind(&WebApiSecurityClass::onAuthenticateGet, this, _1)); + server.on("/api/security/config", HTTP_GET, std::bind(&WebApiSecurityClass::onSecurityGet, this, _1)); + server.on("/api/security/config", HTTP_POST, std::bind(&WebApiSecurityClass::onSecurityPost, this, _1)); + server.on("/api/security/authenticate", HTTP_GET, std::bind(&WebApiSecurityClass::onAuthenticateGet, this, _1)); } void WebApiSecurityClass::onSecurityGet(AsyncWebServerRequest* request) diff --git a/src/WebApi_sysstatus.cpp b/src/WebApi_sysstatus.cpp index 9c8223760..b8c366b35 100644 --- a/src/WebApi_sysstatus.cpp +++ b/src/WebApi_sysstatus.cpp @@ -24,9 +24,7 @@ void WebApiSysstatusClass::init(AsyncWebServer& server, Scheduler& scheduler) { using std::placeholders::_1; - _server = &server; - - _server->on("/api/system/status", HTTP_GET, std::bind(&WebApiSysstatusClass::onSystemStatus, this, _1)); + server.on("/api/system/status", HTTP_GET, std::bind(&WebApiSysstatusClass::onSystemStatus, this, _1)); } void WebApiSysstatusClass::onSystemStatus(AsyncWebServerRequest* request) @@ -47,6 +45,8 @@ void WebApiSysstatusClass::onSystemStatus(AsyncWebServerRequest* request) root["heap_used"] = ESP.getHeapSize() - ESP.getFreeHeap(); root["heap_max_block"] = ESP.getMaxAllocHeap(); root["heap_min_free"] = ESP.getMinFreeHeap(); + root["psram_total"] = ESP.getPsramSize(); + root["psram_used"] = ESP.getPsramSize() - ESP.getFreePsram(); root["sketch_total"] = ESP.getFreeSketchSpace(); root["sketch_used"] = ESP.getSketchSize(); root["littlefs_total"] = LittleFS.totalBytes(); diff --git a/src/WebApi_webapp.cpp b/src/WebApi_webapp.cpp index 9203505b9..b8b813853 100644 --- a/src/WebApi_webapp.cpp +++ b/src/WebApi_webapp.cpp @@ -3,6 +3,7 @@ * Copyright (C) 2022-2024 Thomas Basler and others */ #include "WebApi_webapp.h" +#include extern const uint8_t file_index_html_start[] asm("_binary_webapp_dist_index_html_gz_start"); extern const uint8_t file_favicon_ico_start[] asm("_binary_webapp_dist_favicon_ico_start"); @@ -18,79 +19,78 @@ extern const uint8_t file_zones_json_end[] asm("_binary_webapp_dist_zones_json_g extern const uint8_t file_app_js_end[] asm("_binary_webapp_dist_js_app_js_gz_end"); extern const uint8_t file_site_webmanifest_end[] asm("_binary_webapp_dist_site_webmanifest_end"); -#ifdef AUTO_GIT_HASH -#define ETAG_HTTP_HEADER_VAL "\"" AUTO_GIT_HASH "\"" // ETag value must be between quotes -#endif +void WebApiWebappClass::responseBinaryDataWithETagCache(AsyncWebServerRequest *request, const String &contentType, const String &contentEncoding, const uint8_t *content, size_t len) +{ + auto md5 = MD5Builder(); + md5.begin(); + md5.add(const_cast(content), len); + md5.calculate(); + + String expectedEtag; + expectedEtag = "\""; + expectedEtag += md5.toString(); + expectedEtag += "\""; + + bool eTagMatch = false; + if (request->hasHeader("If-None-Match")) { + const AsyncWebHeader* h = request->getHeader("If-None-Match"); + eTagMatch = h->value().equals(expectedEtag); + } + + // begin response 200 or 304 + AsyncWebServerResponse* response; + if (eTagMatch) { + response = request->beginResponse(304); + } else { + response = request->beginResponse_P(200, contentType, content, len); + if (contentEncoding.length() > 0) { + response->addHeader("Content-Encoding", contentEncoding); + } + } + + // HTTP requires cache headers in 200 and 304 to be identical + response->addHeader("Cache-Control", "public, must-revalidate"); + response->addHeader("ETag", expectedEtag); + + request->send(response); +} void WebApiWebappClass::init(AsyncWebServer& server, Scheduler& scheduler) { - _server = &server; + /* + We don't validate the request header "Accept-Encoding" if gzip compression is supported! + We just have the gzipped data available - so we ship them! + */ - _server->on("/", HTTP_GET, [](AsyncWebServerRequest* request) { - AsyncWebServerResponse* response = request->beginResponse_P(200, "text/html", file_index_html_start, file_index_html_end - file_index_html_start); - response->addHeader("Content-Encoding", "gzip"); - request->send(response); + server.on("/", HTTP_GET, [&](AsyncWebServerRequest* request) { + responseBinaryDataWithETagCache(request, "text/html", "gzip", file_index_html_start, file_index_html_end - file_index_html_start); }); - _server->onNotFound([](AsyncWebServerRequest* request) { - AsyncWebServerResponse* response = request->beginResponse_P(200, "text/html", file_index_html_start, file_index_html_end - file_index_html_start); - response->addHeader("Content-Encoding", "gzip"); - request->send(response); + server.onNotFound([&](AsyncWebServerRequest* request) { + responseBinaryDataWithETagCache(request, "text/html", "gzip", file_index_html_start, file_index_html_end - file_index_html_start); }); - _server->on("/index.html", HTTP_GET, [](AsyncWebServerRequest* request) { - AsyncWebServerResponse* response = request->beginResponse_P(200, "text/html", file_index_html_start, file_index_html_end - file_index_html_start); - response->addHeader("Content-Encoding", "gzip"); - request->send(response); + server.on("/index.html", HTTP_GET, [&](AsyncWebServerRequest* request) { + responseBinaryDataWithETagCache(request, "text/html", "gzip", file_index_html_start, file_index_html_end - file_index_html_start); }); - _server->on("/favicon.ico", HTTP_GET, [](AsyncWebServerRequest* request) { - AsyncWebServerResponse* response = request->beginResponse_P(200, "image/x-icon", file_favicon_ico_start, file_favicon_ico_end - file_favicon_ico_start); - request->send(response); + server.on("/favicon.ico", HTTP_GET, [&](AsyncWebServerRequest* request) { + responseBinaryDataWithETagCache(request, "image/x-icon", "", file_favicon_ico_start, file_favicon_ico_end - file_favicon_ico_start); }); - _server->on("/favicon.png", HTTP_GET, [](AsyncWebServerRequest* request) { - AsyncWebServerResponse* response = request->beginResponse_P(200, "image/png", file_favicon_png_start, file_favicon_png_end - file_favicon_png_start); - request->send(response); + server.on("/favicon.png", HTTP_GET, [&](AsyncWebServerRequest* request) { + responseBinaryDataWithETagCache(request, "image/png", "", file_favicon_png_start, file_favicon_png_end - file_favicon_png_start); }); - _server->on("/zones.json", HTTP_GET, [](AsyncWebServerRequest* request) { - AsyncWebServerResponse* response = request->beginResponse_P(200, "application/json", file_zones_json_start, file_zones_json_end - file_zones_json_start); - response->addHeader("Content-Encoding", "gzip"); - request->send(response); + server.on("/zones.json", HTTP_GET, [&](AsyncWebServerRequest* request) { + responseBinaryDataWithETagCache(request, "application/json", "gzip", file_zones_json_start, file_zones_json_end - file_zones_json_start); }); - _server->on("/site.webmanifest", HTTP_GET, [](AsyncWebServerRequest* request) { - AsyncWebServerResponse* response = request->beginResponse_P(200, "application/json", file_site_webmanifest_start, file_site_webmanifest_end - file_site_webmanifest_start); - request->send(response); + server.on("/site.webmanifest", HTTP_GET, [&](AsyncWebServerRequest* request) { + responseBinaryDataWithETagCache(request, "application/json", "", file_site_webmanifest_start, file_site_webmanifest_end - file_site_webmanifest_start); }); - _server->on("/js/app.js", HTTP_GET, [](AsyncWebServerRequest* request) { -#ifdef ETAG_HTTP_HEADER_VAL - // check client If-None-Match header vs ETag/AUTO_GIT_HASH - bool eTagMatch = false; - if (request->hasHeader("If-None-Match")) { - const AsyncWebHeader* h = request->getHeader("If-None-Match"); - if (strncmp(ETAG_HTTP_HEADER_VAL, h->value().c_str(), strlen(ETAG_HTTP_HEADER_VAL)) == 0) { - eTagMatch = true; - } - } - - // begin response 200 or 304 - AsyncWebServerResponse* response; - if (eTagMatch) { - response = request->beginResponse(304); - } else { - response = request->beginResponse_P(200, "text/javascript", file_app_js_start, file_app_js_end - file_app_js_start); - response->addHeader("Content-Encoding", "gzip"); - } - // HTTP requires cache headers in 200 and 304 to be identical - response->addHeader("Cache-Control", "public, must-revalidate"); - response->addHeader("ETag", ETAG_HTTP_HEADER_VAL); -#else - AsyncWebServerResponse* response = request->beginResponse_P(200, "text/javascript", file_app_js_start, file_app_js_end - file_app_js_start); - response->addHeader("Content-Encoding", "gzip"); -#endif - request->send(response); + server.on("/js/app.js", HTTP_GET, [&](AsyncWebServerRequest* request) { + responseBinaryDataWithETagCache(request, "text/javascript", "gzip", file_app_js_start, file_app_js_end - file_app_js_start); }); } diff --git a/src/WebApi_ws_console.cpp b/src/WebApi_ws_console.cpp index aaca6d895..1f1efcb20 100644 --- a/src/WebApi_ws_console.cpp +++ b/src/WebApi_ws_console.cpp @@ -16,8 +16,7 @@ WebApiWsConsoleClass::WebApiWsConsoleClass() void WebApiWsConsoleClass::init(AsyncWebServer& server, Scheduler& scheduler) { - _server = &server; - _server->addHandler(&_ws); + server.addHandler(&_ws); MessageOutput.register_ws_output(&_ws); scheduler.addTask(_wsCleanupTask); diff --git a/src/WebApi_ws_live.cpp b/src/WebApi_ws_live.cpp index 867b7f8f9..bcf6a1e4e 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -3,7 +3,6 @@ * Copyright (C) 2022-2024 Thomas Basler and others */ #include "WebApi_ws_live.h" -#include "Configuration.h" #include "Datastore.h" #include "MessageOutput.h" #include "Utils.h" @@ -31,10 +30,9 @@ void WebApiWsLiveClass::init(AsyncWebServer& server, Scheduler& scheduler) using std::placeholders::_5; using std::placeholders::_6; - _server = &server; - _server->on("/api/livedata/status", HTTP_GET, std::bind(&WebApiWsLiveClass::onLivedataStatus, this, _1)); + server.on("/api/livedata/status", HTTP_GET, std::bind(&WebApiWsLiveClass::onLivedataStatus, this, _1)); - _server->addHandler(&_ws); + server.addHandler(&_ws); _ws.onEvent(std::bind(&WebApiWsLiveClass::onWebsocketEvent, this, _1, _2, _3, _4, _5, _6)); scheduler.addTask(_wsCleanupTask); @@ -56,49 +54,74 @@ void WebApiWsLiveClass::wsCleanupTaskCb() } } -void WebApiWsLiveClass::sendDataTaskCb() +void WebApiWsLiveClass::generateOnBatteryJsonResponse(JsonVariant& root, bool all) { - // do nothing if no WS client is connected - if (_ws.count() == 0) { - return; - } + auto constexpr halfOfAllMillis = std::numeric_limits::max() / 2; - uint32_t maxTimeStamp = 0; - for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { - auto inv = Hoymiles.getInverterByPos(i); - maxTimeStamp = std::max(maxTimeStamp, inv->Statistics()->getLastUpdate()); + if (all || (millis() - _lastPublishVictron) > VictronMppt.getDataAgeMillis()) { + JsonObject vedirectObj = root.createNestedObject("vedirect"); + vedirectObj["enabled"] = Configuration.get().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(); } } - // Update on every inverter change or at least after 10 seconds - if (millis() - _lastWsPublish > (10 * 1000) || (maxTimeStamp != _newestInverterTimestamp)) { + 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); - try { - std::lock_guard lock(_mutex); - DynamicJsonDocument root(4200 * INV_MAX_COUNT); - if (Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { - JsonVariant var = root; - generateJsonResponse(var); + if (!all) { _lastPublishHuawei = millis(); } + } - String buffer; - serializeJson(root, buffer); + 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); - _ws.textAll(buffer); - _newestInverterTimestamp = maxTimeStamp; - } + if (!all) { _lastPublishBattery = millis(); } + } - } catch (const std::bad_alloc& bad_alloc) { - MessageOutput.printf("Calling /api/livedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what()); - } catch (const std::exception& exc) { - MessageOutput.printf("Unknown exception in /api/livedata/status. Reason: \"%s\".\r\n", exc.what()); - } + 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); - _lastWsPublish = millis(); + if (!all) { _lastPublishPowerMeter = millis(); } } } -void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root) +void WebApiWsLiveClass::sendOnBatteryStats() { - JsonArray invArray = root.createNestedArray("inverters"); + DynamicJsonDocument root(1024); + if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { return; } + + JsonVariant var = root; + + bool all = (millis() - _lastPublishOnBatteryFull) > 10 * 1000; + if (all) { _lastPublishOnBatteryFull = millis(); } + generateOnBatteryJsonResponse(var, all); + + String buffer; + serializeJson(root, buffer); + + _ws.textAll(buffer); +} + +void WebApiWsLiveClass::sendDataTaskCb() +{ + // do nothing if no WS client is connected + if (_ws.count() == 0) { + return; + } + + sendOnBatteryStats(); // Loop all inverters for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { @@ -107,64 +130,43 @@ void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root) continue; } - JsonObject invObject = invArray.createNestedObject(); - INVERTER_CONFIG_T* inv_cfg = Configuration.getInverterConfig(inv->serial()); - if (inv_cfg == nullptr) { + const uint32_t lastUpdateInternal = inv->Statistics()->getLastUpdateFromInternal(); + if (!((lastUpdateInternal > 0 && lastUpdateInternal > _lastPublishStats[i]) || (millis() - _lastPublishStats[i] > (10 * 1000)))) { continue; } - invObject["serial"] = inv->serialString(); - invObject["name"] = inv->name(); - invObject["order"] = inv_cfg->Order; - invObject["data_age"] = (millis() - inv->Statistics()->getLastUpdate()) / 1000; - invObject["poll_enabled"] = inv->getEnablePolling(); - invObject["reachable"] = inv->isReachable(); - invObject["producing"] = inv->isProducing(); - invObject["limit_relative"] = inv->SystemConfigPara()->getLimitPercent(); - if (inv->DevInfo()->getMaxPower() > 0) { - invObject["limit_absolute"] = inv->SystemConfigPara()->getLimitPercent() * inv->DevInfo()->getMaxPower() / 100.0; - } else { - invObject["limit_absolute"] = -1; - } + _lastPublishStats[i] = millis(); - // Loop all channels - for (auto& t : inv->Statistics()->getChannelTypes()) { - JsonObject chanTypeObj = invObject.createNestedObject(inv->Statistics()->getChannelTypeName(t)); - for (auto& c : inv->Statistics()->getChannelsByType(t)) { - if (t == TYPE_DC) { - chanTypeObj[String(static_cast(c))]["name"]["u"] = inv_cfg->channel[c].Name; - } - addField(chanTypeObj, inv, t, c, FLD_PAC); - addField(chanTypeObj, inv, t, c, FLD_UAC); - addField(chanTypeObj, inv, t, c, FLD_IAC); - if (t == TYPE_AC) { - addField(chanTypeObj, inv, t, c, FLD_PDC, "Power DC"); - } else { - addField(chanTypeObj, inv, t, c, FLD_PDC); - } - addField(chanTypeObj, inv, t, c, FLD_UDC); - addField(chanTypeObj, inv, t, c, FLD_IDC); - addField(chanTypeObj, inv, t, c, FLD_YD); - addField(chanTypeObj, inv, t, c, FLD_YT); - addField(chanTypeObj, inv, t, c, FLD_F); - addField(chanTypeObj, inv, t, c, FLD_T); - addField(chanTypeObj, inv, t, c, FLD_PF); - addField(chanTypeObj, inv, t, c, FLD_Q); - addField(chanTypeObj, inv, t, c, FLD_EFF); - if (t == TYPE_DC && inv->Statistics()->getStringMaxPower(c) > 0) { - addField(chanTypeObj, inv, t, c, FLD_IRR); - chanTypeObj[String(c)][inv->Statistics()->getChannelFieldName(t, c, FLD_IRR)]["max"] = inv->Statistics()->getStringMaxPower(c); - } + try { + std::lock_guard lock(_mutex); + DynamicJsonDocument root(4096); + if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { + continue; } - } + JsonVariant var = root; - if (inv->Statistics()->hasChannelFieldValue(TYPE_INV, CH0, FLD_EVT_LOG)) { - invObject["events"] = inv->EventLog()->getEntryCount(); - } else { - invObject["events"] = -1; + auto invArray = var.createNestedArray("inverters"); + auto invObject = invArray.createNestedObject(); + + generateCommonJsonResponse(var); + generateInverterCommonJsonResponse(invObject, inv); + generateInverterChannelJsonResponse(invObject, inv); + + String buffer; + serializeJson(root, buffer); + + _ws.textAll(buffer); + + } catch (const std::bad_alloc& bad_alloc) { + MessageOutput.printf("Calling /api/livedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what()); + } catch (const std::exception& exc) { + MessageOutput.printf("Unknown exception in /api/livedata/status. Reason: \"%s\".\r\n", exc.what()); } } +} +void WebApiWsLiveClass::generateCommonJsonResponse(JsonVariant& root) +{ JsonObject totalObj = root.createNestedObject("total"); addTotalField(totalObj, "Power", Datastore.getTotalAcPowerEnabled(), "W", Datastore.getTotalAcPowerDigits()); addTotalField(totalObj, "YieldDay", Datastore.getTotalAcYieldDayEnabled(), "Wh", Datastore.getTotalAcYieldDayDigits()); @@ -174,33 +176,74 @@ void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root) struct tm timeinfo; hintObj["time_sync"] = !getLocalTime(&timeinfo, 5); hintObj["radio_problem"] = (Hoymiles.getRadioNrf()->isInitialized() && (!Hoymiles.getRadioNrf()->isConnected() || !Hoymiles.getRadioNrf()->isPVariant())) || (Hoymiles.getRadioCmt()->isInitialized() && (!Hoymiles.getRadioCmt()->isConnected())); - if (!strcmp(Configuration.get().Security.Password, ACCESS_POINT_PASSWORD)) { - hintObj["default_password"] = true; - } else { - hintObj["default_password"] = false; - } + hintObj["default_password"] = strcmp(Configuration.get().Security.Password, ACCESS_POINT_PASSWORD) == 0; +} - JsonObject vedirectObj = root.createNestedObject("vedirect"); - vedirectObj["enabled"] = Configuration.get().Vedirect.Enabled; - JsonObject totalVeObj = vedirectObj.createNestedObject("total"); +void WebApiWsLiveClass::generateInverterCommonJsonResponse(JsonObject& root, std::shared_ptr inv) +{ + const INVERTER_CONFIG_T* inv_cfg = Configuration.getInverterConfig(inv->serial()); + if (inv_cfg == nullptr) { + return; + } - addTotalField(totalVeObj, "Power", VictronMppt.getPanelPowerWatts(), "W", 1); - addTotalField(totalVeObj, "YieldDay", VictronMppt.getYieldDay() * 1000, "Wh", 0); - addTotalField(totalVeObj, "YieldTotal", VictronMppt.getYieldTotal(), "kWh", 2); + root["serial"] = inv->serialString(); + root["name"] = inv->name(); + root["order"] = inv_cfg->Order; + root["data_age"] = (millis() - inv->Statistics()->getLastUpdate()) / 1000; + root["poll_enabled"] = inv->getEnablePolling(); + root["reachable"] = inv->isReachable(); + root["producing"] = inv->isProducing(); + root["limit_relative"] = inv->SystemConfigPara()->getLimitPercent(); + if (inv->DevInfo()->getMaxPower() > 0) { + root["limit_absolute"] = inv->SystemConfigPara()->getLimitPercent() * inv->DevInfo()->getMaxPower() / 100.0; + } else { + root["limit_absolute"] = -1; + } +} - 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); - - JsonObject batteryObj = root.createNestedObject("battery"); - batteryObj["enabled"] = Configuration.get().Battery.Enabled; - addTotalField(batteryObj, "soc", Battery.getStats()->getSoC(), "%", 0); +void WebApiWsLiveClass::generateInverterChannelJsonResponse(JsonObject& root, std::shared_ptr inv) +{ + const INVERTER_CONFIG_T* inv_cfg = Configuration.getInverterConfig(inv->serial()); + if (inv_cfg == nullptr) { + return; + } - JsonObject powerMeterObj = root.createNestedObject("power_meter"); - powerMeterObj["enabled"] = Configuration.get().PowerMeter.Enabled; - addTotalField(powerMeterObj, "Power", PowerMeter.getPowerTotal(false), "W", 1); + // Loop all channels + for (auto& t : inv->Statistics()->getChannelTypes()) { + JsonObject chanTypeObj = root.createNestedObject(inv->Statistics()->getChannelTypeName(t)); + for (auto& c : inv->Statistics()->getChannelsByType(t)) { + if (t == TYPE_DC) { + chanTypeObj[String(static_cast(c))]["name"]["u"] = inv_cfg->channel[c].Name; + } + addField(chanTypeObj, inv, t, c, FLD_PAC); + addField(chanTypeObj, inv, t, c, FLD_UAC); + addField(chanTypeObj, inv, t, c, FLD_IAC); + if (t == TYPE_INV) { + addField(chanTypeObj, inv, t, c, FLD_PDC, "Power DC"); + } else { + addField(chanTypeObj, inv, t, c, FLD_PDC); + } + addField(chanTypeObj, inv, t, c, FLD_UDC); + addField(chanTypeObj, inv, t, c, FLD_IDC); + addField(chanTypeObj, inv, t, c, FLD_YD); + addField(chanTypeObj, inv, t, c, FLD_YT); + addField(chanTypeObj, inv, t, c, FLD_F); + addField(chanTypeObj, inv, t, c, FLD_T); + addField(chanTypeObj, inv, t, c, FLD_PF); + addField(chanTypeObj, inv, t, c, FLD_Q); + addField(chanTypeObj, inv, t, c, FLD_EFF); + if (t == TYPE_DC && inv->Statistics()->getStringMaxPower(c) > 0) { + addField(chanTypeObj, inv, t, c, FLD_IRR); + chanTypeObj[String(c)][inv->Statistics()->getChannelFieldName(t, c, FLD_IRR)]["max"] = inv->Statistics()->getStringMaxPower(c); + } + } + } + if (inv->Statistics()->hasChannelFieldValue(TYPE_INV, CH0, FLD_EVT_LOG)) { + root["events"] = inv->EventLog()->getEntryCount(); + } else { + root["events"] = -1; + } } void WebApiWsLiveClass::addField(JsonObject& root, std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId, String topic) @@ -244,10 +287,40 @@ void WebApiWsLiveClass::onLivedataStatus(AsyncWebServerRequest* request) try { std::lock_guard lock(_mutex); - AsyncJsonResponse* response = new AsyncJsonResponse(false, 4200 * INV_MAX_COUNT); + AsyncJsonResponse* response = new AsyncJsonResponse(false, 4096); auto& root = response->getRoot(); - generateJsonResponse(root); + JsonArray invArray = root.createNestedArray("inverters"); + + uint64_t serial = 0; + if (request->hasParam("inv")) { + String s = request->getParam("inv")->value(); + serial = strtoll(s.c_str(), NULL, 16); + } + + if (serial > 0) { + auto inv = Hoymiles.getInverterBySerial(serial); + if (inv != nullptr) { + JsonObject invObject = invArray.createNestedObject(); + generateInverterCommonJsonResponse(invObject, inv); + generateInverterChannelJsonResponse(invObject, inv); + } + } else { + // Loop all inverters + for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { + auto inv = Hoymiles.getInverterByPos(i); + if (inv == nullptr) { + continue; + } + + JsonObject invObject = invArray.createNestedObject(); + generateInverterCommonJsonResponse(invObject, inv); + } + } + + generateCommonJsonResponse(root); + + generateOnBatteryJsonResponse(root, true); response->setLength(); request->send(response); diff --git a/src/main.cpp b/src/main.cpp index 83a94116f..e0a54c155 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -35,9 +35,13 @@ #include #include #include +#include void setup() { + // Move all dynamic allocations >512byte to psram (if available) + heap_caps_malloc_extmem_enable(512); + // Initialize serial output Serial.begin(SERIAL_BAUDRATE); #if ARDUINO_USB_CDC_ON_BOOT @@ -137,12 +141,12 @@ void setup() pin.display_clk, pin.display_cs, pin.display_reset); + Display.setDiagramMode(static_cast(config.Display.Diagram.Mode)); Display.setOrientation(config.Display.Rotation); Display.enablePowerSafe = config.Display.PowerSafe; Display.enableScreensaver = config.Display.ScreenSaver; Display.setContrast(config.Display.Contrast); Display.setLanguage(config.Display.Language); - Display.setDiagramMode(static_cast(config.Display.Diagram.Mode)); Display.setStartupDisplay(); MessageOutput.println("done"); diff --git a/webapp/package.json b/webapp/package.json index ac787a8e1..418eae1aa 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -18,8 +18,8 @@ "mitt": "^3.0.1", "sortablejs": "^1.15.2", "spark-md5": "^3.0.2", - "vue": "^3.4.15", - "vue-i18n": "^9.9.0", + "vue": "^3.4.19", + "vue-i18n": "^9.9.1", "vue-router": "^4.2.5" }, "devDependencies": { @@ -27,23 +27,23 @@ "@rushstack/eslint-patch": "^1.7.2", "@tsconfig/node18": "^18.2.2", "@types/bootstrap": "^5.2.10", - "@types/node": "^20.11.7", + "@types/node": "^20.11.19", "@types/pulltorefreshjs": "^0.1.7", "@types/sortablejs": "^1.15.7", "@types/spark-md5": "^3.0.4", - "@vitejs/plugin-vue": "^5.0.3", + "@vitejs/plugin-vue": "^5.0.4", "@vue/eslint-config-typescript": "^12.0.0", "@vue/tsconfig": "^0.5.1", "eslint": "^8.56.0", - "eslint-plugin-vue": "^9.20.1", + "eslint-plugin-vue": "^9.21.1", "npm-run-all": "^4.1.5", "pulltorefreshjs": "^0.1.22", - "sass": "^1.70.0", - "terser": "^5.27.0", + "sass": "^1.71.0", + "terser": "^5.27.1", "typescript": "^5.3.3", - "vite": "^5.0.12", + "vite": "^5.1.3", "vite-plugin-compression": "^0.5.1", - "vite-plugin-css-injected-by-js": "^3.3.1", + "vite-plugin-css-injected-by-js": "^3.4.0", "vue-tsc": "^1.8.27" } } diff --git a/webapp/src/components/InverterTotalInfo.vue b/webapp/src/components/InverterTotalInfo.vue index b7c671955..755a870ce 100644 --- a/webapp/src/components/InverterTotalInfo.vue +++ b/webapp/src/components/InverterTotalInfo.vue @@ -123,8 +123,8 @@ export default defineComponent({ totalData: { type: Object as PropType, required: true }, totalVeData: { type: Object as PropType, required: true }, totalBattData: { type: Object as PropType, required: true }, - powerMeterData: { type: Object as PropType, required: true }, - huaweiData: { type: Object as PropType, required: true }, + powerMeterData: { type: Object as PropType, required: true }, + huaweiData: { type: Object as PropType, required: true }, }, }); diff --git a/webapp/src/components/MemoryInfo.vue b/webapp/src/components/MemoryInfo.vue index 132d63ca1..b9153dac6 100644 --- a/webapp/src/components/MemoryInfo.vue +++ b/webapp/src/components/MemoryInfo.vue @@ -14,6 +14,8 @@ +