diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dd88f54c..d8a9b1f6 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" @@ -61,7 +61,7 @@ jobs: run: git fetch --force --tags origin - name: Cache pip - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} @@ -69,13 +69,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 af5e8b79..4ee4b4a8 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 00000000..5de69446 --- /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 00000000..be91f95b --- /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 36eed06a..e4bf4144 100644 --- a/include/BatteryStats.h +++ b/include/BatteryStats.h @@ -17,9 +17,12 @@ class BatteryStats { uint32_t getAgeSeconds() const { return (millis() - _lastUpdate) / 1000; } bool updateAvailable(uint32_t since) const { return _lastUpdate > since; } - 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 cc345643..5fdd21c7 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -213,6 +213,7 @@ struct CONFIG_T { int32_t TargetPowerConsumptionHysteresis; int32_t LowerPowerLimit; int32_t UpperPowerLimit; + bool IgnoreSoc; uint32_t BatterySocStartThreshold; uint32_t BatterySocStopThreshold; float VoltageStartThreshold; @@ -230,7 +231,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 83ff412d..61df0450 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 af6ed28f..32260150 100644 --- a/include/PowerLimiter.h +++ b/include/PowerLimiter.h @@ -88,6 +88,7 @@ private: 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(); diff --git a/include/Utils.h b/include/Utils.h index 4d4bfee3..fddc2ab9 100644 --- a/include/Utils.h +++ b/include/Utils.h @@ -11,4 +11,5 @@ public: 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 091ddb00..12d6bdf7 100644 --- a/include/VictronMppt.h +++ b/include/VictronMppt.h @@ -35,6 +35,9 @@ public: // 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 91243c18..f29dc8fc 100644 --- a/include/WebApi_config.h +++ b/include/WebApi_config.h @@ -14,6 +14,4 @@ private: 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 f74315e3..48976bce 100644 --- a/include/WebApi_device.h +++ b/include/WebApi_device.h @@ -11,6 +11,4 @@ public: private: void onDeviceAdminGet(AsyncWebServerRequest* request); void onDeviceAdminPost(AsyncWebServerRequest* request); - - AsyncWebServer* _server; }; diff --git a/include/WebApi_devinfo.h b/include/WebApi_devinfo.h index d1924ecc..e312ecdf 100644 --- a/include/WebApi_devinfo.h +++ b/include/WebApi_devinfo.h @@ -10,6 +10,4 @@ public: private: void onDevInfoStatus(AsyncWebServerRequest* request); - - AsyncWebServer* _server; }; diff --git a/include/WebApi_dtu.h b/include/WebApi_dtu.h index 3acf4494..20f5274e 100644 --- a/include/WebApi_dtu.h +++ b/include/WebApi_dtu.h @@ -13,8 +13,6 @@ private: 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 3cba7e5e..e7fe9874 100644 --- a/include/WebApi_eventlog.h +++ b/include/WebApi_eventlog.h @@ -10,6 +10,4 @@ public: private: void onEventlogStatus(AsyncWebServerRequest* request); - - AsyncWebServer* _server; }; diff --git a/include/WebApi_firmware.h b/include/WebApi_firmware.h index fd9a9642..990a5e06 100644 --- a/include/WebApi_firmware.h +++ b/include/WebApi_firmware.h @@ -11,6 +11,4 @@ public: 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 73c15654..cff4ddb8 100644 --- a/include/WebApi_gridprofile.h +++ b/include/WebApi_gridprofile.h @@ -11,6 +11,4 @@ public: private: void onGridProfileStatus(AsyncWebServerRequest* request); void onGridProfileRawdata(AsyncWebServerRequest* request); - - AsyncWebServer* _server; }; diff --git a/include/WebApi_inverter.h b/include/WebApi_inverter.h index b8f05425..c316622e 100644 --- a/include/WebApi_inverter.h +++ b/include/WebApi_inverter.h @@ -14,6 +14,4 @@ private: 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 84d48d3a..285be27c 100644 --- a/include/WebApi_limit.h +++ b/include/WebApi_limit.h @@ -11,6 +11,4 @@ public: private: void onLimitStatus(AsyncWebServerRequest* request); void onLimitPost(AsyncWebServerRequest* request); - - AsyncWebServer* _server; }; diff --git a/include/WebApi_maintenance.h b/include/WebApi_maintenance.h index 02dc4702..5a00bbab 100644 --- a/include/WebApi_maintenance.h +++ b/include/WebApi_maintenance.h @@ -10,6 +10,4 @@ public: private: void onRebootPost(AsyncWebServerRequest* request); - - AsyncWebServer* _server; }; diff --git a/include/WebApi_mqtt.h b/include/WebApi_mqtt.h index 6ec971c9..b259752b 100644 --- a/include/WebApi_mqtt.h +++ b/include/WebApi_mqtt.h @@ -15,6 +15,4 @@ private: 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 7587bbbd..179fa492 100644 --- a/include/WebApi_network.h +++ b/include/WebApi_network.h @@ -12,6 +12,4 @@ private: 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 75e02c54..5ce040ed 100644 --- a/include/WebApi_ntp.h +++ b/include/WebApi_ntp.h @@ -14,6 +14,4 @@ private: 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 7d186eb4..aed11b0e 100644 --- a/include/WebApi_power.h +++ b/include/WebApi_power.h @@ -11,6 +11,4 @@ public: private: void onPowerStatus(AsyncWebServerRequest* request); void onPowerPost(AsyncWebServerRequest* request); - - AsyncWebServer* _server; }; diff --git a/include/WebApi_prometheus.h b/include/WebApi_prometheus.h index 08e2221d..b3ee6a18 100644 --- a/include/WebApi_prometheus.h +++ b/include/WebApi_prometheus.h @@ -17,8 +17,6 @@ private: 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 b5981e3d..ac76522a 100644 --- a/include/WebApi_security.h +++ b/include/WebApi_security.h @@ -13,6 +13,4 @@ private: void onSecurityPost(AsyncWebServerRequest* request); void onAuthenticateGet(AsyncWebServerRequest* request); - - AsyncWebServer* _server; }; diff --git a/include/WebApi_sysstatus.h b/include/WebApi_sysstatus.h index 32bdc7e3..c754ac0d 100644 --- a/include/WebApi_sysstatus.h +++ b/include/WebApi_sysstatus.h @@ -10,6 +10,4 @@ public: private: void onSystemStatus(AsyncWebServerRequest* request); - - AsyncWebServer* _server; }; diff --git a/include/WebApi_webapp.h b/include/WebApi_webapp.h index 401408c6..5330fbdf 100644 --- a/include/WebApi_webapp.h +++ b/include/WebApi_webapp.h @@ -9,5 +9,5 @@ public: 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 4289afd0..cf7beecc 100644 --- a/include/WebApi_ws_console.h +++ b/include/WebApi_ws_console.h @@ -10,7 +10,6 @@ public: 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 392ca869..05f8ab8f 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,19 @@ public: 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); + + 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 _lastPublishStats[INV_MAX_COUNT] = { 0 }; std::mutex _mutex; diff --git a/include/defaults.h b/include/defaults.h index 7e1d7a0c..56030d9b 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -128,6 +128,7 @@ #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 d112fd6f..035e52f4 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 312cf630..5d906e58 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 b6a9d93e..2cfaa28b 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 d038e772..56c7fc69 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 eff44abc..9aeaf106 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 717099b7..d92a510f 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 6cbd2097..757cf91d 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 7b23207d..670b7dbe 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 2f56ec3e..6d9b7ca9 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 bcad2536..13ca061a 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 fd6ed5cc..b2b30a2e 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 831c1ad1..bd405611 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 10f06e04..90b9a5a9 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 @@ private: 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 00000000..1c48e6bb --- /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 e145e14e..adf7eb87 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 606a372f..807f1a4c 100644 --- a/src/BatteryStats.cpp +++ b/src/BatteryStats.cpp @@ -56,7 +56,8 @@ 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 +69,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 +105,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 +212,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 +224,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 +255,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 +334,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 +366,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 +387,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 +411,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 5004b3cb..441d2dac 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -191,6 +191,7 @@ bool ConfigurationClass::write() 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 +208,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; @@ -435,6 +437,7 @@ bool ConfigurationClass::read() 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 +454,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 5bfbb98e..15a6dad0 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 12b2aa56..98d46eea 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 9e199242..03e141e2 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 1df7237e..21ff0fa2 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 53cf490b..de2778d1 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 a0b23686..c2b94662 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 e7f2ea2d..b4c2229a 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -290,14 +290,15 @@ void PowerLimiterClass::loop() } if (_verboseLogging) { - MessageOutput.printf("[DPL::loop] battery interface %s, SoC: %d %%, StartTH: %d %%, StopTH: %d %%, SoC age: %d s\r\n", + 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, @@ -339,6 +340,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 @@ -592,9 +633,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 +648,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 diff --git a/src/PylontechCanReceiver.cpp b/src/PylontechCanReceiver.cpp index c1b26176..e19cff59 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 386e0ed1..7ad07293 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 fd1073a7..c4dd0bd5 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 b96eb2cf..9e2230c4 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 3372e4f4..29f35319 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 010a539f..cc08dfaa 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 a27cb31e..212a7f7d 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 adfd411c..bbdfd070 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 a92e515e..51e85aff 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 617fca06..9491f935 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 587f4640..60c340fa 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 f1bfc2aa..32a47235 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 be8e1202..1d9c111a 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 922b0ba0..ba257efa 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 bdb19ef0..9e7411bf 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 ba998053..12f637ad 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 e0bcd699..02bbfb10 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 54fc664e..b5196789 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 25cb42e2..df530ca8 100644 --- a/src/WebApi_powerlimiter.cpp +++ b/src/WebApi_powerlimiter.cpp @@ -45,6 +45,7 @@ void WebApiPowerLimiterClass::onStatus(AsyncWebServerRequest* request) 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; @@ -133,6 +134,7 @@ void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request) 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 275e568b..8e6be8c9 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 20519681..b95ebb29 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 9c822376..b8c366b3 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 9203505b..b8b81385 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 aaca6d89..1f1efcb2 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 867b7f8f..e51361d7 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); @@ -63,43 +61,6 @@ void WebApiWsLiveClass::sendDataTaskCb() return; } - 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()); - } - - // Update on every inverter change or at least after 10 seconds - if (millis() - _lastWsPublish > (10 * 1000) || (maxTimeStamp != _newestInverterTimestamp)) { - - try { - std::lock_guard lock(_mutex); - DynamicJsonDocument root(4200 * INV_MAX_COUNT); - if (Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { - JsonVariant var = root; - generateJsonResponse(var); - - String buffer; - serializeJson(root, buffer); - - _ws.textAll(buffer); - _newestInverterTimestamp = maxTimeStamp; - } - - } 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()); - } - - _lastWsPublish = millis(); - } -} - -void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root) -{ - JsonArray invArray = root.createNestedArray("inverters"); - // Loop all inverters for (uint8_t i = 0; i < Hoymiles.getNumInverters(); i++) { auto inv = Hoymiles.getInverterByPos(i); @@ -107,64 +68,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,11 +114,7 @@ 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; @@ -200,7 +136,73 @@ void WebApiWsLiveClass::generateJsonResponse(JsonVariant& root) JsonObject powerMeterObj = root.createNestedObject("power_meter"); powerMeterObj["enabled"] = Configuration.get().PowerMeter.Enabled; addTotalField(powerMeterObj, "Power", PowerMeter.getPowerTotal(false), "W", 1); +} +void WebApiWsLiveClass::generateInverterCommonJsonResponse(JsonObject& root, std::shared_ptr inv) +{ + const INVERTER_CONFIG_T* inv_cfg = Configuration.getInverterConfig(inv->serial()); + if (inv_cfg == nullptr) { + return; + } + + 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; + } +} + +void WebApiWsLiveClass::generateInverterChannelJsonResponse(JsonObject& root, std::shared_ptr inv) +{ + const INVERTER_CONFIG_T* inv_cfg = Configuration.getInverterConfig(inv->serial()); + if (inv_cfg == nullptr) { + return; + } + + // 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 +246,38 @@ 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); response->setLength(); request->send(response); diff --git a/src/main.cpp b/src/main.cpp index dd080832..c72bde4a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -34,9 +34,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 @@ -135,12 +139,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 ac787a8e..418eae1a 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/MemoryInfo.vue b/webapp/src/components/MemoryInfo.vue index 132d63ca..b9153dac 100644 --- a/webapp/src/components/MemoryInfo.vue +++ b/webapp/src/components/MemoryInfo.vue @@ -14,6 +14,8 @@ +