diff --git a/.github/workflows/repo-maintenance.yml b/.github/workflows/repo-maintenance.yml new file mode 100644 index 000000000..f7290c2e5 --- /dev/null +++ b/.github/workflows/repo-maintenance.yml @@ -0,0 +1,54 @@ +name: 'Repository Maintenance' + +on: + schedule: + - cron: '0 4 * * *' + workflow_dispatch: + +permissions: + issues: write + pull-requests: write + discussions: write + +concurrency: + group: lock + +jobs: + stale: + name: 'Stale' + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + days-before-stale: 14 + days-before-close: 60 + any-of-labels: 'cant-reproduce,not a bug' + stale-issue-label: stale + stale-pr-label: stale + stale-issue-message: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. + + lock-threads: + name: 'Lock Old Threads' + runs-on: ubuntu-latest + steps: + - uses: dessant/lock-threads@v5 + with: + issue-inactive-days: '30' + pr-inactive-days: '30' + discussion-inactive-days: '30' + log-output: true + issue-comment: > + This issue has been automatically locked since there + has not been any recent activity after it was closed. + Please open a new discussion or issue for related concerns. + pr-comment: > + This pull request has been automatically locked since there + has not been any recent activity after it was closed. + Please open a new discussion or issue for related concerns. + discussion-comment: > + This discussion has been automatically locked since there + has not been any recent activity after it was closed. + Please open a new discussion for related concerns. diff --git a/include/BatteryStats.h b/include/BatteryStats.h index 86e1750b2..ca669aa87 100644 --- a/include/BatteryStats.h +++ b/include/BatteryStats.h @@ -7,6 +7,7 @@ #include "Arduino.h" #include "JkBmsDataPoints.h" #include "VeDirectShuntController.h" +#include // mandatory interface for all kinds of batteries class BatteryStats { @@ -37,7 +38,10 @@ class BatteryStats { // returns true if the battery reached a critically low voltage/SoC, // such that it is in need of charging to prevent degredation. - virtual bool needsCharging() const { return false; } + virtual bool getImmediateChargingRequest() const { return false; }; + + virtual float getChargeCurrent() const { return 0; }; + virtual float getChargeCurrentLimitation() const { return FLT_MAX; }; protected: virtual void mqttPublish() const; @@ -71,7 +75,9 @@ class PylontechBatteryStats : public BatteryStats { public: void getLiveViewData(JsonVariant& root) const final; void mqttPublish() const final; - bool needsCharging() const final { return _chargeImmediately; } + bool getImmediateChargingRequest() const { return _chargeImmediately; } ; + float getChargeCurrent() const { return _current; } ; + float getChargeCurrentLimitation() const { return _chargeCurrentLimitation; } ; private: void setManufacturer(String&& m) { _manufacturer = std::move(m); } @@ -141,7 +147,7 @@ class VictronSmartShuntStats : public BatteryStats { void getLiveViewData(JsonVariant& root) const final; void mqttPublish() const final; - void updateFrom(VeDirectShuntController::veShuntStruct const& shuntData); + void updateFrom(VeDirectShuntController::data_t const& shuntData); private: float _current; diff --git a/include/Configuration.h b/include/Configuration.h index 077ba9414..a78b80626 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -5,7 +5,7 @@ #include #define CONFIG_FILENAME "/config.json" -#define CONFIG_VERSION 0x00011b00 // 0.1.27 // make sure to clean all after change +#define CONFIG_VERSION 0x00011c00 // 0.1.28 // make sure to clean all after change #define WIFI_MAX_SSID_STRLEN 32 #define WIFI_MAX_PASSWORD_STRLEN 64 @@ -39,8 +39,6 @@ #define POWERMETER_MAX_HTTP_JSON_PATH_STRLEN 256 #define POWERMETER_HTTP_TIMEOUT 1000 -#define JSON_BUFFER_SIZE 15360 - struct CHANNEL_CONFIG_T { uint16_t MaxChannelPower; char Name[CHAN_MAX_NAME_STRLEN]; @@ -62,8 +60,9 @@ struct INVERTER_CONFIG_T { CHANNEL_CONFIG_T channel[INV_MAX_CHAN_COUNT]; }; -enum Auth { none, basic, digest }; struct POWERMETER_HTTP_PHASE_CONFIG_T { + enum Auth { None, Basic, Digest }; + enum Unit { Watts = 0, MilliWatts = 1, KiloWatts = 2 }; bool Enabled; char Url[POWERMETER_MAX_HTTP_URL_STRLEN + 1]; Auth AuthType; @@ -73,7 +72,10 @@ struct POWERMETER_HTTP_PHASE_CONFIG_T { char HeaderValue[POWERMETER_MAX_HTTP_HEADER_VALUE_STRLEN + 1]; uint16_t Timeout; char JsonPath[POWERMETER_MAX_HTTP_JSON_PATH_STRLEN + 1]; + Unit PowerUnit; + bool SignInverted; }; +using PowerMeterHttpConfig = struct POWERMETER_HTTP_PHASE_CONFIG_T; struct CONFIG_T { struct { @@ -196,7 +198,7 @@ struct CONFIG_T { uint32_t SdmAddress; uint32_t HttpInterval; bool HttpIndividualRequests; - POWERMETER_HTTP_PHASE_CONFIG_T Http_Phase[POWERMETER_MAX_PHASES]; + PowerMeterHttpConfig Http_Phase[POWERMETER_MAX_PHASES]; } PowerMeter; struct { @@ -213,6 +215,7 @@ struct CONFIG_T { int32_t TargetPowerConsumption; int32_t TargetPowerConsumptionHysteresis; int32_t LowerPowerLimit; + int32_t BaseLoadLimit; int32_t UpperPowerLimit; bool IgnoreSoc; uint32_t BatterySocStartThreshold; @@ -238,12 +241,17 @@ struct CONFIG_T { struct { bool Enabled; + bool VerboseLogging; uint32_t CAN_Controller_Frequency; bool Auto_Power_Enabled; + bool Auto_Power_BatterySoC_Limits_Enabled; + bool Emergency_Charge_Enabled; float Auto_Power_Voltage_Limit; float Auto_Power_Enable_Voltage_Limit; float Auto_Power_Lower_Power_Limit; float Auto_Power_Upper_Power_Limit; + uint8_t Auto_Power_Stop_BatterySoC_Threshold; + float Auto_Power_Target_Power_Consumption; } Huawei; diff --git a/include/HttpPowerMeter.h b/include/HttpPowerMeter.h index 7ac225a47..25e627cd0 100644 --- a/include/HttpPowerMeter.h +++ b/include/HttpPowerMeter.h @@ -4,6 +4,10 @@ #include #include #include +#include "Configuration.h" + +using Auth_t = PowerMeterHttpConfig::Auth; +using Unit_t = PowerMeterHttpConfig::Unit; class HttpPowerMeterClass { public: @@ -11,23 +15,20 @@ class HttpPowerMeterClass { bool updateValues(); float getPower(int8_t phase); char httpPowerMeterError[256]; - bool queryPhase(int phase, const String& url, Auth authType, const char* username, const char* password, - const char* httpHeader, const char* httpValue, uint32_t timeout, const char* jsonPath); - + bool queryPhase(int phase, PowerMeterHttpConfig const& config); -private: +private: float power[POWERMETER_MAX_PHASES]; HTTPClient httpClient; String httpResponse; - bool httpRequest(int phase, WiFiClient &wifiClient, const String& host, uint16_t port, const String& uri, bool https, Auth authType, const char* username, - const char* password, const char* httpHeader, const char* httpValue, uint32_t timeout, const char* jsonPath); + bool httpRequest(int phase, WiFiClient &wifiClient, const String& host, uint16_t port, const String& uri, bool https, PowerMeterHttpConfig const& config); bool extractUrlComponents(String url, String& _protocol, String& _hostname, String& _uri, uint16_t& uint16_t, String& _base64Authorization); String extractParam(String& authReq, const String& param, const char delimit); String getcNonce(const int len); String getDigestAuth(String& authReq, const String& username, const String& password, const String& method, const String& uri, unsigned int counter); - bool tryGetFloatValueForPhase(int phase, const char* jsonPath); - void prepareRequest(uint32_t timeout, const char* httpHeader, const char* httpValue); - String sha256(const String& data); + bool tryGetFloatValueForPhase(int phase, const char* jsonPath, Unit_t unit, bool signInverted); + void prepareRequest(uint32_t timeout, const char* httpHeader, const char* httpValue); + String sha256(const String& data); }; extern HttpPowerMeterClass HttpPowerMeter; diff --git a/include/Huawei_can.h b/include/Huawei_can.h index e9a3a3d79..3a699cd7d 100644 --- a/include/Huawei_can.h +++ b/include/Huawei_can.h @@ -105,14 +105,14 @@ class HuaweiCanCommClass { SPIClass *SPI; MCP_CAN *_CAN; uint8_t _huaweiIrq; // IRQ pin - uint32_t _nextRequestMillis = 0; // When to send next data request to PSU + uint32_t _nextRequestMillis = 0; // When to send next data request to PSU std::mutex _mutex; uint32_t _recValues[12]; uint16_t _txValues[5]; bool _hasNewTxValue[5]; - + uint8_t _errorCode; bool _completeUpdateReceived; }; @@ -125,8 +125,9 @@ class HuaweiCanClass { void setMode(uint8_t mode); RectifierParameters_t * get(); - uint32_t getLastUpdate(); - bool getAutoPowerStatus(); + uint32_t getLastUpdate() const { return _lastUpdateReceivedMillis; }; + bool getAutoPowerStatus() const { return _autoPowerEnabled; }; + uint8_t getMode() const { return _mode; }; private: void loop(); @@ -150,7 +151,8 @@ class HuaweiCanClass { uint8_t _autoPowerEnabledCounter = 0; bool _autoPowerEnabled = false; + bool _batteryEmergencyCharging = false; }; extern HuaweiCanClass HuaweiCan; -extern HuaweiCanCommClass HuaweiCanComm; \ No newline at end of file +extern HuaweiCanCommClass HuaweiCanComm; diff --git a/include/JkBmsController.h b/include/JkBmsController.h index b21744d3f..bbc5a5ac0 100644 --- a/include/JkBmsController.h +++ b/include/JkBmsController.h @@ -19,7 +19,9 @@ class Controller : public BatteryProvider { void deinit() final; void loop() final; std::shared_ptr getStats() const final { return _stats; } - bool usesHwPort2() const final { return true; } + bool usesHwPort2() const final { + return ARDUINO_USB_CDC_ON_BOOT != 1; + } private: enum class Status : unsigned { diff --git a/include/MqttHandleHass.h b/include/MqttHandleHass.h index feb867435..a76cb0c7b 100644 --- a/include/MqttHandleHass.h +++ b/include/MqttHandleHass.h @@ -66,10 +66,10 @@ class MqttHandleHassClass { void publishInverterNumber(std::shared_ptr inv, const char* caption, const char* icon, const char* category, const char* commandTopic, const char* stateTopic, const char* unitOfMeasure, const int16_t min = 1, const int16_t max = 100); void publishInverterBinarySensor(std::shared_ptr inv, const char* caption, const char* subTopic, const char* payload_on, const char* payload_off); - static void createInverterInfo(DynamicJsonDocument& doc, std::shared_ptr inv); - static void createDtuInfo(DynamicJsonDocument& doc); + static void createInverterInfo(JsonDocument& doc, std::shared_ptr inv); + static void createDtuInfo(JsonDocument& doc); - static void createDeviceInfo(DynamicJsonDocument& doc, const String& name, const String& identifiers, const String& configuration_url, const String& manufacturer, const String& model, const String& sw_version, const String& via_device = ""); + static void createDeviceInfo(JsonDocument& doc, const String& name, const String& identifiers, const String& configuration_url, const String& manufacturer, const String& model, const String& sw_version, const String& via_device = ""); static String getDtuUniqueId(); static String getDtuUrl(); diff --git a/include/MqttHandleVedirect.h b/include/MqttHandleVedirect.h index c420d0884..016ee804a 100644 --- a/include/MqttHandleVedirect.h +++ b/include/MqttHandleVedirect.h @@ -21,7 +21,7 @@ class MqttHandleVedirectClass { void forceUpdate(); private: void loop(); - std::map _kvFrames; + std::map _kvFrames; Task _loopTask; @@ -33,8 +33,8 @@ class MqttHandleVedirectClass { bool _PublishFull; - void publish_mppt_data(const VeDirectMpptController::spData_t &spMpptData, - VeDirectMpptController::veMpptStruct &frame) const; + void publish_mppt_data(const VeDirectMpptController::data_t &mpptData, + const VeDirectMpptController::data_t &frame) const; }; extern MqttHandleVedirectClass MqttHandleVedirect; diff --git a/include/MqttHandleVedirectHass.h b/include/MqttHandleVedirectHass.h index 86d364cda..6d7a17ac6 100644 --- a/include/MqttHandleVedirectHass.h +++ b/include/MqttHandleVedirectHass.h @@ -16,13 +16,13 @@ class MqttHandleVedirectHassClass { void publish(const String& subtopic, const String& payload); void publishBinarySensor(const char *caption, const char *icon, const char *subTopic, const char *payload_on, const char *payload_off, - const VeDirectMpptController::spData_t &spMpptData); + const VeDirectMpptController::data_t &mpptData); void publishSensor(const char *caption, const char *icon, const char *subTopic, const char *deviceClass, const char *stateClass, const char *unitOfMeasurement, - const VeDirectMpptController::spData_t &spMpptData); + const VeDirectMpptController::data_t &mpptData); void createDeviceInfo(JsonObject &object, - const VeDirectMpptController::spData_t &spMpptData); + const VeDirectMpptController::data_t &mpptData); Task _loopTask; diff --git a/include/PowerLimiter.h b/include/PowerLimiter.h index 0a8dfab04..c5727fe5c 100644 --- a/include/PowerLimiter.h +++ b/include/PowerLimiter.h @@ -16,12 +16,6 @@ #define PL_UI_STATE_USE_SOLAR_ONLY 2 #define PL_UI_STATE_USE_SOLAR_AND_BATTERY 3 -typedef enum { - EMPTY_WHEN_FULL= 0, - EMPTY_AT_NIGHT -} batDrainStrategy; - - class PowerLimiterClass { public: enum class Status : unsigned { @@ -29,8 +23,6 @@ class PowerLimiterClass { DisabledByConfig, DisabledByMqtt, WaitingForValidTimestamp, - PowerMeterDisabled, - PowerMeterTimeout, PowerMeterPending, InverterInvalid, InverterChanged, @@ -45,11 +37,11 @@ class PowerLimiterClass { NoVeDirect, NoEnergy, HuaweiPsu, - Settling, Stable, }; void init(Scheduler& scheduler); + uint8_t getInverterUpdateTimeouts() const { return _inverterUpdateTimeouts; } uint8_t getPowerLimiterState(); int32_t getLastRequestedPowerLimit() { return _lastRequestedPowerLimit; } @@ -70,6 +62,7 @@ class PowerLimiterClass { int32_t _lastRequestedPowerLimit = 0; bool _shutdownPending = false; + std::optional _oInverterStatsMillis = std::nullopt; std::optional _oUpdateStartMillis = std::nullopt; std::optional _oTargetPowerLimitWatts = std::nullopt; std::optional _oTargetPowerState = std::nullopt; @@ -85,6 +78,7 @@ class PowerLimiterClass { uint32_t _nextCalculateCheck = 5000; // time in millis for next NTP check to calulate restart bool _fullSolarPassThroughEnabled = false; bool _verboseLogging = true; + uint8_t _inverterUpdateTimeouts = 0; frozen::string const& getStatusText(Status status); void announceStatus(Status status); diff --git a/include/PowerMeter.h b/include/PowerMeter.h index f2b2042c6..0ce38f61b 100644 --- a/include/PowerMeter.h +++ b/include/PowerMeter.h @@ -31,6 +31,7 @@ class PowerMeterClass { void init(Scheduler& scheduler); float getPowerTotal(bool forceUpdate = true); uint32_t getLastPowerMeterUpdate(); + bool isDataValid(); private: void loop(); diff --git a/include/Utils.h b/include/Utils.h index 35d648bc6..f81e73180 100644 --- a/include/Utils.h +++ b/include/Utils.h @@ -10,7 +10,6 @@ class Utils { static uint64_t generateDtuSerial(); static int getTimezoneOffset(); static void restartDtu(); - static bool checkJsonAlloc(const DynamicJsonDocument& doc, const char* function, const uint16_t line); - static bool checkJsonOverflow(const DynamicJsonDocument& doc, const char* function, const uint16_t line); + static bool checkJsonAlloc(const JsonDocument& doc, const char* function, const uint16_t line); static void removeAllFiles(); }; diff --git a/include/VictronMppt.h b/include/VictronMppt.h index 39e85aad7..a4f1b1ab8 100644 --- a/include/VictronMppt.h +++ b/include/VictronMppt.h @@ -25,7 +25,7 @@ class VictronMpptClass { uint32_t getDataAgeMillis(size_t idx) const; size_t controllerAmount() const { return _controllers.size(); } - std::optional getData(size_t idx = 0) const; + std::optional getData(size_t idx = 0) const; // total output of all MPPT charge controllers in Watts int32_t getPowerOutputWatts() const; @@ -34,13 +34,13 @@ class VictronMpptClass { int32_t getPanelPowerWatts() const; // sum of total yield of all MPPT charge controllers in kWh - double getYieldTotal() const; + float getYieldTotal() const; // sum of today's yield of all MPPT charge controllers in kWh - double getYieldDay() const; + float getYieldDay() const; // minimum of all MPPT charge controllers' output voltages in V - double getOutputVoltage() const; + float getOutputVoltage() const; private: void loop(); diff --git a/include/VictronSmartShunt.h b/include/VictronSmartShunt.h index 42b65774e..97b421325 100644 --- a/include/VictronSmartShunt.h +++ b/include/VictronSmartShunt.h @@ -9,7 +9,9 @@ class VictronSmartShunt : public BatteryProvider { void deinit() final { } void loop() final; std::shared_ptr getStats() const final { return _stats; } - bool usesHwPort2() const final { return true; } + bool usesHwPort2() const final { + return ARDUINO_USB_CDC_ON_BOOT != 1; + } private: uint32_t _lastUpdate = 0; diff --git a/include/WebApi.h b/include/WebApi.h index 299ed63cf..0a433b8c3 100644 --- a/include/WebApi.h +++ b/include/WebApi.h @@ -25,6 +25,7 @@ #include "WebApi_webapp.h" #include "WebApi_ws_console.h" #include "WebApi_ws_live.h" +#include #include "WebApi_ws_vedirect_live.h" #include "WebApi_vedirect.h" #include "WebApi_ws_Huawei.h" @@ -45,6 +46,10 @@ class WebApiClass { static void writeConfig(JsonVariant& retMsg, const WebApiError code = WebApiError::GenericSuccess, const String& message = "Settings saved!"); + static bool parseRequestData(AsyncWebServerRequest* request, AsyncJsonResponse* response, JsonDocument& json_document); + static uint64_t parseSerialFromRequest(AsyncWebServerRequest* request, String param_name = "inv"); + static bool sendJsonResponse(AsyncWebServerRequest* request, AsyncJsonResponse* response, const char* function, const uint16_t line); + private: AsyncWebServer _server; diff --git a/include/WebApi_errors.h b/include/WebApi_errors.h index efb890c5c..97d61b220 100644 --- a/include/WebApi_errors.h +++ b/include/WebApi_errors.h @@ -5,10 +5,11 @@ enum WebApiError { GenericBase = 1000, GenericSuccess, GenericNoValueFound, - GenericDataTooLarge, + GenericDataTooLarge, // not used anymore GenericParseError, GenericValueMissing, GenericWriteFailed, + GenericInternalServerError, DtuBase = 2000, DtuSerialZero, diff --git a/include/WebApi_mqtt.h b/include/WebApi_mqtt.h index b259752b1..6e428249e 100644 --- a/include/WebApi_mqtt.h +++ b/include/WebApi_mqtt.h @@ -4,8 +4,6 @@ #include #include -#define MQTT_JSON_DOC_SIZE 10240 - class WebApiMqttClass { public: void init(AsyncWebServer& server, Scheduler& scheduler); diff --git a/include/WebApi_powermeter.h b/include/WebApi_powermeter.h index 64a5ab726..7e873b1c1 100644 --- a/include/WebApi_powermeter.h +++ b/include/WebApi_powermeter.h @@ -3,7 +3,8 @@ #include #include - +#include +#include "Configuration.h" class WebApiPowerMeterClass { public: @@ -13,6 +14,7 @@ class WebApiPowerMeterClass { void onStatus(AsyncWebServerRequest* request); void onAdminGet(AsyncWebServerRequest* request); void onAdminPost(AsyncWebServerRequest* request); + void decodeJsonPhaseConfig(JsonObject const& json, PowerMeterHttpConfig& config) const; void onTestHttpRequest(AsyncWebServerRequest* request); AsyncWebServer* _server; diff --git a/include/WebApi_ws_Huawei.h b/include/WebApi_ws_Huawei.h index c8248aeca..38025bb61 100644 --- a/include/WebApi_ws_Huawei.h +++ b/include/WebApi_ws_Huawei.h @@ -12,7 +12,7 @@ class WebApiWsHuaweiLiveClass { void init(AsyncWebServer& server, Scheduler& scheduler); private: - void generateJsonResponse(JsonVariant& root); + void generateCommonJsonResponse(JsonVariant& root); void onLivedataStatus(AsyncWebServerRequest* request); void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len); diff --git a/include/WebApi_ws_battery.h b/include/WebApi_ws_battery.h index 9882649b9..17c63d42f 100644 --- a/include/WebApi_ws_battery.h +++ b/include/WebApi_ws_battery.h @@ -12,7 +12,7 @@ class WebApiWsBatteryLiveClass { void init(AsyncWebServer& server, Scheduler& scheduler); private: - void generateJsonResponse(JsonVariant& root); + void generateCommonJsonResponse(JsonVariant& root); void onLivedataStatus(AsyncWebServerRequest* request); void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len); diff --git a/include/WebApi_ws_vedirect_live.h b/include/WebApi_ws_vedirect_live.h index d46de4cc2..b9890834e 100644 --- a/include/WebApi_ws_vedirect_live.h +++ b/include/WebApi_ws_vedirect_live.h @@ -14,8 +14,8 @@ class WebApiWsVedirectLiveClass { void init(AsyncWebServer& server, Scheduler& scheduler); private: - void generateJsonResponse(JsonVariant& root, bool fullUpdate); - static void populateJson(const JsonObject &root, const VeDirectMpptController::spData_t &spMpptData); + void generateCommonJsonResponse(JsonVariant& root, bool fullUpdate); + static void populateJson(const JsonObject &root, const VeDirectMpptController::data_t &mpptData); void onLivedataStatus(AsyncWebServerRequest* request); void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len); bool hasUpdate(size_t idx); diff --git a/include/defaults.h b/include/defaults.h index 940080c5f..237a94b06 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -22,7 +22,8 @@ #define MDNS_ENABLED false -#define NTP_SERVER "pool.ntp.org" +#define NTP_SERVER_OLD "pool.ntp.org" +#define NTP_SERVER "opendtu.pool.ntp.org" #define NTP_TIMEZONE "CET-1CEST,M3.5.0,M10.5.0/3" #define NTP_TIMEZONEDESCR "Europe/Berlin" #define NTP_LONGITUDE 10.4515f @@ -131,6 +132,7 @@ #define POWERLIMITER_TARGET_POWER_CONSUMPTION 0 #define POWERLIMITER_TARGET_POWER_CONSUMPTION_HYSTERESIS 0 #define POWERLIMITER_LOWER_POWER_LIMIT 10 +#define POWERLIMITER_BASE_LOAD_LIMIT 100 #define POWERLIMITER_UPPER_POWER_LIMIT 800 #define POWERLIMITER_IGNORE_SOC false #define POWERLIMITER_BATTERY_SOC_START_THRESHOLD 80 @@ -154,5 +156,7 @@ #define HUAWEI_AUTO_POWER_ENABLE_VOLTAGE_LIMIT 42.0 #define HUAWEI_AUTO_POWER_LOWER_POWER_LIMIT 150 #define HUAWEI_AUTO_POWER_UPPER_POWER_LIMIT 2000 +#define HUAWEI_AUTO_POWER_STOP_BATTERYSOC_THRESHOLD 95 +#define HUAWEI_AUTO_POWER_TARGET_POWER_CONSUMPTION 0 #define VERBOSE_LOGGING true diff --git a/lib/Hoymiles/src/Hoymiles.cpp b/lib/Hoymiles/src/Hoymiles.cpp index b759cd6cf..384276d58 100644 --- a/lib/Hoymiles/src/Hoymiles.cpp +++ b/lib/Hoymiles/src/Hoymiles.cpp @@ -114,7 +114,7 @@ void HoymilesClass::loop() } // Fetch grid profile - if (iv->Statistics()->getLastUpdate() > 0 && iv->GridProfile()->getLastUpdate() == 0) { + if (iv->Statistics()->getLastUpdate() > 0 && (iv->GridProfile()->getLastUpdate() == 0 || !iv->GridProfile()->containsValidData())) { iv->sendGridOnProFileParaRequest(); } diff --git a/lib/Hoymiles/src/HoymilesRadio.h b/lib/Hoymiles/src/HoymilesRadio.h index 33b8c613b..cb2a947cd 100644 --- a/lib/Hoymiles/src/HoymilesRadio.h +++ b/lib/Hoymiles/src/HoymilesRadio.h @@ -1,11 +1,11 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once -#include "TimeoutHelper.h" #include "commands/CommandAbstract.h" #include "types.h" -#include #include +#include +#include class HoymilesRadio { public: @@ -43,4 +43,4 @@ class HoymilesRadio { bool _busyFlag = false; TimeoutHelper _rxTimeout; -}; \ 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 609e3350f..c84eff478 100644 --- a/lib/Hoymiles/src/inverters/HMT_4CH.cpp +++ b/lib/Hoymiles/src/inverters/HMT_4CH.cpp @@ -70,7 +70,7 @@ bool HMT_4CH::isValidSerial(const uint64_t serial) String HMT_4CH::typeName() const { - return F("HMT-1600/1800/2000-4T"); + return "HMT-1600/1800/2000-4T"; } const byteAssign_t* HMT_4CH::getByteAssignment() const diff --git a/lib/Hoymiles/src/inverters/HMT_6CH.cpp b/lib/Hoymiles/src/inverters/HMT_6CH.cpp index f8b9f4075..2c3dd5f3a 100644 --- a/lib/Hoymiles/src/inverters/HMT_6CH.cpp +++ b/lib/Hoymiles/src/inverters/HMT_6CH.cpp @@ -84,7 +84,7 @@ bool HMT_6CH::isValidSerial(const uint64_t serial) String HMT_6CH::typeName() const { - return F("HMT-1800/2250-6T"); + return "HMT-1800/2250-6T"; } const byteAssign_t* HMT_6CH::getByteAssignment() const diff --git a/lib/Hoymiles/src/parser/GridProfileParser.cpp b/lib/Hoymiles/src/parser/GridProfileParser.cpp index 37cb1d4ac..a7b912a9e 100644 --- a/lib/Hoymiles/src/parser/GridProfileParser.cpp +++ b/lib/Hoymiles/src/parser/GridProfileParser.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 "GridProfileParser.h" #include "../Hoymiles.h" @@ -446,6 +446,11 @@ std::list GridProfileParser::getProfile() const return l; } +bool GridProfileParser::containsValidData() const +{ + return _gridProfileLength > 6; +} + uint8_t GridProfileParser::getSectionSize(const uint8_t section_id, const uint8_t section_version) { uint8_t count = 0; diff --git a/lib/Hoymiles/src/parser/GridProfileParser.h b/lib/Hoymiles/src/parser/GridProfileParser.h index 1be12e1d3..7afdfb825 100644 --- a/lib/Hoymiles/src/parser/GridProfileParser.h +++ b/lib/Hoymiles/src/parser/GridProfileParser.h @@ -43,6 +43,8 @@ class GridProfileParser : public Parser { std::list getProfile() const; + bool containsValidData() const; + private: static uint8_t getSectionSize(const uint8_t section_id, const uint8_t section_version); static int16_t getSectionStart(const uint8_t section_id, const uint8_t section_version); @@ -52,4 +54,4 @@ class GridProfileParser : public Parser { static const std::array _profileTypes; static const std::array _profileValues; -}; \ No newline at end of file +}; diff --git a/lib/ThreadSafeQueue/README.md b/lib/ThreadSafeQueue/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/lib/ThreadSafeQueue/library.json b/lib/ThreadSafeQueue/library.json new file mode 100644 index 000000000..768cb8b23 --- /dev/null +++ b/lib/ThreadSafeQueue/library.json @@ -0,0 +1,13 @@ +{ + "name": "ThreadSafeQueue", + "keywords": "queue, threadsafe", + "description": "An Arduino for ESP32 thread safe queue implementation", + "authors": { + "name": "Thomas Basler" + }, + "version": "0.0.1", + "frameworks": "arduino", + "platforms": [ + "espressif32" + ] +} diff --git a/lib/ThreadSafeQueue/ThreadSafeQueue.h b/lib/ThreadSafeQueue/src/ThreadSafeQueue.h similarity index 100% rename from lib/ThreadSafeQueue/ThreadSafeQueue.h rename to lib/ThreadSafeQueue/src/ThreadSafeQueue.h diff --git a/lib/TimeoutHelper/README.md b/lib/TimeoutHelper/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/lib/TimeoutHelper/library.json b/lib/TimeoutHelper/library.json new file mode 100644 index 000000000..0e0472ba6 --- /dev/null +++ b/lib/TimeoutHelper/library.json @@ -0,0 +1,13 @@ +{ + "name": "TimeoutHelper", + "keywords": "timeout", + "description": "An Arduino for ESP32 timeout helper", + "authors": { + "name": "Thomas Basler" + }, + "version": "0.0.1", + "frameworks": "arduino", + "platforms": [ + "espressif32" + ] +} diff --git a/lib/TimeoutHelper/TimeoutHelper.cpp b/lib/TimeoutHelper/src/TimeoutHelper.cpp similarity index 100% rename from lib/TimeoutHelper/TimeoutHelper.cpp rename to lib/TimeoutHelper/src/TimeoutHelper.cpp diff --git a/lib/TimeoutHelper/TimeoutHelper.h b/lib/TimeoutHelper/src/TimeoutHelper.h similarity index 100% rename from lib/TimeoutHelper/TimeoutHelper.h rename to lib/TimeoutHelper/src/TimeoutHelper.h diff --git a/lib/VeDirectFrameHandler/VeDirectData.cpp b/lib/VeDirectFrameHandler/VeDirectData.cpp new file mode 100644 index 000000000..d28f9f783 --- /dev/null +++ b/lib/VeDirectFrameHandler/VeDirectData.cpp @@ -0,0 +1,260 @@ +#include "VeDirectData.h" + +template +static frozen::string const& getAsString(frozen::map const& values, T val) +{ + auto pos = values.find(val); + if (pos == values.end()) { + static constexpr frozen::string dummy("???"); + return dummy; + } + return pos->second; +} + +/* + * This function returns the product id (PID) as readable text. + */ +frozen::string const& veStruct::getPidAsString() const +{ + /** + * this map is rendered from [1], which is more recent than [2]. Phoenix + * inverters are not included in the map. unfortunately, the documents do + * not fully align. PID 0xA07F is only present in [1]. PIDs 0xA048, 0xA110, + * and 0xA111 are only present in [2]. PIDs 0xA06D and 0xA078 are rev3 in + * [1] but rev2 in [2]. + * + * [1] https://www.victronenergy.com/upload/documents/VE.Direct-Protocol-3.33.pdf + * [2] https://www.victronenergy.com/upload/documents/BlueSolar-HEX-protocol.pdf + */ + static constexpr frozen::map values = { + { 0x0203, "BMV-700" }, + { 0x0204, "BMV-702" }, + { 0x0205, "BMV-700H" }, + { 0x0300, "BlueSolar MPPT 70|15" }, + { 0xA040, "BlueSolar MPPT 75|50" }, + { 0xA041, "BlueSolar MPPT 150|35" }, + { 0xA042, "BlueSolar MPPT 75|15" }, + { 0xA043, "BlueSolar MPPT 100|15" }, + { 0xA044, "BlueSolar MPPT 100|30" }, + { 0xA045, "BlueSolar MPPT 100|50" }, + { 0xA046, "BlueSolar MPPT 150|70" }, + { 0xA047, "BlueSolar MPPT 150|100" }, + { 0xA048, "BlueSolar MPPT 75|50 rev2" }, + { 0xA049, "BlueSolar MPPT 100|50 rev2" }, + { 0xA04A, "BlueSolar MPPT 100|30 rev2" }, + { 0xA04B, "BlueSolar MPPT 150|35 rev2" }, + { 0xA04C, "BlueSolar MPPT 75|10" }, + { 0xA04D, "BlueSolar MPPT 150|45" }, + { 0xA04E, "BlueSolar MPPT 150|60" }, + { 0xA04F, "BlueSolar MPPT 150|85" }, + { 0xA050, "SmartSolar MPPT 250|100" }, + { 0xA051, "SmartSolar MPPT 150|100" }, + { 0xA052, "SmartSolar MPPT 150|85" }, + { 0xA053, "SmartSolar MPPT 75|15" }, + { 0xA054, "SmartSolar MPPT 75|10" }, + { 0xA055, "SmartSolar MPPT 100|15" }, + { 0xA056, "SmartSolar MPPT 100|30" }, + { 0xA057, "SmartSolar MPPT 100|50" }, + { 0xA058, "SmartSolar MPPT 150|35" }, + { 0xA059, "SmartSolar MPPT 150|100 rev2" }, + { 0xA05A, "SmartSolar MPPT 150|85 rev2" }, + { 0xA05B, "SmartSolar MPPT 250|70" }, + { 0xA05C, "SmartSolar MPPT 250|85" }, + { 0xA05D, "SmartSolar MPPT 250|60" }, + { 0xA05E, "SmartSolar MPPT 250|45" }, + { 0xA05F, "SmartSolar MPPT 100|20" }, + { 0xA060, "SmartSolar MPPT 100|20 48V" }, + { 0xA061, "SmartSolar MPPT 150|45" }, + { 0xA062, "SmartSolar MPPT 150|60" }, + { 0xA063, "SmartSolar MPPT 150|70" }, + { 0xA064, "SmartSolar MPPT 250|85 rev2" }, + { 0xA065, "SmartSolar MPPT 250|100 rev2" }, + { 0xA066, "BlueSolar MPPT 100|20" }, + { 0xA067, "BlueSolar MPPT 100|20 48V" }, + { 0xA068, "SmartSolar MPPT 250|60 rev2" }, + { 0xA069, "SmartSolar MPPT 250|70 rev2" }, + { 0xA06A, "SmartSolar MPPT 150|45 rev2" }, + { 0xA06B, "SmartSolar MPPT 150|60 rev2" }, + { 0xA06C, "SmartSolar MPPT 150|70 rev2" }, + { 0xA06D, "SmartSolar MPPT 150|85 rev3" }, + { 0xA06E, "SmartSolar MPPT 150|100 rev3" }, + { 0xA06F, "BlueSolar MPPT 150|45 rev2" }, + { 0xA070, "BlueSolar MPPT 150|60 rev2" }, + { 0xA071, "BlueSolar MPPT 150|70 rev2" }, + { 0xA072, "BlueSolar MPPT 150|45 rev3" }, + { 0xA073, "SmartSolar MPPT 150|45 rev3" }, + { 0xA074, "SmartSolar MPPT 75|10 rev2" }, + { 0xA075, "SmartSolar MPPT 75|15 rev2" }, + { 0xA076, "BlueSolar MPPT 100|30 rev3" }, + { 0xA077, "BlueSolar MPPT 100|50 rev3" }, + { 0xA078, "BlueSolar MPPT 150|35 rev3" }, + { 0xA079, "BlueSolar MPPT 75|10 rev2" }, + { 0xA07A, "BlueSolar MPPT 75|15 rev2" }, + { 0xA07B, "BlueSolar MPPT 100|15 rev2" }, + { 0xA07C, "BlueSolar MPPT 75|10 rev3" }, + { 0xA07D, "BlueSolar MPPT 75|15 rev3" }, + { 0xA07E, "SmartSolar MPPT 100|30 12V" }, + { 0xA07F, "All-In-1 SmartSolar MPPT 75|15 12V" }, + { 0xA102, "SmartSolar MPPT VE.Can 150|70" }, + { 0xA103, "SmartSolar MPPT VE.Can 150|45" }, + { 0xA104, "SmartSolar MPPT VE.Can 150|60" }, + { 0xA105, "SmartSolar MPPT VE.Can 150|85" }, + { 0xA106, "SmartSolar MPPT VE.Can 150|100" }, + { 0xA107, "SmartSolar MPPT VE.Can 250|45" }, + { 0xA108, "SmartSolar MPPT VE.Can 250|60" }, + { 0xA109, "SmartSolar MPPT VE.Can 250|70" }, + { 0xA10A, "SmartSolar MPPT VE.Can 250|85" }, + { 0xA10B, "SmartSolar MPPT VE.Can 250|100" }, + { 0xA10C, "SmartSolar MPPT VE.Can 150|70 rev2" }, + { 0xA10D, "SmartSolar MPPT VE.Can 150|85 rev2" }, + { 0xA10E, "SmartSolar MPPT VE.Can 150|100 rev2" }, + { 0xA10F, "BlueSolar MPPT VE.Can 150|100" }, + { 0xA110, "SmartSolar MPPT RS 450|100" }, + { 0xA111, "SmartSolar MPPT RS 450|200" }, + { 0xA112, "BlueSolar MPPT VE.Can 250|70" }, + { 0xA113, "BlueSolar MPPT VE.Can 250|100" }, + { 0xA114, "SmartSolar MPPT VE.Can 250|70 rev2" }, + { 0xA115, "SmartSolar MPPT VE.Can 250|100 rev2" }, + { 0xA116, "SmartSolar MPPT VE.Can 250|85 rev2" }, + { 0xA117, "BlueSolar MPPT VE.Can 150|100 rev2" }, + { 0xA340, "Phoenix Smart IP43 Charger 12|50 (1+1)" }, + { 0xA341, "Phoenix Smart IP43 Charger 12|50 (3)" }, + { 0xA342, "Phoenix Smart IP43 Charger 24|25 (1+1)" }, + { 0xA343, "Phoenix Smart IP43 Charger 24|25 (3)" }, + { 0xA344, "Phoenix Smart IP43 Charger 12|30 (1+1)" }, + { 0xA345, "Phoenix Smart IP43 Charger 12|30 (3)" }, + { 0xA346, "Phoenix Smart IP43 Charger 24|16 (1+1)" }, + { 0xA347, "Phoenix Smart IP43 Charger 24|16 (3)" }, + { 0xA381, "BMV-712 Smart" }, + { 0xA382, "BMV-710H Smart" }, + { 0xA383, "BMV-712 Smart Rev2" }, + { 0xA389, "SmartShunt 500A/50mV" }, + { 0xA38A, "SmartShunt 1000A/50mV" }, + { 0xA38B, "SmartShunt 2000A/50mV" }, + { 0xA3F0, "Smart BuckBoost 12V/12V-50A" }, + }; + + return getAsString(values, productID_PID); +} + +/* + * This function returns the state of operations (CS) as readable text. + */ +frozen::string const& veMpptStruct::getCsAsString() const +{ + static constexpr frozen::map values = { + { 0, "OFF" }, + { 2, "Fault" }, + { 3, "Bulk" }, + { 4, "Absorbtion" }, + { 5, "Float" }, + { 7, "Equalize (manual)" }, + { 245, "Starting-up" }, + { 247, "Auto equalize / Recondition" }, + { 252, "External Control" } + }; + + return getAsString(values, currentState_CS); +} + +/* + * This function returns the state of MPPT (MPPT) as readable text. + */ +frozen::string const& veMpptStruct::getMpptAsString() const +{ + static constexpr frozen::map values = { + { 0, "OFF" }, + { 1, "Voltage or current limited" }, + { 2, "MPP Tracker active" } + }; + + return getAsString(values, stateOfTracker_MPPT); +} + +/* + * This function returns error state (ERR) as readable text. + */ +frozen::string const& veMpptStruct::getErrAsString() const +{ + static constexpr frozen::map values = { + { 0, "No error" }, + { 2, "Battery voltage too high" }, + { 17, "Charger temperature too high" }, + { 18, "Charger over current" }, + { 19, "Charger current reversed" }, + { 20, "Bulk time limit exceeded" }, + { 21, "Current sensor issue(sensor bias/sensor broken)" }, + { 26, "Terminals overheated" }, + { 28, "Converter issue (dual converter models only)" }, + { 33, "Input voltage too high (solar panel)" }, + { 34, "Input current too high (solar panel)" }, + { 38, "Input shutdown (due to excessive battery voltage)" }, + { 39, "Input shutdown (due to current flow during off mode)" }, + { 40, "Input" }, + { 65, "Lost communication with one of devices" }, + { 67, "Synchronisedcharging device configuration issue" }, + { 68, "BMS connection lost" }, + { 116, "Factory calibration data lost" }, + { 117, "Invalid/incompatible firmware" }, + { 118, "User settings invalid" } + }; + + return getAsString(values, errorCode_ERR); +} + +/* + * This function returns the off reason (OR) as readable text. + */ +frozen::string const& veMpptStruct::getOrAsString() const +{ + static constexpr frozen::map values = { + { 0x00000000, "Not off" }, + { 0x00000001, "No input power" }, + { 0x00000002, "Switched off (power switch)" }, + { 0x00000004, "Switched off (device moderegister)" }, + { 0x00000008, "Remote input" }, + { 0x00000010, "Protection active" }, + { 0x00000020, "Paygo" }, + { 0x00000040, "BMS" }, + { 0x00000080, "Engine shutdown detection" }, + { 0x00000100, "Analysing input voltage" } + }; + + return getAsString(values, offReason_OR); +} + +frozen::string const& VeDirectHexData::getResponseAsString() const +{ + using Response = VeDirectHexResponse; + static constexpr frozen::map values = { + { Response::DONE, "Done" }, + { Response::UNKNOWN, "Unknown" }, + { Response::ERROR, "Error" }, + { Response::PING, "Ping" }, + { Response::GET, "Get" }, + { Response::SET, "Set" }, + { Response::ASYNC, "Async" } + }; + + return getAsString(values, rsp); +} + +frozen::string const& VeDirectHexData::getRegisterAsString() const +{ + using Register = VeDirectHexRegister; + static constexpr frozen::map values = { + { Register::DeviceMode, "Device Mode" }, + { Register::DeviceState, "Device State" }, + { Register::RemoteControlUsed, "Remote Control Used" }, + { Register::PanelVoltage, "Panel Voltage" }, + { Register::ChargerVoltage, "Charger Voltage" }, + { Register::NetworkTotalDcInputPower, "Network Total DC Input Power" }, + { Register::ChargeControllerTemperature, "Charger Controller Temperature" }, + { Register::SmartBatterySenseTemperature, "Smart Battery Sense Temperature" }, + { Register::NetworkInfo, "Network Info" }, + { Register::NetworkMode, "Network Mode" }, + { Register::NetworkStatus, "Network Status" } + }; + + return getAsString(values, addr); +} diff --git a/lib/VeDirectFrameHandler/VeDirectData.h b/lib/VeDirectFrameHandler/VeDirectData.h new file mode 100644 index 000000000..86be497ff --- /dev/null +++ b/lib/VeDirectFrameHandler/VeDirectData.h @@ -0,0 +1,139 @@ +#pragma once + +#include +#include + +#define VE_MAX_VALUE_LEN 33 // VE.Direct Protocol: max value size is 33 including /0 +#define VE_MAX_HEX_LEN 100 // Maximum size of hex frame - max payload 34 byte (=68 char) + safe buffer + +typedef struct { + uint16_t productID_PID = 0; // product id + char serialNr_SER[VE_MAX_VALUE_LEN]; // serial number + char firmwareNr_FW[VE_MAX_VALUE_LEN]; // firmware release number + uint32_t batteryVoltage_V_mV = 0; // battery voltage in mV + int32_t batteryCurrent_I_mA = 0; // battery current in mA (can be negative) + float mpptEfficiency_Percent = 0; // efficiency in percent (calculated, moving average) + + frozen::string const& getPidAsString() const; // product ID as string +} veStruct; + +struct veMpptStruct : veStruct { + uint8_t stateOfTracker_MPPT; // state of MPP tracker + uint16_t panelPower_PPV_W; // panel power in W + uint32_t panelVoltage_VPV_mV; // panel voltage in mV + uint32_t panelCurrent_mA; // panel current in mA (calculated) + int16_t batteryOutputPower_W; // battery output power in W (calculated, can be negative if load output is used) + uint32_t loadCurrent_IL_mA; // Load current in mA (Available only for models with a load output) + bool loadOutputState_LOAD; // virtual load output state (on if battery voltage reaches upper limit, off if battery reaches lower limit) + uint8_t currentState_CS; // current state of operation e.g. OFF or Bulk + uint8_t errorCode_ERR; // error code + uint32_t offReason_OR; // off reason + uint16_t daySequenceNr_HSDS; // day sequence number 1...365 + uint32_t yieldTotal_H19_Wh; // yield total resetable Wh + uint32_t yieldToday_H20_Wh; // yield today Wh + uint16_t maxPowerToday_H21_W; // maximum power today W + uint32_t yieldYesterday_H22_Wh; // yield yesterday Wh + uint16_t maxPowerYesterday_H23_W; // maximum power yesterday W + + // these are values communicated through the HEX protocol. the pair's first + // value is the timestamp the respective info was last received. if it is + // zero, the value is deemed invalid. the timestamp is reset if no current + // value could be retrieved. + std::pair MpptTemperatureMilliCelsius; + std::pair SmartBatterySenseTemperatureMilliCelsius; + std::pair NetworkTotalDcInputPowerMilliWatts; + std::pair NetworkInfo; + std::pair NetworkMode; + std::pair NetworkStatus; + + frozen::string const& getMpptAsString() const; // state of mppt as string + frozen::string const& getCsAsString() const; // current state as string + frozen::string const& getErrAsString() const; // error state as string + frozen::string const& getOrAsString() const; // off reason as string +}; + +struct veShuntStruct : veStruct { + int32_t T; // Battery temperature + bool tempPresent; // Battery temperature sensor is attached to the shunt + int32_t P; // Instantaneous power + int32_t CE; // Consumed Amp Hours + int32_t SOC; // State-of-charge + uint32_t TTG; // Time-to-go + bool ALARM; // Alarm condition active + uint16_t alarmReason_AR; // Alarm Reason + int32_t H1; // Depth of the deepest discharge + int32_t H2; // Depth of the last discharge + int32_t H3; // Depth of the average discharge + int32_t H4; // Number of charge cycles + int32_t H5; // Number of full discharges + int32_t H6; // Cumulative Amp Hours drawn + int32_t H7; // Minimum main (battery) voltage + int32_t H8; // Maximum main (battery) voltage + int32_t H9; // Number of seconds since last full charge + int32_t H10; // Number of automatic synchronizations + int32_t H11; // Number of low main voltage alarms + int32_t H12; // Number of high main voltage alarms + int32_t H13; // Number of low auxiliary voltage alarms + int32_t H14; // Number of high auxiliary voltage alarms + int32_t H15; // Minimum auxiliary (battery) voltage + int32_t H16; // Maximum auxiliary (battery) voltage + int32_t H17; // Amount of discharged energy + int32_t H18; // Amount of charged energy + int8_t dcMonitorMode_MON; // DC monitor mode +}; + +enum class VeDirectHexCommand : uint8_t { + ENTER_BOOT = 0x0, + PING = 0x1, + RSV1 = 0x2, + APP_VERSION = 0x3, + PRODUCT_ID = 0x4, + RSV2 = 0x5, + RESTART = 0x6, + GET = 0x7, + SET = 0x8, + RSV3 = 0x9, + ASYNC = 0xA, + RSV4 = 0xB, + RSV5 = 0xC, + RSV6 = 0xD, + RSV7 = 0xE, + RSV8 = 0xF +}; + +enum class VeDirectHexResponse : uint8_t { + DONE = 0x1, + UNKNOWN = 0x3, + ERROR = 0x4, + PING = 0x5, + GET = 0x7, + SET = 0x8, + ASYNC = 0xA +}; + +enum class VeDirectHexRegister : uint16_t { + DeviceMode = 0x0200, + DeviceState = 0x0201, + RemoteControlUsed = 0x0202, + PanelVoltage = 0xEDBB, + ChargerVoltage = 0xEDD5, + NetworkTotalDcInputPower = 0x2027, + ChargeControllerTemperature = 0xEDDB, + SmartBatterySenseTemperature = 0xEDEC, + NetworkInfo = 0x200D, + NetworkMode = 0x200E, + NetworkStatus = 0x200F, + HistoryTotal = 0x104F, + HistoryMPPTD30 = 0x10BE +}; + +struct VeDirectHexData { + VeDirectHexResponse rsp; // hex response code + VeDirectHexRegister addr; // register address + uint8_t flags; // flags + uint32_t value; // integer value of register + char text[VE_MAX_HEX_LEN]; // text/string response + + frozen::string const& getResponseAsString() const; + frozen::string const& getRegisterAsString() const; +}; diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp index 64c1e5df0..44435dacf 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp +++ b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp @@ -30,7 +30,7 @@ * 2020.05.05 - 0.2 - initial release * 2020.06.21 - 0.2 - add MIT license, no code changes * 2020.08.20 - 0.3 - corrected #include reference - * + * 2024.03.08 - 0.4 - adds the ability to send hex commands and disassemble hex messages */ #include @@ -39,18 +39,6 @@ // The name of the record that contains the checksum. static constexpr char checksumTagName[] = "CHECKSUM"; -// state machine -enum States { - IDLE = 1, - RECORD_BEGIN = 2, - RECORD_NAME = 3, - RECORD_VALUE = 4, - CHECKSUM = 5, - RECORD_HEX = 6 -}; - - - class Silent : public Print { public: size_t write(uint8_t c) final { return 0; } @@ -58,10 +46,11 @@ class Silent : public Print { static Silent MessageOutputDummy; -VeDirectFrameHandler::VeDirectFrameHandler() : +template +VeDirectFrameHandler::VeDirectFrameHandler() : _msgOut(&MessageOutputDummy), _lastUpdate(0), - _state(IDLE), + _state(State::IDLE), _checksum(0), _textPointer(0), _hexSize(0), @@ -72,21 +61,27 @@ VeDirectFrameHandler::VeDirectFrameHandler() : { } -void VeDirectFrameHandler::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort) +template +void VeDirectFrameHandler::init(char const* who, int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort) { _vedirectSerial = std::make_unique(hwSerialPort); + _vedirectSerial->end(); // make sure the UART will be re-initialized _vedirectSerial->begin(19200, SERIAL_8N1, rx, tx); _vedirectSerial->flush(); + _canSend = (tx != -1); _msgOut = msgOut; _verboseLogging = verboseLogging; _debugIn = 0; + snprintf(_logId, sizeof(_logId), "[VE.Direct %s %d/%d]", who, rx, tx); + if (_verboseLogging) { _msgOut->printf("%s init complete\r\n", _logId); } } -void VeDirectFrameHandler::dumpDebugBuffer() { - _msgOut->printf("[VE.Direct] serial input (%d Bytes):", _debugIn); +template +void VeDirectFrameHandler::dumpDebugBuffer() { + _msgOut->printf("%s serial input (%d Bytes):", _logId, _debugIn); for (int i = 0; i < _debugIn; ++i) { if (i % 16 == 0) { - _msgOut->printf("\r\n[VE.Direct]"); + _msgOut->printf("\r\n%s", _logId); } _msgOut->printf(" %02x", _debugBuffer[i]); } @@ -94,21 +89,30 @@ void VeDirectFrameHandler::dumpDebugBuffer() { _debugIn = 0; } -void VeDirectFrameHandler::loop() +template +void VeDirectFrameHandler::reset() +{ + _checksum = 0; + _state = State::IDLE; + _textData.clear(); +} + +template +void VeDirectFrameHandler::loop() { while ( _vedirectSerial->available()) { rxData(_vedirectSerial->read()); _lastByteMillis = millis(); } - // there will never be a large gap between two bytes of the same frame. + // there will never be a large gap between two bytes. // if such a large gap is observed, reset the state machine so it tries - // to decode a new frame once more data arrives. - if (IDLE != _state && _lastByteMillis + 500 < millis()) { - _msgOut->printf("[VE.Direct] Resetting state machine (was %d) after timeout\r\n", _state); + // to decode a new frame / hex messages once more data arrives. + if ((State::IDLE != _state) && ((millis() - _lastByteMillis) > 500)) { + _msgOut->printf("%s Resetting state machine (was %d) after timeout\r\n", + _logId, static_cast(_state)); if (_verboseLogging) { dumpDebugBuffer(); } - _checksum = 0; - _state = IDLE; + reset(); } } @@ -117,44 +121,45 @@ void VeDirectFrameHandler::loop() * This function is called by loop() which passes a byte of serial data * Based on Victron's example code. But using String and Map instead of pointer and arrays */ -void VeDirectFrameHandler::rxData(uint8_t inbyte) +template +void VeDirectFrameHandler::rxData(uint8_t inbyte) { if (_verboseLogging) { _debugBuffer[_debugIn] = inbyte; _debugIn = (_debugIn + 1) % _debugBuffer.size(); if (0 == _debugIn) { - _msgOut->println("[VE.Direct] ERROR: debug buffer overrun!"); + _msgOut->printf("%s ERROR: debug buffer overrun!\r\n", _logId); } } - if ( (inbyte == ':') && (_state != CHECKSUM) ) { + if ( (inbyte == ':') && (_state != State::CHECKSUM) ) { _prevState = _state; //hex frame can interrupt TEXT - _state = RECORD_HEX; + _state = State::RECORD_HEX; _hexSize = 0; } - if (_state != RECORD_HEX) { + if (_state != State::RECORD_HEX) { _checksum += inbyte; } inbyte = toupper(inbyte); switch(_state) { - case IDLE: + case State::IDLE: /* wait for \n of the start of an record */ switch(inbyte) { case '\n': - _state = RECORD_BEGIN; + _state = State::RECORD_BEGIN; break; case '\r': /* Skip */ default: break; } break; - case RECORD_BEGIN: + case State::RECORD_BEGIN: _textPointer = _name; *_textPointer++ = inbyte; - _state = RECORD_NAME; + _state = State::RECORD_NAME; break; - case RECORD_NAME: + case State::RECORD_NAME: // The record name is being received, terminated by a \t switch(inbyte) { case '\t': @@ -162,12 +167,12 @@ void VeDirectFrameHandler::rxData(uint8_t inbyte) if ( _textPointer < (_name + sizeof(_name)) ) { *_textPointer = 0; /* Zero terminate */ if (strcmp(_name, checksumTagName) == 0) { - _state = CHECKSUM; + _state = State::CHECKSUM; break; } } _textPointer = _value; /* Reset value pointer */ - _state = RECORD_VALUE; + _state = State::RECORD_VALUE; break; case '#': /* Ignore # from serial number*/ break; @@ -178,15 +183,15 @@ void VeDirectFrameHandler::rxData(uint8_t inbyte) break; } break; - case RECORD_VALUE: + case State::RECORD_VALUE: // The record value is being received. The \r indicates a new record. switch(inbyte) { case '\n': if ( _textPointer < (_value + sizeof(_value)) ) { *_textPointer = 0; // make zero ended - textRxEvent(_name, _value); + _textData.push_back({_name, _value}); } - _state = RECORD_BEGIN; + _state = State::RECORD_BEGIN; break; case '\r': /* Skip */ break; @@ -197,221 +202,120 @@ void VeDirectFrameHandler::rxData(uint8_t inbyte) break; } break; - case CHECKSUM: + case State::CHECKSUM: { - bool valid = _checksum == 0; - if (!valid) { - _msgOut->printf("[VE.Direct] checksum 0x%02x != 0, invalid frame\r\n", _checksum); - } if (_verboseLogging) { dumpDebugBuffer(); } - _checksum = 0; - _state = IDLE; - if (valid) { frameValidEvent(); } + if (_checksum == 0) { + for (auto const& event : _textData) { + processTextData(event.first, event.second); + } + _lastUpdate = millis(); + frameValidEvent(); + } + else { + _msgOut->printf("%s checksum 0x%02x != 0x00, invalid frame\r\n", _logId, _checksum); + } + reset(); break; } - case RECORD_HEX: + case State::RECORD_HEX: _state = hexRxEvent(inbyte); break; } } /* - * textRxEvent * This function is called every time a new name/value is successfully parsed. It writes the values to the temporary buffer. */ -bool VeDirectFrameHandler::textRxEvent(std::string const& who, char* name, char* value, veStruct& frame) { +template +void VeDirectFrameHandler::processTextData(std::string const& name, std::string const& value) { if (_verboseLogging) { - _msgOut->printf("[Victron %s] Text Event %s: Value: %s\r\n", - who.c_str(), name, value ); + _msgOut->printf("%s Text Data '%s' = '%s'\r\n", + _logId, name.c_str(), value.c_str()); } - if (strcmp(name, "PID") == 0) { - frame.PID = strtol(value, nullptr, 0); - return true; + if (processTextDataDerived(name, value)) { return; } + + if (name == "PID") { + _tmpFrame.productID_PID = strtol(value.c_str(), nullptr, 0); + return; } - if (strcmp(name, "SER") == 0) { - strcpy(frame.SER, value); - return true; + if (name == "SER") { + strcpy(_tmpFrame.serialNr_SER, value.c_str()); + return; } - if (strcmp(name, "FW") == 0) { - strcpy(frame.FW, value); - return true; + if (name == "FW") { + strcpy(_tmpFrame.firmwareNr_FW, value.c_str()); + return; } - if (strcmp(name, "V") == 0) { - frame.V = round(atof(value) / 10.0) / 100.0; - return true; + if (name == "V") { + _tmpFrame.batteryVoltage_V_mV = atol(value.c_str()); + return; } - if (strcmp(name, "I") == 0) { - frame.I = round(atof(value) / 10.0) / 100.0; - return true; + if (name == "I") { + _tmpFrame.batteryCurrent_I_mA = atol(value.c_str()); + return; } - return false; + _msgOut->printf("%s Unknown text data '%s' (value '%s')\r\n", + _logId, name.c_str(), value.c_str()); } - - /* * hexRxEvent * This function records hex answers or async messages */ -int VeDirectFrameHandler::hexRxEvent(uint8_t inbyte) { - int ret=RECORD_HEX; // default - continue recording until end of frame +template +typename VeDirectFrameHandler::State VeDirectFrameHandler::hexRxEvent(uint8_t inbyte) +{ + State ret = State::RECORD_HEX; // default - continue recording until end of frame switch (inbyte) { case '\n': + // now we can analyse the hex message + _hexBuffer[_hexSize] = '\0'; + VeDirectHexData data; + if (disassembleHexData(data) && !hexDataHandler(data) && _verboseLogging) { + _msgOut->printf("%s Unhandled Hex %s Response, addr: 0x%04X (%s), " + "value: 0x%08X, flags: 0x%02X\r\n", _logId, + data.getResponseAsString().data(), + static_cast(data.addr), + data.getRegisterAsString().data(), + data.value, data.flags); + } + // restore previous state ret=_prevState; break; default: - _hexSize++; + _hexBuffer[_hexSize++]=inbyte; + if (_hexSize>=VE_MAX_HEX_LEN) { // oops -buffer overflow - something went wrong, we abort - _msgOut->println("[VE.Direct] hexRx buffer overflow - aborting read"); + _msgOut->printf("%s hexRx buffer overflow - aborting read\r\n", _logId); _hexSize=0; - ret=IDLE; + ret = State::IDLE; } } return ret; } -bool VeDirectFrameHandler::isDataValid(veStruct const& frame) const { - return strlen(frame.SER) > 0 && _lastUpdate > 0 && (millis() - _lastUpdate) < (10 * 1000); -} - -uint32_t VeDirectFrameHandler::getLastUpdate() const +template +bool VeDirectFrameHandler::isDataValid() const { - return _lastUpdate; + // VE.Direct text frame data is valid if we receive a device serialnumber and + // the data is not older as 10 seconds + // we accept a glitch where the data is valid for ten seconds when serialNr_SER != "" and (millis() - _lastUpdate) overflows + return strlen(_tmpFrame.serialNr_SER) > 0 && (millis() - _lastUpdate) < (10 * 1000); } -/* - * getPidAsString - * This function returns the product id (PID) as readable text. - */ -frozen::string const& VeDirectFrameHandler::veStruct::getPidAsString() const +template +uint32_t VeDirectFrameHandler::getLastUpdate() const { - /** - * this map is rendered from [1], which is more recent than [2]. Phoenix - * inverters are not included in the map. unfortunately, the documents do - * not fully align. PID 0xA07F is only present in [1]. PIDs 0xA048, 0xA110, - * and 0xA111 are only present in [2]. PIDs 0xA06D and 0xA078 are rev3 in - * [1] but rev2 in [2]. - * - * [1] https://www.victronenergy.com/upload/documents/VE.Direct-Protocol-3.33.pdf - * [2] https://www.victronenergy.com/upload/documents/BlueSolar-HEX-protocol.pdf - */ - static constexpr frozen::map values = { - { 0x0203, "BMV-700" }, - { 0x0204, "BMV-702" }, - { 0x0205, "BMV-700H" }, - { 0x0300, "BlueSolar MPPT 70|15" }, - { 0xA040, "BlueSolar MPPT 75|50" }, - { 0xA041, "BlueSolar MPPT 150|35" }, - { 0xA042, "BlueSolar MPPT 75|15" }, - { 0xA043, "BlueSolar MPPT 100|15" }, - { 0xA044, "BlueSolar MPPT 100|30" }, - { 0xA045, "BlueSolar MPPT 100|50" }, - { 0xA046, "BlueSolar MPPT 150|70" }, - { 0xA047, "BlueSolar MPPT 150|100" }, - { 0xA048, "BlueSolar MPPT 75|50 rev2" }, - { 0xA049, "BlueSolar MPPT 100|50 rev2" }, - { 0xA04A, "BlueSolar MPPT 100|30 rev2" }, - { 0xA04B, "BlueSolar MPPT 150|35 rev2" }, - { 0xA04C, "BlueSolar MPPT 75|10" }, - { 0xA04D, "BlueSolar MPPT 150|45" }, - { 0xA04E, "BlueSolar MPPT 150|60" }, - { 0xA04F, "BlueSolar MPPT 150|85" }, - { 0xA050, "SmartSolar MPPT 250|100" }, - { 0xA051, "SmartSolar MPPT 150|100" }, - { 0xA052, "SmartSolar MPPT 150|85" }, - { 0xA053, "SmartSolar MPPT 75|15" }, - { 0xA054, "SmartSolar MPPT 75|10" }, - { 0xA055, "SmartSolar MPPT 100|15" }, - { 0xA056, "SmartSolar MPPT 100|30" }, - { 0xA057, "SmartSolar MPPT 100|50" }, - { 0xA058, "SmartSolar MPPT 150|35" }, - { 0xA059, "SmartSolar MPPT 150|100 rev2" }, - { 0xA05A, "SmartSolar MPPT 150|85 rev2" }, - { 0xA05B, "SmartSolar MPPT 250|70" }, - { 0xA05C, "SmartSolar MPPT 250|85" }, - { 0xA05D, "SmartSolar MPPT 250|60" }, - { 0xA05E, "SmartSolar MPPT 250|45" }, - { 0xA05F, "SmartSolar MPPT 100|20" }, - { 0xA060, "SmartSolar MPPT 100|20 48V" }, - { 0xA061, "SmartSolar MPPT 150|45" }, - { 0xA062, "SmartSolar MPPT 150|60" }, - { 0xA063, "SmartSolar MPPT 150|70" }, - { 0xA064, "SmartSolar MPPT 250|85 rev2" }, - { 0xA065, "SmartSolar MPPT 250|100 rev2" }, - { 0xA066, "BlueSolar MPPT 100|20" }, - { 0xA067, "BlueSolar MPPT 100|20 48V" }, - { 0xA068, "SmartSolar MPPT 250|60 rev2" }, - { 0xA069, "SmartSolar MPPT 250|70 rev2" }, - { 0xA06A, "SmartSolar MPPT 150|45 rev2" }, - { 0xA06B, "SmartSolar MPPT 150|60 rev2" }, - { 0xA06C, "SmartSolar MPPT 150|70 rev2" }, - { 0xA06D, "SmartSolar MPPT 150|85 rev3" }, - { 0xA06E, "SmartSolar MPPT 150|100 rev3" }, - { 0xA06F, "BlueSolar MPPT 150|45 rev2" }, - { 0xA070, "BlueSolar MPPT 150|60 rev2" }, - { 0xA071, "BlueSolar MPPT 150|70 rev2" }, - { 0xA072, "BlueSolar MPPT 150|45 rev3" }, - { 0xA073, "SmartSolar MPPT 150|45 rev3" }, - { 0xA074, "SmartSolar MPPT 75|10 rev2" }, - { 0xA075, "SmartSolar MPPT 75|15 rev2" }, - { 0xA076, "BlueSolar MPPT 100|30 rev3" }, - { 0xA077, "BlueSolar MPPT 100|50 rev3" }, - { 0xA078, "BlueSolar MPPT 150|35 rev3" }, - { 0xA079, "BlueSolar MPPT 75|10 rev2" }, - { 0xA07A, "BlueSolar MPPT 75|15 rev2" }, - { 0xA07B, "BlueSolar MPPT 100|15 rev2" }, - { 0xA07C, "BlueSolar MPPT 75|10 rev3" }, - { 0xA07D, "BlueSolar MPPT 75|15 rev3" }, - { 0xA07E, "SmartSolar MPPT 100|30 12V" }, - { 0xA07F, "All-In-1 SmartSolar MPPT 75|15 12V" }, - { 0xA102, "SmartSolar MPPT VE.Can 150|70" }, - { 0xA103, "SmartSolar MPPT VE.Can 150|45" }, - { 0xA104, "SmartSolar MPPT VE.Can 150|60" }, - { 0xA105, "SmartSolar MPPT VE.Can 150|85" }, - { 0xA106, "SmartSolar MPPT VE.Can 150|100" }, - { 0xA107, "SmartSolar MPPT VE.Can 250|45" }, - { 0xA108, "SmartSolar MPPT VE.Can 250|60" }, - { 0xA109, "SmartSolar MPPT VE.Can 250|70" }, - { 0xA10A, "SmartSolar MPPT VE.Can 250|85" }, - { 0xA10B, "SmartSolar MPPT VE.Can 250|100" }, - { 0xA10C, "SmartSolar MPPT VE.Can 150|70 rev2" }, - { 0xA10D, "SmartSolar MPPT VE.Can 150|85 rev2" }, - { 0xA10E, "SmartSolar MPPT VE.Can 150|100 rev2" }, - { 0xA10F, "BlueSolar MPPT VE.Can 150|100" }, - { 0xA110, "SmartSolar MPPT RS 450|100" }, - { 0xA111, "SmartSolar MPPT RS 450|200" }, - { 0xA112, "BlueSolar MPPT VE.Can 250|70" }, - { 0xA113, "BlueSolar MPPT VE.Can 250|100" }, - { 0xA114, "SmartSolar MPPT VE.Can 250|70 rev2" }, - { 0xA115, "SmartSolar MPPT VE.Can 250|100 rev2" }, - { 0xA116, "SmartSolar MPPT VE.Can 250|85 rev2" }, - { 0xA117, "BlueSolar MPPT VE.Can 150|100 rev2" }, - { 0xA340, "Phoenix Smart IP43 Charger 12|50 (1+1)" }, - { 0xA341, "Phoenix Smart IP43 Charger 12|50 (3)" }, - { 0xA342, "Phoenix Smart IP43 Charger 24|25 (1+1)" }, - { 0xA343, "Phoenix Smart IP43 Charger 24|25 (3)" }, - { 0xA344, "Phoenix Smart IP43 Charger 12|30 (1+1)" }, - { 0xA345, "Phoenix Smart IP43 Charger 12|30 (3)" }, - { 0xA346, "Phoenix Smart IP43 Charger 24|16 (1+1)" }, - { 0xA347, "Phoenix Smart IP43 Charger 24|16 (3)" }, - { 0xA381, "BMV-712 Smart" }, - { 0xA382, "BMV-710H Smart" }, - { 0xA383, "BMV-712 Smart Rev2" }, - { 0xA389, "SmartShunt 500A/50mV" }, - { 0xA38A, "SmartShunt 1000A/50mV" }, - { 0xA38B, "SmartShunt 2000A/50mV" }, - { 0xA3F0, "Smart BuckBoost 12V/12V-50A" }, - }; - - return getAsString(values, PID); + return _lastUpdate; } diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHandler.h b/lib/VeDirectFrameHandler/VeDirectFrameHandler.h index 1acf56ad6..1c482920c 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHandler.h +++ b/lib/VeDirectFrameHandler/VeDirectFrameHandler.h @@ -6,6 +6,7 @@ * 2020.05.05 - 0.2 - initial release * 2021.02.23 - 0.3 - change frameLen to 22 per VE.Direct Protocol version 3.30 * 2022.08.20 - 0.4 - changes for OpenDTU + * 2024.03.08 - 0.4 - adds the ability to send hex commands and disassemble hex messages * */ @@ -13,67 +14,79 @@ #include #include -#include -#include #include +#include +#include +#include "VeDirectData.h" -#define VE_MAX_VALUE_LEN 33 // VE.Direct Protocol: max value size is 33 including /0 -#define VE_MAX_HEX_LEN 100 // Maximum size of hex frame - max payload 34 byte (=68 char) + safe buffer - +template class VeDirectFrameHandler { public: - VeDirectFrameHandler(); - virtual void init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort); - void loop(); // main loop to read ve.direct data + virtual void loop(); // main loop to read ve.direct data uint32_t getLastUpdate() const; // timestamp of last successful frame read + bool isDataValid() const; // return true if data valid and not outdated + T const& getData() const { return _tmpFrame; } + bool sendHexCommand(VeDirectHexCommand cmd, VeDirectHexRegister addr, uint32_t value = 0, uint8_t valsize = 0); protected: + VeDirectFrameHandler(); + void init(char const* who, int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort); + virtual bool hexDataHandler(VeDirectHexData const &data) { return false; } // handles the disassembeled hex response + bool _verboseLogging; Print* _msgOut; uint32_t _lastUpdate; - typedef struct { - uint16_t PID = 0; // product id - char SER[VE_MAX_VALUE_LEN]; // serial number - char FW[VE_MAX_VALUE_LEN]; // firmware release number - double V = 0; // battery voltage in V - double I = 0; // battery current in A - double E = 0; // efficiency in percent (calculated, moving average) - - frozen::string const& getPidAsString() const; // product ID as string - } veStruct; - - bool textRxEvent(std::string const& who, char* name, char* value, veStruct& frame); - bool isDataValid(veStruct const& frame) const; // return true if data valid and not outdated + T _tmpFrame; - template - static frozen::string const& getAsString(frozen::map const& values, T val) - { - auto pos = values.find(val); - if (pos == values.end()) { - static constexpr frozen::string dummy("???"); - return dummy; - } - return pos->second; - } + bool _canSend; + char _logId[32]; private: - void setLastUpdate(); // set timestampt after successful frame read + void reset(); void dumpDebugBuffer(); void rxData(uint8_t inbyte); // byte of serial data - virtual void textRxEvent(char *, char *) = 0; - virtual void frameValidEvent() = 0; - int hexRxEvent(uint8_t); + void processTextData(std::string const& name, std::string const& value); + virtual bool processTextDataDerived(std::string const& name, std::string const& value) = 0; + virtual void frameValidEvent() { } + bool disassembleHexData(VeDirectHexData &data); //return true if disassembling was possible std::unique_ptr _vedirectSerial; - int _state; // current state - int _prevState; // previous state + + enum class State { + IDLE = 1, + RECORD_BEGIN = 2, + RECORD_NAME = 3, + RECORD_VALUE = 4, + CHECKSUM = 5, + RECORD_HEX = 6 + }; + State _state; + State _prevState; + + State hexRxEvent(uint8_t inbyte); + uint8_t _checksum; // checksum value char * _textPointer; // pointer to the private buffer we're writing to, name or value - int _hexSize; // length of hex buffer + int _hexSize; // length of hex buffer + char _hexBuffer[VE_MAX_HEX_LEN]; // buffer for received hex frames char _name[VE_MAX_VALUE_LEN]; // buffer for the field name char _value[VE_MAX_VALUE_LEN]; // buffer for the field value std::array _debugBuffer; unsigned _debugIn; - uint32_t _lastByteMillis; + uint32_t _lastByteMillis; // time of last parsed byte + + /** + * not every frame contains every value the device is communicating, i.e., + * a set of values can be fragmented across multiple frames. frames can be + * invalid. in order to only process data from valid frames, we add data + * to this queue and only process it once the frame was found to be valid. + * this also handles fragmentation nicely, since there is no need to reset + * our data buffer. we simply update the interpreted data from this event + * queue, which is fine as we know the source frame was valid. + */ + std::deque> _textData; }; + +template class VeDirectFrameHandler; +template class VeDirectFrameHandler; diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHexHandler.cpp b/lib/VeDirectFrameHandler/VeDirectFrameHexHandler.cpp new file mode 100644 index 000000000..392d2f8a9 --- /dev/null +++ b/lib/VeDirectFrameHandler/VeDirectFrameHexHandler.cpp @@ -0,0 +1,226 @@ +/* VeDirectFrame +HexHandler.cpp + * + * Library to read/write from Victron devices using VE.Direct Hex protocol. + * Add on to Victron framehandler reference implementation. + * + * How to use: + * 1. Use sendHexCommand() to send hex messages. Use the Victron documentation to find the parameter. + * 2. The from class "VeDirectFrameHandler" derived class X must overwrite the function + * void VeDirectFrameHandler::hexDataHandler(VeDirectHexData const &data) + * to handle the received hex messages. All hex messages will be forwarted to function hexDataHandler() + * 3. Analyse the content of data (struct VeDirectHexData) to check if a message fits. + * + * 2024.03.08 - 0.4 - adds the ability to send hex commands and to parse hex messages + * + */ +#include +#include "VeDirectFrameHandler.h" + +/* + * calcHexFrameCheckSum() + * help function to calculate the hex checksum + */ +#define ascii2hex(v) (v-48-(v>='A'?7:0)) +#define hex2byte(b) (ascii2hex(*(b)))*16+((ascii2hex(*(b+1)))) +static uint8_t calcHexFrameCheckSum(const char* buffer, int size) { + uint8_t checksum=0x55-ascii2hex(buffer[1]); + for (int i=2; i(strtoul(help, nullptr, 16))); +} + + +/* + * disassembleHexData() + * analysis the hex message and extract: response, address, flags and value/text + * buffer: pointer to message (ascii hex little endian format) + * data: disassembeled message + * return: true = successful disassembeld, false = hex sum fault or message + * do not aligin with VE.Diekt syntax + */ +template +bool VeDirectFrameHandler::disassembleHexData(VeDirectHexData &data) { + bool state = false; + char * buffer = _hexBuffer; + auto len = strlen(buffer); + + // reset hex data first + data = {}; + + if ((len > 3) && (calcHexFrameCheckSum(buffer, len) == 0x00)) { + data.rsp = static_cast(AsciiHexLE2Int(buffer+1, 1)); + + using Response = VeDirectHexResponse; + switch (data.rsp) { + case Response::DONE: + case Response::ERROR: + case Response::PING: + case Response::UNKNOWN: + strncpy(data.text, buffer+2, len-4); + state = true; + break; + case Response::GET: + case Response::SET: + case Response::ASYNC: + data.addr = static_cast(AsciiHexLE2Int(buffer+2, 4)); + + // future option: Up to now we do not use historical data + if ((data.addr >= VeDirectHexRegister::HistoryTotal) && (data.addr <= VeDirectHexRegister::HistoryMPPTD30)) { + state = true; + break; + } + + // future option: to analyse the flags here? + data.flags = AsciiHexLE2Int(buffer+6, 2); + + if (len == 12) { // 8bit value + data.value = AsciiHexLE2Int(buffer+8, 2); + state = true; + } + + if (len == 14) { // 16bit value + data.value = AsciiHexLE2Int(buffer+8, 4); + state = true; + } + + if (len == 18) { // 32bit value + data.value = AsciiHexLE2Int(buffer+8, 8); + state = true; + } + break; + default: + break; // something went wrong + } + } + + if (!state) + _msgOut->printf("%s failed to disassemble the hex message: %s\r\n", _logId, buffer); + + return (state); +} + + +/* + * uint2toHexLEString() + * help function to convert up to 32 bits into little endian hex String + * ascii: pointer to Ascii Hex Little Endian data + * anz: 1,2,4 or 8 nibble + */ +static String Int2HexLEString(uint32_t value, uint8_t anz) { + char hexchar[] = "0123456789ABCDEF"; + char help[9] = {}; + + switch (anz) { + case 1: + help[0] = hexchar[(value & 0x0000000F)]; + break; + case 2: + case 4: + case 8: + for (uint8_t i = 0; i < anz; i += 2) { + help[i] = hexchar[(value>>((1+1*i)*4)) & 0x0000000F]; + help[i+1] = hexchar[(value>>((1*i)*4)) & 0x0000000F]; + } + default: + ; + } + return String(help); +} + + +/* + * sendHexCommand() + * send the hex commend after assembling the command string + * cmd: command + * addr: register address, default 0 + * value: value to write into a register, default 0 + * valsize: size of the value, 8, 16 or 32 bit, default 0 + * return: true = message assembeld and send, false = it was not possible to put the message together + * SAMPLE: ping command: sendHexCommand(PING), + * read total DC input power sendHexCommand(GET, 0xEDEC) + * set Charge current limit 10A sendHexCommand(SET, 0x2015, 64, 16) + * + * WARNING: some values are stored in non-volatile memory. Continuous writing, for example from a control loop, will + * lead to early failure. + * On MPPT for example 0xEDE0 - 0xEDFF. Check the Vivtron doc "BlueSolar-HEX-protocol.pdf" + */ +template +bool VeDirectFrameHandler::sendHexCommand(VeDirectHexCommand cmd, VeDirectHexRegister addr, uint32_t value, uint8_t valsize) { + bool ret = false; + uint8_t flags = 0x00; // always 0x00 + + String txData = ":" + Int2HexLEString(static_cast(cmd), 1); // add the command nibble + + using Command = VeDirectHexCommand; + switch (cmd) { + case Command::PING: + case Command::APP_VERSION: + case Command::PRODUCT_ID: + ret = true; + break; + case Command::GET: + case Command::ASYNC: + txData += Int2HexLEString(static_cast(addr), 4); + txData += Int2HexLEString(flags, 2); // add the flags (2 nibble) + ret = true; + break; + case Command::SET: + txData += Int2HexLEString(static_cast(addr), 4); + txData += Int2HexLEString(flags, 2); // add the flags (2 nibble) + if ((valsize == 8) || (valsize == 16) || (valsize == 32)) { + txData += Int2HexLEString(value, valsize/4); // add value (2-8 nibble) + ret = true; + } + break; + default: + ret = false; + break; + } + + if (ret) { + // add the checksum (2 nibble) + txData += Int2HexLEString(calcHexFrameCheckSum(txData.c_str(), txData.length()), 2); + String send = txData + "\n"; // hex command end byte + _vedirectSerial->write(send.c_str(), send.length()); + + if (_verboseLogging) { + auto blen = _vedirectSerial->availableForWrite(); + _msgOut->printf("%s Sending Hex Command: %s, Free FIFO-Buffer: %u\r\n", + _logId, txData.c_str(), blen); + } + } + + if (!ret) + _msgOut->printf("%s send hex command fault: %s\r\n", _logId, txData.c_str()); + + return (ret); +} diff --git a/lib/VeDirectFrameHandler/VeDirectMpptController.cpp b/lib/VeDirectFrameHandler/VeDirectMpptController.cpp index 4112510f4..f1bd00dec 100644 --- a/lib/VeDirectFrameHandler/VeDirectMpptController.cpp +++ b/lib/VeDirectFrameHandler/VeDirectMpptController.cpp @@ -1,65 +1,82 @@ +/* VeDirectMpptController.cpp + * + * + * 2020.08.20 - 0.0 - ??? + * 2024.03.18 - 0.1 - add of: - temperature from "Smart Battery Sense" connected over VE.Smart network + * - temperature from internal MPPT sensor + * - "total DC input power" from MPPT's connected over VE.Smart network + */ + #include #include "VeDirectMpptController.h" +//#define PROCESS_NETWORK_STATE + void VeDirectMpptController::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort) { - VeDirectFrameHandler::init(rx, tx, msgOut, verboseLogging, hwSerialPort); - _spData = std::make_shared(); - if (_verboseLogging) { _msgOut->println("Finished init MPPTController"); } + VeDirectFrameHandler::init("MPPT", rx, tx, msgOut, verboseLogging, hwSerialPort); } -bool VeDirectMpptController::isDataValid() const { - return VeDirectFrameHandler::isDataValid(*_spData); -} - -void VeDirectMpptController::textRxEvent(char* name, char* value) +bool VeDirectMpptController::processTextDataDerived(std::string const& name, std::string const& value) { - if (VeDirectFrameHandler::textRxEvent("MPPT", name, value, _tmpFrame)) { - return; + if (name == "IL") { + _tmpFrame.loadCurrent_IL_mA = atol(value.c_str()); + return true; } - - if (strcmp(name, "LOAD") == 0) { - if (strcmp(value, "ON") == 0) - _tmpFrame.LOAD = true; - else - _tmpFrame.LOAD = false; + if (name == "LOAD") { + _tmpFrame.loadOutputState_LOAD = (value == "ON"); + return true; } - else if (strcmp(name, "CS") == 0) { - _tmpFrame.CS = atoi(value); + if (name == "CS") { + _tmpFrame.currentState_CS = atoi(value.c_str()); + return true; } - else if (strcmp(name, "ERR") == 0) { - _tmpFrame.ERR = atoi(value); + if (name == "ERR") { + _tmpFrame.errorCode_ERR = atoi(value.c_str()); + return true; } - else if (strcmp(name, "OR") == 0) { - _tmpFrame.OR = strtol(value, nullptr, 0); + if (name == "OR") { + _tmpFrame.offReason_OR = strtol(value.c_str(), nullptr, 0); + return true; } - else if (strcmp(name, "MPPT") == 0) { - _tmpFrame.MPPT = atoi(value); + if (name == "MPPT") { + _tmpFrame.stateOfTracker_MPPT = atoi(value.c_str()); + return true; } - else if (strcmp(name, "HSDS") == 0) { - _tmpFrame.HSDS = atoi(value); + if (name == "HSDS") { + _tmpFrame.daySequenceNr_HSDS = atoi(value.c_str()); + return true; } - else if (strcmp(name, "VPV") == 0) { - _tmpFrame.VPV = round(atof(value) / 10.0) / 100.0; + if (name == "VPV") { + _tmpFrame.panelVoltage_VPV_mV = atol(value.c_str()); + return true; } - else if (strcmp(name, "PPV") == 0) { - _tmpFrame.PPV = atoi(value); + if (name == "PPV") { + _tmpFrame.panelPower_PPV_W = atoi(value.c_str()); + return true; } - else if (strcmp(name, "H19") == 0) { - _tmpFrame.H19 = atof(value) / 100.0; + if (name == "H19") { + _tmpFrame.yieldTotal_H19_Wh = atol(value.c_str()) * 10; + return true; } - else if (strcmp(name, "H20") == 0) { - _tmpFrame.H20 = atof(value) / 100.0; + if (name == "H20") { + _tmpFrame.yieldToday_H20_Wh = atol(value.c_str()) * 10; + return true; } - else if (strcmp(name, "H21") == 0) { - _tmpFrame.H21 = atoi(value); + if (name == "H21") { + _tmpFrame.maxPowerToday_H21_W = atoi(value.c_str()); + return true; } - else if (strcmp(name, "H22") == 0) { - _tmpFrame.H22 = atof(value) / 100.0; + if (name == "H22") { + _tmpFrame.yieldYesterday_H22_Wh = atol(value.c_str()) * 10; + return true; } - else if (strcmp(name, "H23") == 0) { - _tmpFrame.H23 = atoi(value); + if (name == "H23") { + _tmpFrame.maxPowerYesterday_H23_W = atoi(value.c_str()); + return true; } + + return false; } /* @@ -67,110 +84,174 @@ void VeDirectMpptController::textRxEvent(char* name, char* value) * This function is called at the end of the received frame. */ void VeDirectMpptController::frameValidEvent() { - _tmpFrame.P = _tmpFrame.V * _tmpFrame.I; + // power into the battery, (+) means charging, (-) means discharging + _tmpFrame.batteryOutputPower_W = static_cast((_tmpFrame.batteryVoltage_V_mV / 1000.0f) * (_tmpFrame.batteryCurrent_I_mA / 1000.0f)); - _tmpFrame.IPV = 0; - if (_tmpFrame.VPV > 0) { - _tmpFrame.IPV = _tmpFrame.PPV / _tmpFrame.VPV; + // calculation of the panel current + if ((_tmpFrame.panelVoltage_VPV_mV > 0) && (_tmpFrame.panelPower_PPV_W >= 1)) { + _tmpFrame.panelCurrent_mA = static_cast(_tmpFrame.panelPower_PPV_W * 1000000.0f / _tmpFrame.panelVoltage_VPV_mV); + } else { + _tmpFrame.panelCurrent_mA = 0; } - _tmpFrame.E = 0; - if ( _tmpFrame.PPV > 0) { - _efficiency.addNumber(static_cast(_tmpFrame.P * 100) / _tmpFrame.PPV); - _tmpFrame.E = _efficiency.getAverage(); + // calculation of the MPPT efficiency + float totalPower_W = (_tmpFrame.loadCurrent_IL_mA / 1000.0f + _tmpFrame.batteryCurrent_I_mA / 1000.0f) * _tmpFrame.batteryVoltage_V_mV /1000.0f; + if (_tmpFrame.panelPower_PPV_W > 0) { + _efficiency.addNumber(totalPower_W * 100.0f / _tmpFrame.panelPower_PPV_W); + _tmpFrame.mpptEfficiency_Percent = _efficiency.getAverage(); + } else { + _tmpFrame.mpptEfficiency_Percent = 0.0f; } - _spData = std::make_shared(_tmpFrame); - _tmpFrame = {}; - _lastUpdate = millis(); -} + if (!_canSend) { return; } -/* - * getCsAsString - * This function returns the state of operations (CS) as readable text. - */ -frozen::string const& VeDirectMpptController::veMpptStruct::getCsAsString() const -{ - static constexpr frozen::map values = { - { 0, "OFF" }, - { 2, "Fault" }, - { 3, "Bulk" }, - { 4, "Absorbtion" }, - { 5, "Float" }, - { 7, "Equalize (manual)" }, - { 245, "Starting-up" }, - { 247, "Auto equalize / Recondition" }, - { 252, "External Control" } - }; + // Copy from the "VE.Direct Protocol" documentation + // For firmware version v1.52 and below, when no VE.Direct queries are sent to the device, the + // charger periodically sends human readable (TEXT) data to the serial port. For firmware + // versions v1.53 and above, the charger always periodically sends TEXT data to the serial port. + // --> We just use hex commandes for firmware >= 1.53 to keep text messages alive + if (atoi(_tmpFrame.firmwareNr_FW) < 153) { return; } - return getAsString(values, CS); -} + using Command = VeDirectHexCommand; + using Register = VeDirectHexRegister; -/* - * getMpptAsString - * This function returns the state of MPPT (MPPT) as readable text. - */ -frozen::string const& VeDirectMpptController::veMpptStruct::getMpptAsString() const -{ - static constexpr frozen::map values = { - { 0, "OFF" }, - { 1, "Voltage or current limited" }, - { 2, "MPP Tracker active" } - }; + sendHexCommand(Command::GET, Register::ChargeControllerTemperature); + sendHexCommand(Command::GET, Register::SmartBatterySenseTemperature); + sendHexCommand(Command::GET, Register::NetworkTotalDcInputPower); - return getAsString(values, MPPT); +#ifdef PROCESS_NETWORK_STATE + sendHexCommand(Command::GET, Register::NetworkInfo); + sendHexCommand(Command::GET, Register::NetworkMode); + sendHexCommand(Command::GET, Register::NetworkStatus); +#endif // PROCESS_NETWORK_STATE } -/* - * getErrAsString - * This function returns error state (ERR) as readable text. - */ -frozen::string const& VeDirectMpptController::veMpptStruct::getErrAsString() const + +void VeDirectMpptController::loop() { - static constexpr frozen::map values = { - { 0, "No error" }, - { 2, "Battery voltage too high" }, - { 17, "Charger temperature too high" }, - { 18, "Charger over current" }, - { 19, "Charger current reversed" }, - { 20, "Bulk time limit exceeded" }, - { 21, "Current sensor issue(sensor bias/sensor broken)" }, - { 26, "Terminals overheated" }, - { 28, "Converter issue (dual converter models only)" }, - { 33, "Input voltage too high (solar panel)" }, - { 34, "Input current too high (solar panel)" }, - { 38, "Input shutdown (due to excessive battery voltage)" }, - { 39, "Input shutdown (due to current flow during off mode)" }, - { 40, "Input" }, - { 65, "Lost communication with one of devices" }, - { 67, "Synchronisedcharging device configuration issue" }, - { 68, "BMS connection lost" }, - { 116, "Factory calibration data lost" }, - { 117, "Invalid/incompatible firmware" }, - { 118, "User settings invalid" } + VeDirectFrameHandler::loop(); + + auto resetTimestamp = [this](auto& pair) { + if (pair.first > 0 && (millis() - pair.first) > (10 * 1000)) { + pair.first = 0; + } }; - return getAsString(values, ERR); + resetTimestamp(_tmpFrame.MpptTemperatureMilliCelsius); + resetTimestamp(_tmpFrame.SmartBatterySenseTemperatureMilliCelsius); + resetTimestamp(_tmpFrame.NetworkTotalDcInputPowerMilliWatts); + +#ifdef PROCESS_NETWORK_STATE + resetTimestamp(_tmpFrame.NetworkInfo); + resetTimestamp(_tmpFrame.NetworkMode); + resetTimestamp(_tmpFrame.NetworkStatus); +#endif // PROCESS_NETWORK_STATE } + /* - * getOrAsString - * This function returns the off reason (OR) as readable text. + * hexDataHandler() + * analyse the content of VE.Direct hex messages + * Handels the received hex data from the MPPT */ -frozen::string const& VeDirectMpptController::veMpptStruct::getOrAsString() const -{ - static constexpr frozen::map values = { - { 0x00000000, "Not off" }, - { 0x00000001, "No input power" }, - { 0x00000002, "Switched off (power switch)" }, - { 0x00000004, "Switched off (device moderegister)" }, - { 0x00000008, "Remote input" }, - { 0x00000010, "Protection active" }, - { 0x00000020, "Paygo" }, - { 0x00000040, "BMS" }, - { 0x00000080, "Engine shutdown detection" }, - { 0x00000100, "Analysing input voltage" } - }; +bool VeDirectMpptController::hexDataHandler(VeDirectHexData const &data) { + if (data.rsp != VeDirectHexResponse::GET && + data.rsp != VeDirectHexResponse::ASYNC) { return false; } + + auto regLog = static_cast(data.addr); + + switch (data.addr) { + case VeDirectHexRegister::ChargeControllerTemperature: + _tmpFrame.MpptTemperatureMilliCelsius = + { millis(), static_cast(data.value) * 10 }; + + if (_verboseLogging) { + _msgOut->printf("%s Hex Data: MPPT Temperature (0x%04X): %.2f°C\r\n", + _logId, regLog, + _tmpFrame.MpptTemperatureMilliCelsius.second / 1000.0); + } + return true; + break; + + case VeDirectHexRegister::SmartBatterySenseTemperature: + if (data.value == 0xFFFF) { + if (_verboseLogging) { + _msgOut->printf("%s Hex Data: Smart Battery Sense Temperature is not available\r\n", _logId); + } + return true; // we know what to do with it, and we decided to ignore the value + } + + _tmpFrame.SmartBatterySenseTemperatureMilliCelsius = + { millis(), static_cast(data.value) * 10 - 272150 }; + + if (_verboseLogging) { + _msgOut->printf("%s Hex Data: Smart Battery Sense Temperature (0x%04X): %.2f°C\r\n", + _logId, regLog, + _tmpFrame.SmartBatterySenseTemperatureMilliCelsius.second / 1000.0); + } + return true; + break; + + case VeDirectHexRegister::NetworkTotalDcInputPower: + if (data.value == 0xFFFFFFFF) { + if (_verboseLogging) { + _msgOut->printf("%s Hex Data: Network total DC power value " + "indicates non-networked controller\r\n", _logId); + } + _tmpFrame.NetworkTotalDcInputPowerMilliWatts = { 0, 0 }; + return true; // we know what to do with it, and we decided to ignore the value + } + + _tmpFrame.NetworkTotalDcInputPowerMilliWatts = + { millis(), data.value * 10 }; + + if (_verboseLogging) { + _msgOut->printf("%s Hex Data: Network Total DC Power (0x%04X): %.2fW\r\n", + _logId, regLog, + _tmpFrame.NetworkTotalDcInputPowerMilliWatts.second / 1000.0); + } + return true; + break; + +#ifdef PROCESS_NETWORK_STATE + case VeDirectHexRegister::NetworkInfo: + _tmpFrame.NetworkInfo = + { millis(), static_cast(data.value) }; + + if (_verboseLogging) { + _msgOut->printf("%s Hex Data: Network Info (0x%04X): 0x%X\r\n", + _logId, regLog, data.value); + } + return true; + break; + + case VeDirectHexRegister::NetworkMode: + _tmpFrame.NetworkMode = + { millis(), static_cast(data.value) }; + + if (_verboseLogging) { + _msgOut->printf("%s Hex Data: Network Mode (0x%04X): 0x%X\r\n", + _logId, regLog, data.value); + } + return true; + break; + + case VeDirectHexRegister::NetworkStatus: + _tmpFrame.NetworkStatus = + { millis(), static_cast(data.value) }; + + if (_verboseLogging) { + _msgOut->printf("%s Hex Data: Network Status (0x%04X): 0x%X\r\n", + _logId, regLog, data.value); + } + return true; + break; +#endif // PROCESS_NETWORK_STATE + + default: + return false; + break; + } - return getAsString(values, OR); + return false; } diff --git a/lib/VeDirectFrameHandler/VeDirectMpptController.h b/lib/VeDirectFrameHandler/VeDirectMpptController.h index 08574252d..595988985 100644 --- a/lib/VeDirectFrameHandler/VeDirectMpptController.h +++ b/lib/VeDirectFrameHandler/VeDirectMpptController.h @@ -1,6 +1,7 @@ #pragma once #include +#include "VeDirectData.h" #include "VeDirectFrameHandler.h" template @@ -23,9 +24,9 @@ class MovingAverage { _index = (_index + 1) % WINDOW_SIZE; } - double getAverage() const { + float getAverage() const { if (_count == 0) { return 0.0; } - return static_cast(_sum) / _count; + return static_cast(_sum) / _count; } private: @@ -35,43 +36,19 @@ class MovingAverage { size_t _count; }; -class VeDirectMpptController : public VeDirectFrameHandler { +class VeDirectMpptController : public VeDirectFrameHandler { public: VeDirectMpptController() = default; void init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint16_t hwSerialPort); - bool isDataValid() const; // return true if data valid and not outdated - struct veMpptStruct : veStruct { - uint8_t MPPT; // state of MPP tracker - int32_t PPV; // panel power in W - int32_t P; // battery output power in W (calculated) - double VPV; // panel voltage in V - double IPV; // panel current in A (calculated) - bool LOAD; // virtual load output state (on if battery voltage reaches upper limit, off if battery reaches lower limit) - uint8_t CS; // current state of operation e.g. OFF or Bulk - uint8_t ERR; // error code - uint32_t OR; // off reason - uint32_t HSDS; // day sequence number 1...365 - double H19; // yield total kWh - double H20; // yield today kWh - int32_t H21; // maximum power today W - double H22; // yield yesterday kWh - int32_t H23; // maximum power yesterday W + using data_t = veMpptStruct; - frozen::string const& getMpptAsString() const; // state of mppt as string - frozen::string const& getCsAsString() const; // current state as string - frozen::string const& getErrAsString() const; // error state as string - frozen::string const& getOrAsString() const; // off reason as string - }; - - using spData_t = std::shared_ptr; - spData_t getData() const { return _spData; } + void loop() final; private: - void textRxEvent(char* name, char* value) final; + bool hexDataHandler(VeDirectHexData const &data) final; + bool processTextDataDerived(std::string const& name, std::string const& value) final; void frameValidEvent() final; - spData_t _spData = nullptr; - veMpptStruct _tmpFrame{}; // private struct for received name and value pairs - MovingAverage _efficiency; + MovingAverage _efficiency; }; diff --git a/lib/VeDirectFrameHandler/VeDirectShuntController.cpp b/lib/VeDirectFrameHandler/VeDirectShuntController.cpp index 27357a1d1..2d8b85a71 100644 --- a/lib/VeDirectFrameHandler/VeDirectShuntController.cpp +++ b/lib/VeDirectFrameHandler/VeDirectShuntController.cpp @@ -3,110 +3,123 @@ VeDirectShuntController VeDirectShunt; -VeDirectShuntController::VeDirectShuntController() -{ -} - void VeDirectShuntController::init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging) { - VeDirectFrameHandler::init(rx, tx, msgOut, verboseLogging, 2); - if (_verboseLogging) { - _msgOut->println("Finished init ShuntController"); - } + VeDirectFrameHandler::init("SmartShunt", rx, tx, msgOut, verboseLogging, + ((ARDUINO_USB_CDC_ON_BOOT != 1)?2:0)); } -void VeDirectShuntController::textRxEvent(char* name, char* value) +bool VeDirectShuntController::processTextDataDerived(std::string const& name, std::string const& value) { - if (VeDirectFrameHandler::textRxEvent("SmartShunt", name, value, _tmpFrame)) { - return; - } - - if (strcmp(name, "T") == 0) { - _tmpFrame.T = atoi(value); + if (name == "T") { + _tmpFrame.T = atoi(value.c_str()); _tmpFrame.tempPresent = true; + return true; } - else if (strcmp(name, "P") == 0) { - _tmpFrame.P = atoi(value); + if (name == "P") { + _tmpFrame.P = atoi(value.c_str()); + return true; } - else if (strcmp(name, "CE") == 0) { - _tmpFrame.CE = atoi(value); + if (name == "CE") { + _tmpFrame.CE = atoi(value.c_str()); + return true; } - else if (strcmp(name, "SOC") == 0) { - _tmpFrame.SOC = atoi(value); + if (name == "SOC") { + _tmpFrame.SOC = atoi(value.c_str()); + return true; } - else if (strcmp(name, "TTG") == 0) { - _tmpFrame.TTG = atoi(value); + if (name == "TTG") { + _tmpFrame.TTG = atoi(value.c_str()); + return true; } - else if (strcmp(name, "ALARM") == 0) { - _tmpFrame.ALARM = (strcmp(value, "ON") == 0); + if (name == "ALARM") { + _tmpFrame.ALARM = (value == "ON"); + return true; } - else if (strcmp(name, "H1") == 0) { - _tmpFrame.H1 = atoi(value); + if (name == "AR") { + _tmpFrame.alarmReason_AR = atoi(value.c_str()); + return true; } - else if (strcmp(name, "H2") == 0) { - _tmpFrame.H2 = atoi(value); + if (name == "H1") { + _tmpFrame.H1 = atoi(value.c_str()); + return true; } - else if (strcmp(name, "H3") == 0) { - _tmpFrame.H3 = atoi(value); + if (name == "H2") { + _tmpFrame.H2 = atoi(value.c_str()); + return true; } - else if (strcmp(name, "H4") == 0) { - _tmpFrame.H4 = atoi(value); + if (name == "H3") { + _tmpFrame.H3 = atoi(value.c_str()); + return true; } - else if (strcmp(name, "H5") == 0) { - _tmpFrame.H5 = atoi(value); + if (name == "H4") { + _tmpFrame.H4 = atoi(value.c_str()); + return true; } - else if (strcmp(name, "H6") == 0) { - _tmpFrame.H6 = atoi(value); + if (name == "H5") { + _tmpFrame.H5 = atoi(value.c_str()); + return true; } - else if (strcmp(name, "H7") == 0) { - _tmpFrame.H7 = atoi(value); + if (name == "H6") { + _tmpFrame.H6 = atoi(value.c_str()); + return true; } - else if (strcmp(name, "H8") == 0) { - _tmpFrame.H8 = atoi(value); + if (name == "H7") { + _tmpFrame.H7 = atoi(value.c_str()); + return true; } - else if (strcmp(name, "H9") == 0) { - _tmpFrame.H9 = atoi(value); + if (name == "H8") { + _tmpFrame.H8 = atoi(value.c_str()); + return true; } - else if (strcmp(name, "H10") == 0) { - _tmpFrame.H10 = atoi(value); + if (name == "H9") { + _tmpFrame.H9 = atoi(value.c_str()); + return true; } - else if (strcmp(name, "H11") == 0) { - _tmpFrame.H11 = atoi(value); + if (name == "H10") { + _tmpFrame.H10 = atoi(value.c_str()); + return true; } - else if (strcmp(name, "H12") == 0) { - _tmpFrame.H12 = atoi(value); + if (name == "H11") { + _tmpFrame.H11 = atoi(value.c_str()); + return true; } - else if (strcmp(name, "H13") == 0) { - _tmpFrame.H13 = atoi(value); + if (name == "H12") { + _tmpFrame.H12 = atoi(value.c_str()); + return true; } - else if (strcmp(name, "H14") == 0) { - _tmpFrame.H14 = atoi(value); + if (name == "H13") { + _tmpFrame.H13 = atoi(value.c_str()); + return true; } - else if (strcmp(name, "H15") == 0) { - _tmpFrame.H15 = atoi(value); + if (name == "H14") { + _tmpFrame.H14 = atoi(value.c_str()); + return true; } - else if (strcmp(name, "H16") == 0) { - _tmpFrame.H16 = atoi(value); + if (name == "H15") { + _tmpFrame.H15 = atoi(value.c_str()); + return true; } - else if (strcmp(name, "H17") == 0) { - _tmpFrame.H17 = atoi(value); + if (name == "H16") { + _tmpFrame.H16 = atoi(value.c_str()); + return true; } - else if (strcmp(name, "H18") == 0) { - _tmpFrame.H18 = atoi(value); + if (name == "H17") { + _tmpFrame.H17 = atoi(value.c_str()); + return true; } -} - -/* - * frameValidEvent - * This function is called at the end of the received frame. - */ -void VeDirectShuntController::frameValidEvent() { - // other than in the MPPT controller, the SmartShunt seems to split all data - // into two seperate messagesas. Thus we update veFrame only every second message - // after a value for PID has been received - if (_tmpFrame.PID == 0) { return; } - - veFrame = _tmpFrame; - _tmpFrame = {}; - _lastUpdate = millis(); + if (name == "H18") { + _tmpFrame.H18 = atoi(value.c_str()); + return true; + } + if (name == "BMV") { + // This field contains a textual description of the BMV model, + // for example 602S or 702. It is deprecated, refer to the field PID instead. + return true; + } + if (name == "MON") { + _tmpFrame.dcMonitorMode_MON = static_cast(atoi(value.c_str())); + return true; + } + return false; } diff --git a/lib/VeDirectFrameHandler/VeDirectShuntController.h b/lib/VeDirectFrameHandler/VeDirectShuntController.h index 9e1a5f131..03bc96b8b 100644 --- a/lib/VeDirectFrameHandler/VeDirectShuntController.h +++ b/lib/VeDirectFrameHandler/VeDirectShuntController.h @@ -1,49 +1,19 @@ #pragma once #include +#include "VeDirectData.h" #include "VeDirectFrameHandler.h" -class VeDirectShuntController : public VeDirectFrameHandler { +class VeDirectShuntController : public VeDirectFrameHandler { public: - VeDirectShuntController(); + VeDirectShuntController() = default; void init(int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging); - struct veShuntStruct : veStruct { - int32_t T; // Battery temperature - bool tempPresent = false; // Battery temperature sensor is attached to the shunt - int32_t P; // Instantaneous power - int32_t CE; // Consumed Amp Hours - int32_t SOC; // State-of-charge - uint32_t TTG; // Time-to-go - bool ALARM; // Alarm condition active - uint32_t AR; // Alarm Reason - int32_t H1; // Depth of the deepest discharge - int32_t H2; // Depth of the last discharge - int32_t H3; // Depth of the average discharge - int32_t H4; // Number of charge cycles - int32_t H5; // Number of full discharges - int32_t H6; // Cumulative Amp Hours drawn - int32_t H7; // Minimum main (battery) voltage - int32_t H8; // Maximum main (battery) voltage - int32_t H9; // Number of seconds since last full charge - int32_t H10; // Number of automatic synchronizations - int32_t H11; // Number of low main voltage alarms - int32_t H12; // Number of high main voltage alarms - int32_t H13; // Number of low auxiliary voltage alarms - int32_t H14; // Number of high auxiliary voltage alarms - int32_t H15; // Minimum auxiliary (battery) voltage - int32_t H16; // Maximum auxiliary (battery) voltage - int32_t H17; // Amount of discharged energy - int32_t H18; // Amount of charged energy - }; - - veShuntStruct veFrame{}; + using data_t = veShuntStruct; private: - void textRxEvent(char * name, char * value) final; - void frameValidEvent() final; - veShuntStruct _tmpFrame{}; // private struct for received name and value pairs + bool processTextDataDerived(std::string const& name, std::string const& value) final; }; extern VeDirectShuntController VeDirectShunt; diff --git a/patches/async_tcp/event_queue_size.patch b/patches/async_tcp/event_queue_size.patch new file mode 100644 index 000000000..1280d46a8 --- /dev/null +++ b/patches/async_tcp/event_queue_size.patch @@ -0,0 +1,26 @@ +diff --color -ruN a/.pio/libdeps/$$$env$$$/AsyncTCP-esphome/src/AsyncTCP.cpp b/.pio/libdeps/$$$env$$$/AsyncTCP-esphome/src/AsyncTCP.cpp +--- a/.pio/libdeps/$$$env$$$/AsyncTCP-esphome/src/AsyncTCP.cpp ++++ b/.pio/libdeps/$$$env$$$/AsyncTCP-esphome/src/AsyncTCP.cpp +@@ -97,7 +97,7 @@ + + static inline bool _init_async_event_queue(){ + if(!_async_queue){ +- _async_queue = xQueueCreate(32, sizeof(lwip_event_packet_t *)); ++ _async_queue = xQueueCreate(CONFIG_ASYNC_TCP_EVENT_QUEUE_SIZE, sizeof(lwip_event_packet_t *)); + if(!_async_queue){ + return false; + } +diff --color -ruN a/.pio/libdeps/$$$env$$$/AsyncTCP-esphome/src/AsyncTCP.h b/.pio/libdeps/$$$env$$$/AsyncTCP-esphome/src/AsyncTCP.h +--- a/.pio/libdeps/$$$env$$$/AsyncTCP-esphome/src/AsyncTCP.h ++++ b/.pio/libdeps/$$$env$$$/AsyncTCP-esphome/src/AsyncTCP.h +@@ -53,6 +53,10 @@ + #define CONFIG_ASYNC_TCP_STACK_SIZE 8192 * 2 + #endif + ++#ifndef CONFIG_ASYNC_TCP_EVENT_QUEUE_SIZE ++#define CONFIG_ASYNC_TCP_EVENT_QUEUE_SIZE 32 ++#endif ++ + class AsyncClient; + + #define ASYNC_MAX_ACK_TIME 5000 diff --git a/patches/esp32c3/EspAsyncWebserver.patch b/patches/esp32c3/EspAsyncWebserver.patch deleted file mode 100644 index 079c164d4..000000000 --- a/patches/esp32c3/EspAsyncWebserver.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/.pio/libdeps/$$$env$$$/ESP Async WebServer/src/AsyncWebSocket.cpp b/.pio/libdeps/$$$env$$$/ESP Async WebServer/src/AsyncWebSocket.cpp -index 12be5f8..8505f73 100644 ---- a/.pio/libdeps/$$$env$$$/ESP Async WebServer/src/AsyncWebSocket.cpp -+++ b/.pio/libdeps/$$$env$$$/ESP Async WebServer/src/AsyncWebSocket.cpp -@@ -737,7 +737,7 @@ void AsyncWebSocketClient::binary(const __FlashStringHelper *data, size_t len) - IPAddress AsyncWebSocketClient::remoteIP() const - { - if (!_client) -- return IPAddress(0U); -+ return IPAddress((uint32_t)0); - - return _client->remoteIP(); - } diff --git a/platformio.ini b/platformio.ini index ce9998ae4..b8b248a66 100644 --- a/platformio.ini +++ b/platformio.ini @@ -19,12 +19,13 @@ extra_configs = custom_ci_action = generic,generic_esp32,generic_esp32s3,generic_esp32s3_usb framework = arduino -platform = espressif32@6.5.0 +platform = espressif32@6.6.0 build_flags = -DPIOENV=\"$PIOENV\" -D_TASK_STD_FUNCTION=1 -D_TASK_THREAD_SAFE=1 + -DCONFIG_ASYNC_TCP_EVENT_QUEUE_SIZE=128 -Wall -Wextra -Wunused -Wmisleading-indentation -Wduplicated-cond -Wlogical-op -Wnull-dereference ; Have to remove -Werror because of ; https://github.com/espressif/arduino-esp32/issues/9044 and @@ -36,11 +37,11 @@ build_unflags = -std=gnu++11 lib_deps = - mathieucarbou/ESP Async WebServer @ 2.8.1 - bblanchon/ArduinoJson @ ^6.21.5 + mathieucarbou/ESP Async WebServer @ 2.9.3 + bblanchon/ArduinoJson @ ^7.0.4 https://github.com/bertmelis/espMqttClient.git#v1.6.0 nrf24/RF24 @ ^1.4.8 - olikraus/U8g2 @ ^2.35.15 + olikraus/U8g2 @ ^2.35.17 buelowp/sunset @ ^1.1.7 https://github.com/arkhipenko/TaskScheduler#testing https://github.com/coryjfowler/MCP_CAN_lib @@ -64,7 +65,7 @@ board_build.embed_files = webapp_dist/js/app.js.gz webapp_dist/site.webmanifest -custom_patches = +custom_patches = async_tcp monitor_filters = esp32_exception_decoder, time, log2file, colorize monitor_speed = 115200 @@ -92,13 +93,13 @@ build_flags = ${env.build_flags} [env:generic_esp32c3] board = esp32-c3-devkitc-02 -custom_patches = ${env.custom_patches},esp32c3 +custom_patches = ${env.custom_patches} build_flags = ${env.build_flags} [env:generic_esp32c3_usb] board = esp32-c3-devkitc-02 -custom_patches = ${env.custom_patches},esp32c3 +custom_patches = ${env.custom_patches} build_flags = ${env.build_flags} -DARDUINO_USB_MODE=1 -DARDUINO_USB_CDC_ON_BOOT=1 diff --git a/src/BatteryStats.cpp b/src/BatteryStats.cpp index 563562c82..8992efabc 100644 --- a/src/BatteryStats.cpp +++ b/src/BatteryStats.cpp @@ -61,8 +61,8 @@ bool BatteryStats::updateAvailable(uint32_t since) const void BatteryStats::getLiveViewData(JsonVariant& root) const { - root[F("manufacturer")] = _manufacturer; - root[F("data_age")] = getAgeSeconds(); + root["manufacturer"] = _manufacturer; + root["data_age"] = getAgeSeconds(); addLiveViewValue(root, "SoC", _soc, "%", _socPrecision); addLiveViewValue(root, "voltage", _voltage, "V", 2); @@ -218,39 +218,39 @@ uint32_t BatteryStats::getMqttFullPublishIntervalMs() const 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/voltage"), String(_voltage)); + MqttSettings.publish("battery/manufacturer", _manufacturer); + MqttSettings.publish("battery/dataAge", String(getAgeSeconds())); + MqttSettings.publish("battery/stateOfCharge", String(_soc)); + MqttSettings.publish("battery/voltage", String(_voltage)); } void PylontechBatteryStats::mqttPublish() const { BatteryStats::mqttPublish(); - MqttSettings.publish(F("battery/settings/chargeVoltage"), String(_chargeVoltage)); - 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/current"), String(_current)); - MqttSettings.publish(F("battery/temperature"), String(_temperature)); - MqttSettings.publish(F("battery/alarm/overCurrentDischarge"), String(_alarmOverCurrentDischarge)); - MqttSettings.publish(F("battery/alarm/overCurrentCharge"), String(_alarmOverCurrentCharge)); - MqttSettings.publish(F("battery/alarm/underTemperature"), String(_alarmUnderTemperature)); - MqttSettings.publish(F("battery/alarm/overTemperature"), String(_alarmOverTemperature)); - MqttSettings.publish(F("battery/alarm/underVoltage"), String(_alarmUnderVoltage)); - MqttSettings.publish(F("battery/alarm/overVoltage"), String(_alarmOverVoltage)); - MqttSettings.publish(F("battery/alarm/bmsInternal"), String(_alarmBmsInternal)); - MqttSettings.publish(F("battery/warning/highCurrentDischarge"), String(_warningHighCurrentDischarge)); - MqttSettings.publish(F("battery/warning/highCurrentCharge"), String(_warningHighCurrentCharge)); - MqttSettings.publish(F("battery/warning/lowTemperature"), String(_warningLowTemperature)); - MqttSettings.publish(F("battery/warning/highTemperature"), String(_warningHighTemperature)); - MqttSettings.publish(F("battery/warning/lowVoltage"), String(_warningLowVoltage)); - MqttSettings.publish(F("battery/warning/highVoltage"), String(_warningHighVoltage)); - MqttSettings.publish(F("battery/warning/bmsInternal"), String(_warningBmsInternal)); - MqttSettings.publish(F("battery/charging/chargeEnabled"), String(_chargeEnabled)); - MqttSettings.publish(F("battery/charging/dischargeEnabled"), String(_dischargeEnabled)); - MqttSettings.publish(F("battery/charging/chargeImmediately"), String(_chargeImmediately)); + MqttSettings.publish("battery/settings/chargeVoltage", String(_chargeVoltage)); + MqttSettings.publish("battery/settings/chargeCurrentLimitation", String(_chargeCurrentLimitation)); + MqttSettings.publish("battery/settings/dischargeCurrentLimitation", String(_dischargeCurrentLimitation)); + MqttSettings.publish("battery/stateOfHealth", String(_stateOfHealth)); + MqttSettings.publish("battery/current", String(_current)); + MqttSettings.publish("battery/temperature", String(_temperature)); + MqttSettings.publish("battery/alarm/overCurrentDischarge", String(_alarmOverCurrentDischarge)); + MqttSettings.publish("battery/alarm/overCurrentCharge", String(_alarmOverCurrentCharge)); + MqttSettings.publish("battery/alarm/underTemperature", String(_alarmUnderTemperature)); + MqttSettings.publish("battery/alarm/overTemperature", String(_alarmOverTemperature)); + MqttSettings.publish("battery/alarm/underVoltage", String(_alarmUnderVoltage)); + MqttSettings.publish("battery/alarm/overVoltage", String(_alarmOverVoltage)); + MqttSettings.publish("battery/alarm/bmsInternal", String(_alarmBmsInternal)); + MqttSettings.publish("battery/warning/highCurrentDischarge", String(_warningHighCurrentDischarge)); + MqttSettings.publish("battery/warning/highCurrentCharge", String(_warningHighCurrentCharge)); + MqttSettings.publish("battery/warning/lowTemperature", String(_warningLowTemperature)); + MqttSettings.publish("battery/warning/highTemperature", String(_warningHighTemperature)); + MqttSettings.publish("battery/warning/lowVoltage", String(_warningLowVoltage)); + MqttSettings.publish("battery/warning/highVoltage", String(_warningHighVoltage)); + MqttSettings.publish("battery/warning/bmsInternal", String(_warningBmsInternal)); + MqttSettings.publish("battery/charging/chargeEnabled", String(_chargeEnabled)); + MqttSettings.publish("battery/charging/dischargeEnabled", String(_dischargeEnabled)); + MqttSettings.publish("battery/charging/chargeImmediately", String(_chargeImmediately)); } void JkBmsBatteryStats::mqttPublish() const @@ -333,7 +333,12 @@ void JkBmsBatteryStats::updateFrom(JkBms::DataPointContainer const& dp) _manufacturer = "JKBMS"; auto oProductId = dp.get(); if (oProductId.has_value()) { - _manufacturer = oProductId->c_str(); + // the first twelve chars are expected to be the "User Private Data" + // setting (see smartphone app). the remainder is expected be the BMS + // name, which can be changed at will using the smartphone app. so + // there is not always a "JK" in this string. if there is, we still cut + // the string there to avoid possible regressions. + _manufacturer = oProductId->substr(12).c_str(); auto pos = oProductId->rfind("JK"); if (pos != std::string::npos) { _manufacturer = oProductId->substr(pos).c_str(); @@ -373,11 +378,11 @@ void JkBmsBatteryStats::updateFrom(JkBms::DataPointContainer const& dp) _lastUpdate = millis(); } -void VictronSmartShuntStats::updateFrom(VeDirectShuntController::veShuntStruct const& shuntData) { - BatteryStats::setVoltage(shuntData.V, millis()); +void VictronSmartShuntStats::updateFrom(VeDirectShuntController::data_t const& shuntData) { + BatteryStats::setVoltage(shuntData.batteryVoltage_V_mV / 1000.0, millis()); BatteryStats::setSoC(static_cast(shuntData.SOC) / 10, 1/*precision*/, millis()); - _current = shuntData.I; + _current = static_cast(shuntData.batteryCurrent_I_mA) / 1000; _modelName = shuntData.getPidAsString().data(); _chargeCycles = shuntData.H4; _timeToGo = shuntData.TTG / 60; @@ -390,11 +395,11 @@ void VictronSmartShuntStats::updateFrom(VeDirectShuntController::veShuntStruct c _consumedAmpHours = static_cast(shuntData.CE) / 1000; _lastFullCharge = shuntData.H9 / 60; // shuntData.AR is a bitfield, so we need to check each bit individually - _alarmLowVoltage = shuntData.AR & 1; - _alarmHighVoltage = shuntData.AR & 2; - _alarmLowSOC = shuntData.AR & 4; - _alarmLowTemperature = shuntData.AR & 32; - _alarmHighTemperature = shuntData.AR & 64; + _alarmLowVoltage = shuntData.alarmReason_AR & 1; + _alarmHighVoltage = shuntData.alarmReason_AR & 2; + _alarmLowSOC = shuntData.alarmReason_AR & 4; + _alarmLowTemperature = shuntData.alarmReason_AR & 32; + _alarmHighTemperature = shuntData.alarmReason_AR & 64; _lastUpdate = VeDirectShunt.getLastUpdate(); } @@ -424,11 +429,11 @@ void VictronSmartShuntStats::getLiveViewData(JsonVariant& root) const { void VictronSmartShuntStats::mqttPublish() const { BatteryStats::mqttPublish(); - MqttSettings.publish(F("battery/current"), String(_current)); - MqttSettings.publish(F("battery/chargeCycles"), String(_chargeCycles)); - MqttSettings.publish(F("battery/chargedEnergy"), String(_chargedEnergy)); - MqttSettings.publish(F("battery/dischargedEnergy"), String(_dischargedEnergy)); - MqttSettings.publish(F("battery/instantaneousPower"), String(_instantaneousPower)); - MqttSettings.publish(F("battery/consumedAmpHours"), String(_consumedAmpHours)); - MqttSettings.publish(F("battery/lastFullCharge"), String(_lastFullCharge)); + MqttSettings.publish("battery/current", String(_current)); + MqttSettings.publish("battery/chargeCycles", String(_chargeCycles)); + MqttSettings.publish("battery/chargedEnergy", String(_chargedEnergy)); + MqttSettings.publish("battery/dischargedEnergy", String(_dischargedEnergy)); + MqttSettings.publish("battery/instantaneousPower", String(_instantaneousPower)); + MqttSettings.publish("battery/consumedAmpHours", String(_consumedAmpHours)); + MqttSettings.publish("battery/lastFullCharge", String(_lastFullCharge)); } diff --git a/src/Configuration.cpp b/src/Configuration.cpp index c00617cf7..70d0ba813 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -25,17 +25,13 @@ bool ConfigurationClass::write() } config.Cfg.SaveCount++; - DynamicJsonDocument doc(JSON_BUFFER_SIZE); + JsonDocument doc; - if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) { - return false; - } - - JsonObject cfg = doc.createNestedObject("cfg"); + JsonObject cfg = doc["cfg"].to(); cfg["version"] = config.Cfg.Version; cfg["save_count"] = config.Cfg.SaveCount; - JsonObject wifi = doc.createNestedObject("wifi"); + JsonObject wifi = doc["wifi"].to(); wifi["ssid"] = config.WiFi.Ssid; wifi["password"] = config.WiFi.Password; wifi["ip"] = IPAddress(config.WiFi.Ip).toString(); @@ -47,10 +43,10 @@ bool ConfigurationClass::write() wifi["hostname"] = config.WiFi.Hostname; wifi["aptimeout"] = config.WiFi.ApTimeout; - JsonObject mdns = doc.createNestedObject("mdns"); + JsonObject mdns = doc["mdns"].to(); mdns["enabled"] = config.Mdns.Enabled; - JsonObject ntp = doc.createNestedObject("ntp"); + JsonObject ntp = doc["ntp"].to(); ntp["server"] = config.Ntp.Server; ntp["timezone"] = config.Ntp.Timezone; ntp["timezone_descr"] = config.Ntp.TimezoneDescr; @@ -58,7 +54,7 @@ bool ConfigurationClass::write() ntp["longitude"] = config.Ntp.Longitude; ntp["sunsettype"] = config.Ntp.SunsetType; - JsonObject mqtt = doc.createNestedObject("mqtt"); + JsonObject mqtt = doc["mqtt"].to(); mqtt["enabled"] = config.Mqtt.Enabled; mqtt["verbose_logging"] = config.Mqtt.VerboseLogging; mqtt["hostname"] = config.Mqtt.Hostname; @@ -70,27 +66,27 @@ bool ConfigurationClass::write() mqtt["publish_interval"] = config.Mqtt.PublishInterval; mqtt["clean_session"] = config.Mqtt.CleanSession; - JsonObject mqtt_lwt = mqtt.createNestedObject("lwt"); + JsonObject mqtt_lwt = mqtt["lwt"].to(); mqtt_lwt["topic"] = config.Mqtt.Lwt.Topic; mqtt_lwt["value_online"] = config.Mqtt.Lwt.Value_Online; mqtt_lwt["value_offline"] = config.Mqtt.Lwt.Value_Offline; mqtt_lwt["qos"] = config.Mqtt.Lwt.Qos; - JsonObject mqtt_tls = mqtt.createNestedObject("tls"); + JsonObject mqtt_tls = mqtt["tls"].to(); mqtt_tls["enabled"] = config.Mqtt.Tls.Enabled; mqtt_tls["root_ca_cert"] = config.Mqtt.Tls.RootCaCert; mqtt_tls["certlogin"] = config.Mqtt.Tls.CertLogin; mqtt_tls["client_cert"] = config.Mqtt.Tls.ClientCert; mqtt_tls["client_key"] = config.Mqtt.Tls.ClientKey; - JsonObject mqtt_hass = mqtt.createNestedObject("hass"); + JsonObject mqtt_hass = mqtt["hass"].to(); mqtt_hass["enabled"] = config.Mqtt.Hass.Enabled; mqtt_hass["retain"] = config.Mqtt.Hass.Retain; mqtt_hass["topic"] = config.Mqtt.Hass.Topic; mqtt_hass["individual_panels"] = config.Mqtt.Hass.IndividualPanels; mqtt_hass["expire"] = config.Mqtt.Hass.Expire; - JsonObject dtu = doc.createNestedObject("dtu"); + JsonObject dtu = doc["dtu"].to(); dtu["serial"] = config.Dtu.Serial; dtu["poll_interval"] = config.Dtu.PollInterval; dtu["verbose_logging"] = config.Dtu.VerboseLogging; @@ -99,14 +95,14 @@ bool ConfigurationClass::write() dtu["cmt_frequency"] = config.Dtu.Cmt.Frequency; dtu["cmt_country_mode"] = config.Dtu.Cmt.CountryMode; - JsonObject security = doc.createNestedObject("security"); + JsonObject security = doc["security"].to(); security["password"] = config.Security.Password; security["allow_readonly"] = config.Security.AllowReadonly; - JsonObject device = doc.createNestedObject("device"); + JsonObject device = doc["device"].to(); device["pinmapping"] = config.Dev_PinMapping; - JsonObject display = device.createNestedObject("display"); + JsonObject display = device["display"].to(); display["powersafe"] = config.Display.PowerSafe; display["screensaver"] = config.Display.ScreenSaver; display["rotation"] = config.Display.Rotation; @@ -115,15 +111,15 @@ bool ConfigurationClass::write() display["diagram_duration"] = config.Display.Diagram.Duration; display["diagram_mode"] = config.Display.Diagram.Mode; - JsonArray leds = device.createNestedArray("led"); + JsonArray leds = device["led"].to(); for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) { - JsonObject led = leds.createNestedObject(); + JsonObject led = leds.add(); led["brightness"] = config.Led_Single[i].Brightness; } - JsonArray inverters = doc.createNestedArray("inverters"); + JsonArray inverters = doc["inverters"].to(); for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { - JsonObject inv = inverters.createNestedObject(); + JsonObject inv = inverters.add(); inv["serial"] = config.Inverter[i].Serial; inv["name"] = config.Inverter[i].Name; inv["order"] = config.Inverter[i].Order; @@ -136,21 +132,21 @@ bool ConfigurationClass::write() inv["zero_day"] = config.Inverter[i].ZeroYieldDayOnMidnight; inv["yieldday_correction"] = config.Inverter[i].YieldDayCorrection; - JsonArray channel = inv.createNestedArray("channel"); + JsonArray channel = inv["channel"].to(); for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { - JsonObject chanData = channel.createNestedObject(); + JsonObject chanData = channel.add(); chanData["name"] = config.Inverter[i].channel[c].Name; chanData["max_power"] = config.Inverter[i].channel[c].MaxChannelPower; chanData["yield_total_offset"] = config.Inverter[i].channel[c].YieldTotalOffset; } } - JsonObject vedirect = doc.createNestedObject("vedirect"); + JsonObject vedirect = doc["vedirect"].to(); vedirect["enabled"] = config.Vedirect.Enabled; vedirect["verbose_logging"] = config.Vedirect.VerboseLogging; vedirect["updates_only"] = config.Vedirect.UpdatesOnly; - JsonObject powermeter = doc.createNestedObject("powermeter"); + JsonObject powermeter = doc["powermeter"].to(); powermeter["enabled"] = config.PowerMeter.Enabled; powermeter["verbose_logging"] = config.PowerMeter.VerboseLogging; powermeter["interval"] = config.PowerMeter.Interval; @@ -162,9 +158,9 @@ bool ConfigurationClass::write() powermeter["sdmaddress"] = config.PowerMeter.SdmAddress; powermeter["http_individual_requests"] = config.PowerMeter.HttpIndividualRequests; - JsonArray powermeter_http_phases = powermeter.createNestedArray("http_phases"); + JsonArray powermeter_http_phases = powermeter["http_phases"].to(); for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) { - JsonObject powermeter_phase = powermeter_http_phases.createNestedObject(); + JsonObject powermeter_phase = powermeter_http_phases.add(); powermeter_phase["enabled"] = config.PowerMeter.Http_Phase[i].Enabled; powermeter_phase["url"] = config.PowerMeter.Http_Phase[i].Url; @@ -175,9 +171,11 @@ bool ConfigurationClass::write() powermeter_phase["header_value"] = config.PowerMeter.Http_Phase[i].HeaderValue; powermeter_phase["timeout"] = config.PowerMeter.Http_Phase[i].Timeout; powermeter_phase["json_path"] = config.PowerMeter.Http_Phase[i].JsonPath; + powermeter_phase["unit"] = config.PowerMeter.Http_Phase[i].PowerUnit; + powermeter_phase["sign_inverted"] = config.PowerMeter.Http_Phase[i].SignInverted; } - JsonObject powerlimiter = doc.createNestedObject("powerlimiter"); + JsonObject powerlimiter = doc["powerlimiter"].to(); powerlimiter["enabled"] = config.PowerLimiter.Enabled; powerlimiter["verbose_logging"] = config.PowerLimiter.VerboseLogging; powerlimiter["solar_passtrough_enabled"] = config.PowerLimiter.SolarPassThroughEnabled; @@ -191,6 +189,7 @@ bool ConfigurationClass::write() powerlimiter["target_power_consumption"] = config.PowerLimiter.TargetPowerConsumption; powerlimiter["target_power_consumption_hysteresis"] = config.PowerLimiter.TargetPowerConsumptionHysteresis; powerlimiter["lower_power_limit"] = config.PowerLimiter.LowerPowerLimit; + powerlimiter["base_load_limit"] = config.PowerLimiter.BaseLoadLimit; powerlimiter["upper_power_limit"] = config.PowerLimiter.UpperPowerLimit; powerlimiter["ignore_soc"] = config.PowerLimiter.IgnoreSoc; powerlimiter["battery_soc_start_threshold"] = config.PowerLimiter.BatterySocStartThreshold; @@ -203,7 +202,7 @@ bool ConfigurationClass::write() powerlimiter["full_solar_passthrough_start_voltage"] = config.PowerLimiter.FullSolarPassThroughStartVoltage; powerlimiter["full_solar_passthrough_stop_voltage"] = config.PowerLimiter.FullSolarPassThroughStopVoltage; - JsonObject battery = doc.createNestedObject("battery"); + JsonObject battery = doc["battery"].to(); battery["enabled"] = config.Battery.Enabled; battery["verbose_logging"] = config.Battery.VerboseLogging; battery["provider"] = config.Battery.Provider; @@ -212,14 +211,23 @@ bool ConfigurationClass::write() battery["mqtt_topic"] = config.Battery.MqttSocTopic; battery["mqtt_voltage_topic"] = config.Battery.MqttVoltageTopic; - JsonObject huawei = doc.createNestedObject("huawei"); + JsonObject huawei = doc["huawei"].to(); huawei["enabled"] = config.Huawei.Enabled; + huawei["verbose_logging"] = config.Huawei.VerboseLogging; huawei["can_controller_frequency"] = config.Huawei.CAN_Controller_Frequency; huawei["auto_power_enabled"] = config.Huawei.Auto_Power_Enabled; + huawei["auto_power_batterysoc_limits_enabled"] = config.Huawei.Auto_Power_BatterySoC_Limits_Enabled; + huawei["emergency_charge_enabled"] = config.Huawei.Emergency_Charge_Enabled; huawei["voltage_limit"] = config.Huawei.Auto_Power_Voltage_Limit; huawei["enable_voltage_limit"] = config.Huawei.Auto_Power_Enable_Voltage_Limit; huawei["lower_power_limit"] = config.Huawei.Auto_Power_Lower_Power_Limit; huawei["upper_power_limit"] = config.Huawei.Auto_Power_Upper_Power_Limit; + huawei["stop_batterysoc_threshold"] = config.Huawei.Auto_Power_Stop_BatterySoC_Threshold; + huawei["target_power_consumption"] = config.Huawei.Auto_Power_Target_Power_Consumption; + + if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) { + return false; + } // Serialize JSON to file if (serializeJson(doc, f) == 0) { @@ -235,11 +243,7 @@ bool ConfigurationClass::read() { File f = LittleFS.open(CONFIG_FILENAME, "r", false); - DynamicJsonDocument doc(JSON_BUFFER_SIZE); - - if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) { - return false; - } + JsonDocument doc; // Deserialize the JSON document const DeserializationError error = deserializeJson(doc, f); @@ -247,6 +251,10 @@ bool ConfigurationClass::read() MessageOutput.println("Failed to read file, using default configuration"); } + if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) { + return false; + } + JsonObject cfg = doc["cfg"]; config.Cfg.Version = cfg["version"] | CONFIG_VERSION; config.Cfg.SaveCount = cfg["save_count"] | 0; @@ -415,13 +423,15 @@ bool ConfigurationClass::read() config.PowerMeter.Http_Phase[i].Enabled = powermeter_phase["enabled"] | (i == 0); strlcpy(config.PowerMeter.Http_Phase[i].Url, powermeter_phase["url"] | "", sizeof(config.PowerMeter.Http_Phase[i].Url)); - config.PowerMeter.Http_Phase[i].AuthType = powermeter_phase["auth_type"] | Auth::none; + config.PowerMeter.Http_Phase[i].AuthType = powermeter_phase["auth_type"] | PowerMeterHttpConfig::Auth::None; strlcpy(config.PowerMeter.Http_Phase[i].Username, powermeter_phase["username"] | "", sizeof(config.PowerMeter.Http_Phase[i].Username)); strlcpy(config.PowerMeter.Http_Phase[i].Password, powermeter_phase["password"] | "", sizeof(config.PowerMeter.Http_Phase[i].Password)); strlcpy(config.PowerMeter.Http_Phase[i].HeaderKey, powermeter_phase["header_key"] | "", sizeof(config.PowerMeter.Http_Phase[i].HeaderKey)); strlcpy(config.PowerMeter.Http_Phase[i].HeaderValue, powermeter_phase["header_value"] | "", sizeof(config.PowerMeter.Http_Phase[i].HeaderValue)); config.PowerMeter.Http_Phase[i].Timeout = powermeter_phase["timeout"] | POWERMETER_HTTP_TIMEOUT; strlcpy(config.PowerMeter.Http_Phase[i].JsonPath, powermeter_phase["json_path"] | "", sizeof(config.PowerMeter.Http_Phase[i].JsonPath)); + config.PowerMeter.Http_Phase[i].PowerUnit = powermeter_phase["unit"] | PowerMeterHttpConfig::Unit::Watts; + config.PowerMeter.Http_Phase[i].SignInverted = powermeter_phase["sign_inverted"] | false; } JsonObject powerlimiter = doc["powerlimiter"]; @@ -439,6 +449,7 @@ bool ConfigurationClass::read() config.PowerLimiter.TargetPowerConsumption = powerlimiter["target_power_consumption"] | POWERLIMITER_TARGET_POWER_CONSUMPTION; config.PowerLimiter.TargetPowerConsumptionHysteresis = powerlimiter["target_power_consumption_hysteresis"] | POWERLIMITER_TARGET_POWER_CONSUMPTION_HYSTERESIS; config.PowerLimiter.LowerPowerLimit = powerlimiter["lower_power_limit"] | POWERLIMITER_LOWER_POWER_LIMIT; + config.PowerLimiter.BaseLoadLimit = powerlimiter["base_load_limit"] | POWERLIMITER_BASE_LOAD_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; @@ -462,12 +473,17 @@ bool ConfigurationClass::read() JsonObject huawei = doc["huawei"]; config.Huawei.Enabled = huawei["enabled"] | HUAWEI_ENABLED; + config.Huawei.VerboseLogging = huawei["verbose_logging"] | VERBOSE_LOGGING; config.Huawei.CAN_Controller_Frequency = huawei["can_controller_frequency"] | HUAWEI_CAN_CONTROLLER_FREQUENCY; config.Huawei.Auto_Power_Enabled = huawei["auto_power_enabled"] | false; + config.Huawei.Auto_Power_BatterySoC_Limits_Enabled = huawei["auto_power_batterysoc_limits_enabled"] | false; + config.Huawei.Emergency_Charge_Enabled = huawei["emergency_charge_enabled"] | false; config.Huawei.Auto_Power_Voltage_Limit = huawei["voltage_limit"] | HUAWEI_AUTO_POWER_VOLTAGE_LIMIT; config.Huawei.Auto_Power_Enable_Voltage_Limit = huawei["enable_voltage_limit"] | HUAWEI_AUTO_POWER_ENABLE_VOLTAGE_LIMIT; config.Huawei.Auto_Power_Lower_Power_Limit = huawei["lower_power_limit"] | HUAWEI_AUTO_POWER_LOWER_POWER_LIMIT; config.Huawei.Auto_Power_Upper_Power_Limit = huawei["upper_power_limit"] | HUAWEI_AUTO_POWER_UPPER_POWER_LIMIT; + config.Huawei.Auto_Power_Stop_BatterySoC_Threshold = huawei["stop_batterysoc_threshold"] | HUAWEI_AUTO_POWER_STOP_BATTERYSOC_THRESHOLD; + config.Huawei.Auto_Power_Target_Power_Consumption = huawei["target_power_consumption"] | HUAWEI_AUTO_POWER_TARGET_POWER_CONSUMPTION; f.close(); return true; @@ -481,11 +497,7 @@ void ConfigurationClass::migrate() return; } - DynamicJsonDocument doc(JSON_BUFFER_SIZE); - - if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) { - return; - } + JsonDocument doc; // Deserialize the JSON document const DeserializationError error = deserializeJson(doc, f); @@ -494,6 +506,10 @@ void ConfigurationClass::migrate() return; } + if (!Utils::checkJsonAlloc(doc, __FUNCTION__, __LINE__)) { + return; + } + if (config.Cfg.Version < 0x00011700) { JsonArray inverters = doc["inverters"]; for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { @@ -529,6 +545,12 @@ void ConfigurationClass::migrate() config.Dtu.Cmt.Frequency *= 1000; } + if (config.Cfg.Version < 0x00011c00) { + if (!strcmp(config.Ntp.Server, NTP_SERVER_OLD)) { + strlcpy(config.Ntp.Server, NTP_SERVER, sizeof(config.Ntp.Server)); + } + } + f.close(); config.Cfg.Version = CONFIG_VERSION; diff --git a/src/HttpPowerMeter.cpp b/src/HttpPowerMeter.cpp index cb829e1d7..cdb1253f3 100644 --- a/src/HttpPowerMeter.cpp +++ b/src/HttpPowerMeter.cpp @@ -16,15 +16,17 @@ void HttpPowerMeterClass::init() float HttpPowerMeterClass::getPower(int8_t phase) { + if (phase < 1 || phase > POWERMETER_MAX_PHASES) { return 0.0; } + return power[phase - 1]; } bool HttpPowerMeterClass::updateValues() { - const CONFIG_T& config = Configuration.get(); + auto const& config = Configuration.get(); for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) { - POWERMETER_HTTP_PHASE_CONFIG_T phaseConfig = config.PowerMeter.Http_Phase[i]; + auto const& phaseConfig = config.PowerMeter.Http_Phase[i]; if (!phaseConfig.Enabled) { power[i] = 0.0; @@ -32,8 +34,7 @@ bool HttpPowerMeterClass::updateValues() } if (i == 0 || config.PowerMeter.HttpIndividualRequests) { - if (!queryPhase(i, phaseConfig.Url, phaseConfig.AuthType, phaseConfig.Username, phaseConfig.Password, phaseConfig.HeaderKey, phaseConfig.HeaderValue, phaseConfig.Timeout, - phaseConfig.JsonPath)) { + if (!queryPhase(i, phaseConfig)) { MessageOutput.printf("[HttpPowerMeter] Getting the power of phase %d failed.\r\n", i + 1); MessageOutput.printf("%s\r\n", httpPowerMeterError); return false; @@ -41,7 +42,7 @@ bool HttpPowerMeterClass::updateValues() continue; } - if(!tryGetFloatValueForPhase(i, phaseConfig.JsonPath)) { + if(!tryGetFloatValueForPhase(i, phaseConfig.JsonPath, phaseConfig.PowerUnit, phaseConfig.SignInverted)) { MessageOutput.printf("[HttpPowerMeter] Getting the power of phase %d (from JSON fetched with Phase 1 config) failed.\r\n", i + 1); MessageOutput.printf("%s\r\n", httpPowerMeterError); return false; @@ -50,8 +51,7 @@ bool HttpPowerMeterClass::updateValues() return true; } -bool HttpPowerMeterClass::queryPhase(int phase, const String& url, Auth authType, const char* username, const char* password, - const char* httpHeader, const char* httpValue, uint32_t timeout, const char* jsonPath) +bool HttpPowerMeterClass::queryPhase(int phase, PowerMeterHttpConfig const& config) { //hostByName in WiFiGeneric fails to resolve local names. issue described in //https://github.com/espressif/arduino-esp32/issues/3822 @@ -63,7 +63,7 @@ bool HttpPowerMeterClass::queryPhase(int phase, const String& url, Auth authType String uri; String base64Authorization; uint16_t port; - extractUrlComponents(url, protocol, host, uri, port, base64Authorization); + extractUrlComponents(config.Url, protocol, host, uri, port, base64Authorization); IPAddress ipaddr((uint32_t)0); //first check if "host" is already an IP adress @@ -105,43 +105,42 @@ bool HttpPowerMeterClass::queryPhase(int phase, const String& url, Auth authType wifiClient = std::make_unique(); } - return httpRequest(phase, *wifiClient, ipaddr.toString(), port, uri, https, authType, username, password, httpHeader, httpValue, timeout, jsonPath); + return httpRequest(phase, *wifiClient, ipaddr.toString(), port, uri, https, config); } -bool HttpPowerMeterClass::httpRequest(int phase, WiFiClient &wifiClient, const String& host, uint16_t port, const String& uri, bool https, Auth authType, const char* username, - const char* password, const char* httpHeader, const char* httpValue, uint32_t timeout, const char* jsonPath) +bool HttpPowerMeterClass::httpRequest(int phase, WiFiClient &wifiClient, const String& host, uint16_t port, const String& uri, bool https, PowerMeterHttpConfig const& config) { if(!httpClient.begin(wifiClient, host, port, uri, https)){ snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("httpClient.begin() failed for %s://%s"), (https ? "https" : "http"), host.c_str()); return false; } - prepareRequest(timeout, httpHeader, httpValue); - if (authType == Auth::digest) { + prepareRequest(config.Timeout, config.HeaderKey, config.HeaderValue); + if (config.AuthType == Auth_t::Digest) { const char *headers[1] = {"WWW-Authenticate"}; httpClient.collectHeaders(headers, 1); - } else if (authType == Auth::basic) { - String authString = username; + } else if (config.AuthType == Auth_t::Basic) { + String authString = config.Username; authString += ":"; - authString += password; + authString += config.Password; String auth = "Basic "; auth.concat(base64::encode(authString)); httpClient.addHeader("Authorization", auth); } int httpCode = httpClient.GET(); - if (httpCode == HTTP_CODE_UNAUTHORIZED && authType == Auth::digest) { + if (httpCode == HTTP_CODE_UNAUTHORIZED && config.AuthType == Auth_t::Digest) { // Handle authentication challenge if (httpClient.hasHeader("WWW-Authenticate")) { String authReq = httpClient.header("WWW-Authenticate"); - String authorization = getDigestAuth(authReq, String(username), String(password), "GET", String(uri), 1); + String authorization = getDigestAuth(authReq, String(config.Username), String(config.Password), "GET", String(uri), 1); httpClient.end(); if(!httpClient.begin(wifiClient, host, port, uri, https)){ snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("httpClient.begin() failed for %s://%s using digest auth"), (https ? "https" : "http"), host.c_str()); return false; } - prepareRequest(timeout, httpHeader, httpValue); + prepareRequest(config.Timeout, config.HeaderKey, config.HeaderValue); httpClient.addHeader("Authorization", authorization); httpCode = httpClient.GET(); } @@ -160,7 +159,9 @@ bool HttpPowerMeterClass::httpRequest(int phase, WiFiClient &wifiClient, const S httpResponse = httpClient.getString(); // very unfortunate that we cannot parse WifiClient stream directly httpClient.end(); - return tryGetFloatValueForPhase(phase, jsonPath); + // TODO(schlimmchen): postpone calling tryGetFloatValueForPhase, as it + // will be called twice for each phase when doing separate requests. + return tryGetFloatValueForPhase(phase, config.JsonPath, config.PowerUnit, config.SignInverted); } String HttpPowerMeterClass::extractParam(String& authReq, const String& param, const char delimit) { @@ -218,7 +219,7 @@ String HttpPowerMeterClass::getDigestAuth(String& authReq, const String& usernam return authorization; } -bool HttpPowerMeterClass::tryGetFloatValueForPhase(int phase, const char* jsonPath) +bool HttpPowerMeterClass::tryGetFloatValueForPhase(int phase, const char* jsonPath, Unit_t unit, bool signInverted) { FirebaseJson json; json.setJsonData(httpResponse); @@ -228,7 +229,22 @@ bool HttpPowerMeterClass::tryGetFloatValueForPhase(int phase, const char* jsonPa return false; } + // this value is supposed to be in Watts and positive if energy is consumed. power[phase] = value.to(); + + switch (unit) { + case Unit_t::MilliWatts: + power[phase] /= 1000; + break; + case Unit_t::KiloWatts: + power[phase] *= 1000; + break; + default: + break; + } + + if (signInverted) { power[phase] *= -1; } + return true; } diff --git a/src/Huawei_can.cpp b/src/Huawei_can.cpp index df1d5eb24..a288981a7 100644 --- a/src/Huawei_can.cpp +++ b/src/Huawei_can.cpp @@ -2,17 +2,20 @@ /* * Copyright (C) 2023 Malte Schmidt and others */ +#include "Battery.h" #include "Huawei_can.h" #include "MessageOutput.h" #include "PowerMeter.h" #include "PowerLimiter.h" #include "Configuration.h" +#include "Battery.h" #include #include #include #include #include +#include #include HuaweiCanClass HuaweiCan; @@ -65,10 +68,10 @@ bool HuaweiCanCommClass::init(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t // Public methods need to obtain semaphore -void HuaweiCanCommClass::loop() -{ +void HuaweiCanCommClass::loop() +{ std::lock_guard lock(_mutex); - + INT32U rxId; unsigned char len = 0; unsigned char rxBuf[8]; @@ -119,7 +122,7 @@ void HuaweiCanCommClass::loop() if ( _hasNewTxValue[i] == true) { uint8_t data[8] = {0x01, i, 0x00, 0x00, 0x00, 0x00, (uint8_t)((_txValues[i] & 0xFF00) >> 8), (uint8_t)(_txValues[i] & 0xFF)}; - // Send extended message + // Send extended message byte sndStat = _CAN->sendMsgBuf(0x108180FE, 1, 8, data); if (sndStat == CAN_OK) { _hasNewTxValue[i] = false; @@ -134,10 +137,10 @@ void HuaweiCanCommClass::loop() _nextRequestMillis = millis() + HUAWEI_DATA_REQUEST_INTERVAL_MS; } -} +} -uint32_t HuaweiCanCommClass::getParameterValue(uint8_t parameter) -{ +uint32_t HuaweiCanCommClass::getParameterValue(uint8_t parameter) +{ std::lock_guard lock(_mutex); uint32_t v = 0; if (parameter < HUAWEI_OUTPUT_CURRENT1_IDX) { @@ -146,8 +149,8 @@ uint32_t HuaweiCanCommClass::getParameterValue(uint8_t parameter) return v; } -bool HuaweiCanCommClass::gotNewRxDataFrame(bool clear) -{ +bool HuaweiCanCommClass::gotNewRxDataFrame(bool clear) +{ std::lock_guard lock(_mutex); bool b = false; b = _completeUpdateReceived; @@ -157,8 +160,8 @@ bool HuaweiCanCommClass::gotNewRxDataFrame(bool clear) return b; } -uint8_t HuaweiCanCommClass::getErrorCode(bool clear) -{ +uint8_t HuaweiCanCommClass::getErrorCode(bool clear) +{ std::lock_guard lock(_mutex); uint8_t e = 0; e = _errorCode; @@ -168,7 +171,7 @@ uint8_t HuaweiCanCommClass::getErrorCode(bool clear) return e; } -void HuaweiCanCommClass::setParameterValue(uint16_t in, uint8_t parameterType) +void HuaweiCanCommClass::setParameterValue(uint16_t in, uint8_t parameterType) { std::lock_guard lock(_mutex); if (parameterType < HUAWEI_OFFLINE_CURRENT) { @@ -182,7 +185,7 @@ void HuaweiCanCommClass::setParameterValue(uint16_t in, uint8_t parameterType) void HuaweiCanCommClass::sendRequest() { uint8_t data[8] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - //Send extended message + //Send extended message byte sndStat = _CAN->sendMsgBuf(0x108040FE, 1, 8, data); if(sndStat != CAN_OK) { _errorCode |= HUAWEI_ERROR_CODE_RX; @@ -239,10 +242,6 @@ RectifierParameters_t * HuaweiCanClass::get() return &_rp; } -uint32_t HuaweiCanClass::getLastUpdate() -{ - return _lastUpdateReceivedMillis; -} void HuaweiCanClass::processReceivedParameters() { @@ -272,6 +271,8 @@ void HuaweiCanClass::loop() return; } + bool verboseLogging = config.Huawei.VerboseLogging; + processReceivedParameters(); uint8_t com_error = HuaweiCanComm.getErrorCode(true); @@ -279,11 +280,11 @@ void HuaweiCanClass::loop() MessageOutput.println("[HuaweiCanClass::loop] Data request error"); } if (com_error & HUAWEI_ERROR_CODE_TX) { - MessageOutput.println("[HuaweiCanClass::loop] Data set error"); + MessageOutput.println("[HuaweiCanClass::loop] Data set error"); } // Print updated data - if (HuaweiCanComm.gotNewRxDataFrame(false)) { + if (HuaweiCanComm.gotNewRxDataFrame(false) && verboseLogging) { MessageOutput.printf("[HuaweiCanClass::loop] In: %.02fV, %.02fA, %.02fW\n", _rp.input_voltage, _rp.input_current, _rp.input_power); MessageOutput.printf("[HuaweiCanClass::loop] Out: %.02fV, %.02fA of %.02fA, %.02fW\n", _rp.output_voltage, _rp.output_current, _rp.max_output_current, _rp.output_power); MessageOutput.printf("[HuaweiCanClass::loop] Eff : %.01f%%, Temp in: %.01fC, Temp out: %.01fC\n", _rp.efficiency * 100, _rp.input_temp, _rp.output_temp); @@ -293,25 +294,52 @@ void HuaweiCanClass::loop() if (_rp.output_current > HUAWEI_AUTO_MODE_SHUTDOWN_CURRENT) { _outputCurrentOnSinceMillis = millis(); } - if (_outputCurrentOnSinceMillis + HUAWEI_AUTO_MODE_SHUTDOWN_DELAY < millis() && + if (_outputCurrentOnSinceMillis + HUAWEI_AUTO_MODE_SHUTDOWN_DELAY < millis() && (_mode == HUAWEI_MODE_AUTO_EXT || _mode == HUAWEI_MODE_AUTO_INT)) { digitalWrite(_huaweiPower, 1); } - // *********************** - // Automatic power control - // *********************** - if (_mode == HUAWEI_MODE_AUTO_INT ) { + if (_mode == HUAWEI_MODE_AUTO_INT || _batteryEmergencyCharging) { - // Set voltage limit in periodic intervals + // Set voltage limit in periodic intervals if we're in auto mode or if emergency battery charge is requested. if ( _nextAutoModePeriodicIntMillis < millis()) { MessageOutput.printf("[HuaweiCanClass::loop] Periodically setting voltage limit: %f \r\n", config.Huawei.Auto_Power_Voltage_Limit); _setValue(config.Huawei.Auto_Power_Voltage_Limit, HUAWEI_ONLINE_VOLTAGE); _nextAutoModePeriodicIntMillis = millis() + 60000; } + } + // *********************** + // Emergency charge + // *********************** + auto stats = Battery.getStats(); + if (config.Huawei.Emergency_Charge_Enabled && stats->getImmediateChargingRequest()) { + _batteryEmergencyCharging = true; + + // Set output current + float efficiency = (_rp.efficiency > 0.5 ? _rp.efficiency : 1.0); + float outputCurrent = efficiency * (config.Huawei.Auto_Power_Upper_Power_Limit / _rp.output_voltage); + MessageOutput.printf("[HuaweiCanClass::loop] Emergency Charge Output current %f \r\n", outputCurrent); + _setValue(outputCurrent, HUAWEI_ONLINE_CURRENT); + return; + } - // Check if we should run automatic power calculation at all. + if (_batteryEmergencyCharging && !stats->getImmediateChargingRequest()) { + // Battery request has changed. Set current to 0, wait for PSU to respond and then clear state + _setValue(0, HUAWEI_ONLINE_CURRENT); + if (_rp.output_current < 1) { + _batteryEmergencyCharging = false; + } + return; + } + + // *********************** + // Automatic power control + // *********************** + + if (_mode == HUAWEI_MODE_AUTO_INT ) { + + // Check if we should run automatic power calculation at all. // We may have set a value recently and still wait for output stabilization if (_autoModeBlockedTillMillis > millis()) { return; @@ -336,7 +364,7 @@ void HuaweiCanClass::loop() if (inverter != nullptr) { if(inverter->isProducing()) { _setValue(0.0, HUAWEI_ONLINE_CURRENT); - // Don't run auto mode for a second now. Otherwise we may send too much over the CAN bus + // Don't run auto mode for a second now. Otherwise we may send too much over the CAN bus _autoModeBlockedTillMillis = millis() + 1000; MessageOutput.printf("[HuaweiCanClass::loop] Inverter is active, disable\r\n"); return; @@ -352,8 +380,26 @@ void HuaweiCanClass::loop() // Calculate new power limit float newPowerLimit = -1 * round(PowerMeter.getPowerTotal()); - newPowerLimit += _rp.output_power; - MessageOutput.printf("[HuaweiCanClass::loop] PL: %f, OP: %f \r\n", newPowerLimit, _rp.output_power); + float efficiency = (_rp.efficiency > 0.5 ? _rp.efficiency : 1.0); + + // Powerlimit is the requested output power + permissable Grid consumption factoring in the efficiency factor + newPowerLimit += _rp.output_power + config.Huawei.Auto_Power_Target_Power_Consumption / efficiency; + + if (verboseLogging){ + MessageOutput.printf("[HuaweiCanClass::loop] newPowerLimit: %f, output_power: %f \r\n", newPowerLimit, _rp.output_power); + } + + if (config.Battery.Enabled && config.Huawei.Auto_Power_BatterySoC_Limits_Enabled) { + uint8_t _batterySoC = Battery.getStats()->getSoC(); + if (_batterySoC >= config.Huawei.Auto_Power_Stop_BatterySoC_Threshold) { + newPowerLimit = 0; + if (verboseLogging) { + MessageOutput.printf("[HuaweiCanClass::loop] Current battery SoC %i reached " + "stop threshold %i, set newPowerLimit to %f \r\n", _batterySoC, + config.Huawei.Auto_Power_Stop_BatterySoC_Threshold, newPowerLimit); + } + } + } if (newPowerLimit > config.Huawei.Auto_Power_Lower_Power_Limit) { @@ -377,10 +423,17 @@ void HuaweiCanClass::loop() newPowerLimit = config.Huawei.Auto_Power_Upper_Power_Limit; } - // Set the actual output limit - float efficiency = (_rp.efficiency > 0.5 ? _rp.efficiency : 1.0); - float outputCurrent = efficiency * (newPowerLimit / _rp.output_voltage); - MessageOutput.printf("[HuaweiCanClass::loop] Output current %f \r\n", outputCurrent); + // Calculate output current + float calculatedCurrent = efficiency * (newPowerLimit / _rp.output_voltage); + + // Limit output current to value requested by BMS + float permissableCurrent = stats->getChargeCurrentLimitation() - (stats->getChargeCurrent() - _rp.output_current); // BMS current limit - current from other sources + float outputCurrent = std::min(calculatedCurrent, permissableCurrent); + outputCurrent= outputCurrent > 0 ? outputCurrent : 0; + + if (verboseLogging) { + MessageOutput.printf("[HuaweiCanClass::loop] Setting output current to %.2fA. This is the lower value of calculated %.2fA and BMS permissable %.2fA currents\r\n", outputCurrent, calculatedCurrent, permissableCurrent); + } _autoPowerEnabled = true; _setValue(outputCurrent, HUAWEI_ONLINE_CURRENT); @@ -392,7 +445,7 @@ void HuaweiCanClass::loop() _setValue(0.0, HUAWEI_ONLINE_CURRENT); } } - } + } } void HuaweiCanClass::setValue(float in, uint8_t parameterType) @@ -415,10 +468,11 @@ void HuaweiCanClass::_setValue(float in, uint8_t parameterType) if (in < 0) { MessageOutput.printf("[HuaweiCanClass::_setValue] Error: Tried to set voltage/current to negative value %f \r\n", in); + return; } // Start PSU if needed - if (in > HUAWEI_AUTO_MODE_SHUTDOWN_CURRENT && parameterType == HUAWEI_ONLINE_CURRENT && + if (in > HUAWEI_AUTO_MODE_SHUTDOWN_CURRENT && parameterType == HUAWEI_ONLINE_CURRENT && (_mode == HUAWEI_MODE_AUTO_EXT || _mode == HUAWEI_MODE_AUTO_INT)) { digitalWrite(_huaweiPower, 0); _outputCurrentOnSinceMillis = millis(); @@ -466,7 +520,5 @@ void HuaweiCanClass::setMode(uint8_t mode) { } } -bool HuaweiCanClass::getAutoPowerStatus() { - return _autoPowerEnabled; -} + diff --git a/src/InverterSettings.cpp b/src/InverterSettings.cpp index 04ec1e009..19605af14 100644 --- a/src/InverterSettings.cpp +++ b/src/InverterSettings.cpp @@ -51,9 +51,9 @@ void InverterSettingsClass::init(Scheduler& scheduler) if (PinMapping.isValidCmt2300Config()) { Hoymiles.initCMT(pin.cmt_sdio, pin.cmt_clk, pin.cmt_cs, pin.cmt_fcs, pin.cmt_gpio2, pin.cmt_gpio3); - MessageOutput.println(F(" Setting country mode... ")); + MessageOutput.println(" Setting country mode... "); Hoymiles.getRadioCmt()->setCountryMode(static_cast(config.Dtu.Cmt.CountryMode)); - MessageOutput.println(F(" Setting CMT target frequency... ")); + MessageOutput.println(" Setting CMT target frequency... "); Hoymiles.getRadioCmt()->setInverterTargetFrequency(config.Dtu.Cmt.Frequency); } diff --git a/src/JkBmsController.cpp b/src/JkBmsController.cpp index 3f924030f..94f80cb08 100644 --- a/src/JkBmsController.cpp +++ b/src/JkBmsController.cpp @@ -198,7 +198,7 @@ class DummySerial { }; DummySerial HwSerial; #else -HardwareSerial HwSerial(2); +HardwareSerial HwSerial((ARDUINO_USB_CDC_ON_BOOT != 1)?2:0); #endif namespace JkBms { @@ -220,6 +220,7 @@ bool Controller::init(bool verboseLogging) return false; } + HwSerial.end(); // make sure the UART will be re-initialized HwSerial.begin(115200, SERIAL_8N1, pin.battery_rx, pin.battery_tx); HwSerial.flush(); diff --git a/src/MqttHandlVedirectHass.cpp b/src/MqttHandlVedirectHass.cpp index b839af3c2..a3ddb8884 100644 --- a/src/MqttHandlVedirectHass.cpp +++ b/src/MqttHandlVedirectHass.cpp @@ -58,42 +58,44 @@ void MqttHandleVedirectHassClass::publishConfig() // device info for (int idx = 0; idx < VictronMppt.controllerAmount(); ++idx) { - // ensure data is received from victron - if (!VictronMppt.isDataValid(idx)) { - continue; - } - - std::optional spOptMpptData = VictronMppt.getData(idx); - if (!spOptMpptData.has_value()) { - continue; - } - - VeDirectMpptController::spData_t &spMpptData = spOptMpptData.value(); - - publishBinarySensor("MPPT load output state", "mdi:export", "LOAD", "ON", "OFF", spMpptData); - publishSensor("MPPT serial number", "mdi:counter", "SER", nullptr, nullptr, nullptr, spMpptData); - publishSensor("MPPT firmware number", "mdi:counter", "FW", nullptr, nullptr, nullptr, spMpptData); - publishSensor("MPPT state of operation", "mdi:wrench", "CS", nullptr, nullptr, nullptr, spMpptData); - publishSensor("MPPT error code", "mdi:bell", "ERR", nullptr, nullptr, nullptr, spMpptData); - publishSensor("MPPT off reason", "mdi:wrench", "OR", nullptr, nullptr, nullptr, spMpptData); - publishSensor("MPPT tracker operation mode", "mdi:wrench", "MPPT", nullptr, nullptr, nullptr, spMpptData); - publishSensor("MPPT Day sequence number (0...364)", "mdi:calendar-month-outline", "HSDS", NULL, "total", "d", spMpptData); + auto optMpptData = VictronMppt.getData(idx); + if (!optMpptData.has_value()) { continue; } + + publishBinarySensor("MPPT load output state", "mdi:export", "LOAD", "ON", "OFF", *optMpptData); + publishSensor("MPPT serial number", "mdi:counter", "SER", nullptr, nullptr, nullptr, *optMpptData); + publishSensor("MPPT firmware number", "mdi:counter", "FW", nullptr, nullptr, nullptr, *optMpptData); + publishSensor("MPPT state of operation", "mdi:wrench", "CS", nullptr, nullptr, nullptr, *optMpptData); + publishSensor("MPPT error code", "mdi:bell", "ERR", nullptr, nullptr, nullptr, *optMpptData); + publishSensor("MPPT off reason", "mdi:wrench", "OR", nullptr, nullptr, nullptr, *optMpptData); + publishSensor("MPPT tracker operation mode", "mdi:wrench", "MPPT", nullptr, nullptr, nullptr, *optMpptData); + publishSensor("MPPT Day sequence number (0...364)", "mdi:calendar-month-outline", "HSDS", NULL, "total", "d", *optMpptData); // battery info - publishSensor("Battery voltage", NULL, "V", "voltage", "measurement", "V", spMpptData); - publishSensor("Battery current", NULL, "I", "current", "measurement", "A", spMpptData); - publishSensor("Battery power (calculated)", NULL, "P", "power", "measurement", "W", spMpptData); - publishSensor("Battery efficiency (calculated)", NULL, "E", NULL, "measurement", "%", spMpptData); + publishSensor("Battery voltage", NULL, "V", "voltage", "measurement", "V", *optMpptData); + publishSensor("Battery current", NULL, "I", "current", "measurement", "A", *optMpptData); + publishSensor("Battery power (calculated)", NULL, "P", "power", "measurement", "W", *optMpptData); + publishSensor("Battery efficiency (calculated)", NULL, "E", NULL, "measurement", "%", *optMpptData); // panel info - publishSensor("Panel voltage", NULL, "VPV", "voltage", "measurement", "V", spMpptData); - publishSensor("Panel current (calculated)", NULL, "IPV", "current", "measurement", "A", spMpptData); - publishSensor("Panel power", NULL, "PPV", "power", "measurement", "W", spMpptData); - publishSensor("Panel yield total", NULL, "H19", "energy", "total_increasing", "kWh", spMpptData); - publishSensor("Panel yield today", NULL, "H20", "energy", "total", "kWh", spMpptData); - publishSensor("Panel maximum power today", NULL, "H21", "power", "measurement", "W", spMpptData); - publishSensor("Panel yield yesterday", NULL, "H22", "energy", "total", "kWh", spMpptData); - publishSensor("Panel maximum power yesterday", NULL, "H23", "power", "measurement", "W", spMpptData); + publishSensor("Panel voltage", NULL, "VPV", "voltage", "measurement", "V", *optMpptData); + publishSensor("Panel current (calculated)", NULL, "IPV", "current", "measurement", "A", *optMpptData); + publishSensor("Panel power", NULL, "PPV", "power", "measurement", "W", *optMpptData); + publishSensor("Panel yield total", NULL, "H19", "energy", "total_increasing", "kWh", *optMpptData); + publishSensor("Panel yield today", NULL, "H20", "energy", "total", "kWh", *optMpptData); + publishSensor("Panel maximum power today", NULL, "H21", "power", "measurement", "W", *optMpptData); + publishSensor("Panel yield yesterday", NULL, "H22", "energy", "total", "kWh", *optMpptData); + publishSensor("Panel maximum power yesterday", NULL, "H23", "power", "measurement", "W", *optMpptData); + + // optional info, provided only if TX is connected to charge controller + if (optMpptData->NetworkTotalDcInputPowerMilliWatts.first != 0) { + publishSensor("VE.Smart network total DC input power", "mdi:solar-power", "NetworkTotalDcInputPower", "power", "measurement", "W", *optMpptData); + } + if (optMpptData->MpptTemperatureMilliCelsius.first != 0) { + publishSensor("MPPT temperature", "mdi:temperature-celsius", "MpptTemperature", "temperature", "measurement", "W", *optMpptData); + } + if (optMpptData->SmartBatterySenseTemperatureMilliCelsius.first != 0) { + publishSensor("Smart Battery Sense temperature", "mdi:temperature-celsius", "SmartBatterySenseTemperature", "temperature", "measurement", "W", *optMpptData); + } } yield(); @@ -102,9 +104,9 @@ void MqttHandleVedirectHassClass::publishConfig() void MqttHandleVedirectHassClass::publishSensor(const char *caption, const char *icon, const char *subTopic, const char *deviceClass, const char *stateClass, const char *unitOfMeasurement, - const VeDirectMpptController::spData_t &spMpptData) + const VeDirectMpptController::data_t &mpptData) { - String serial = spMpptData->SER; + String serial = mpptData.serialNr_SER; String sensorId = caption; sensorId.replace(" ", "_"); @@ -122,10 +124,8 @@ void MqttHandleVedirectHassClass::publishSensor(const char *caption, const char statTopic.concat("/"); statTopic.concat(subTopic); - DynamicJsonDocument root(1024); - if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { - return; - } + JsonDocument root; + root["name"] = caption; root["stat_t"] = statTopic; root["uniq_id"] = serial + "_" + sensorId; @@ -138,8 +138,8 @@ void MqttHandleVedirectHassClass::publishSensor(const char *caption, const char root["unit_of_meas"] = unitOfMeasurement; } - JsonObject deviceObj = root.createNestedObject("dev"); - createDeviceInfo(deviceObj, spMpptData); + JsonObject deviceObj = root["dev"].to(); + createDeviceInfo(deviceObj, mpptData); if (Configuration.get().Mqtt.Hass.Expire) { root["exp_aft"] = Configuration.get().Mqtt.PublishInterval * 3; @@ -151,7 +151,9 @@ void MqttHandleVedirectHassClass::publishSensor(const char *caption, const char root["stat_cla"] = stateClass; } - if (Utils::checkJsonOverflow(root, __FUNCTION__, __LINE__)) { return; } + if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { + return; + } char buffer[512]; serializeJson(root, buffer); @@ -160,9 +162,9 @@ void MqttHandleVedirectHassClass::publishSensor(const char *caption, const char } void MqttHandleVedirectHassClass::publishBinarySensor(const char *caption, const char *icon, const char *subTopic, const char *payload_on, const char *payload_off, - const VeDirectMpptController::spData_t &spMpptData) + const VeDirectMpptController::data_t &mpptData) { - String serial = spMpptData->SER; + String serial = mpptData.serialNr_SER; String sensorId = caption; sensorId.replace(" ", "_"); @@ -180,10 +182,7 @@ void MqttHandleVedirectHassClass::publishBinarySensor(const char *caption, const statTopic.concat("/"); statTopic.concat(subTopic); - DynamicJsonDocument root(1024); - if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { - return; - } + JsonDocument root; root["name"] = caption; root["uniq_id"] = serial + "_" + sensorId; root["stat_t"] = statTopic; @@ -194,10 +193,12 @@ void MqttHandleVedirectHassClass::publishBinarySensor(const char *caption, const root["icon"] = icon; } - JsonObject deviceObj = root.createNestedObject("dev"); - createDeviceInfo(deviceObj, spMpptData); + JsonObject deviceObj = root["dev"].to(); + createDeviceInfo(deviceObj, mpptData); - if (Utils::checkJsonOverflow(root, __FUNCTION__, __LINE__)) { return; } + if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { + return; + } char buffer[512]; serializeJson(root, buffer); @@ -205,14 +206,14 @@ void MqttHandleVedirectHassClass::publishBinarySensor(const char *caption, const } void MqttHandleVedirectHassClass::createDeviceInfo(JsonObject &object, - const VeDirectMpptController::spData_t &spMpptData) + const VeDirectMpptController::data_t &mpptData) { - String serial = spMpptData->SER; + String serial = mpptData.serialNr_SER; object["name"] = "Victron(" + serial + ")"; object["ids"] = serial; object["cu"] = String("http://") + NetworkSettings.localIP().toString(); object["mf"] = "OpenDTU"; - object["mdl"] = spMpptData->getPidAsString(); + object["mdl"] = mpptData.getPidAsString(); object["sw"] = AUTO_GIT_HASH; } diff --git a/src/MqttHandleBatteryHass.cpp b/src/MqttHandleBatteryHass.cpp index 75912817f..28a713334 100644 --- a/src/MqttHandleBatteryHass.cpp +++ b/src/MqttHandleBatteryHass.cpp @@ -144,10 +144,7 @@ void MqttHandleBatteryHassClass::publishSensor(const char* caption, const char* // statTopic.concat("/"); statTopic.concat(subTopic); - DynamicJsonDocument root(1024); - if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { - return; - } + JsonDocument root; root["name"] = caption; root["stat_t"] = statTopic; root["uniq_id"] = serial + "_" + sensorId; @@ -160,7 +157,7 @@ void MqttHandleBatteryHassClass::publishSensor(const char* caption, const char* root["unit_of_meas"] = unitOfMeasurement; } - JsonObject deviceObj = root.createNestedObject("dev"); + JsonObject deviceObj = root["dev"].to(); createDeviceInfo(deviceObj); if (Configuration.get().Mqtt.Hass.Expire) { @@ -173,7 +170,9 @@ void MqttHandleBatteryHassClass::publishSensor(const char* caption, const char* root["stat_cla"] = stateClass; } - if (Utils::checkJsonOverflow(root, __FUNCTION__, __LINE__)) { return; } + if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { + return; + } char buffer[512]; serializeJson(root, buffer); @@ -201,10 +200,8 @@ void MqttHandleBatteryHassClass::publishBinarySensor(const char* caption, const // statTopic.concat("/"); statTopic.concat(subTopic); - DynamicJsonDocument root(1024); - if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { - return; - } + JsonDocument root; + root["name"] = caption; root["uniq_id"] = serial + "_" + sensorId; root["stat_t"] = statTopic; @@ -215,10 +212,12 @@ void MqttHandleBatteryHassClass::publishBinarySensor(const char* caption, const root["icon"] = icon; } - JsonObject deviceObj = root.createNestedObject("dev"); + auto deviceObj = root["dev"].to(); createDeviceInfo(deviceObj); - if (Utils::checkJsonOverflow(root, __FUNCTION__, __LINE__)) { return; } + if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { + return; + } char buffer[512]; serializeJson(root, buffer); diff --git a/src/MqttHandleHass.cpp b/src/MqttHandleHass.cpp index 21ff0fa2b..0de7bdedb 100644 --- a/src/MqttHandleHass.cpp +++ b/src/MqttHandleHass.cpp @@ -137,10 +137,7 @@ void MqttHandleHassClass::publishInverterField(std::shared_ptr name = "CH" + chanNum + " " + fieldName; } - DynamicJsonDocument root(1024); - if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { - return; - } + JsonDocument root; root["name"] = name; root["stat_t"] = stateTopic; @@ -163,6 +160,10 @@ void MqttHandleHassClass::publishInverterField(std::shared_ptr root["stat_cla"] = stateCls; } + if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { + return; + } + String buffer; serializeJson(root, buffer); publish(configTopic, buffer); @@ -185,10 +186,7 @@ void MqttHandleHassClass::publishInverterButton(std::shared_ptr inv) +void MqttHandleHassClass::createInverterInfo(JsonDocument& root, std::shared_ptr inv) { createDeviceInfo( root, @@ -378,7 +382,7 @@ void MqttHandleHassClass::createInverterInfo(DynamicJsonDocument& root, std::sha getDtuUniqueId()); } -void MqttHandleHassClass::createDtuInfo(DynamicJsonDocument& root) +void MqttHandleHassClass::createDtuInfo(JsonDocument& root) { createDeviceInfo( root, @@ -391,12 +395,12 @@ void MqttHandleHassClass::createDtuInfo(DynamicJsonDocument& root) } void MqttHandleHassClass::createDeviceInfo( - DynamicJsonDocument& root, + JsonDocument& root, const String& name, const String& identifiers, const String& configuration_url, const String& manufacturer, const String& model, const String& sw_version, const String& via_device) { - auto object = root.createNestedObject("dev"); + auto object = root["dev"].to(); object["name"] = name; object["ids"] = identifiers; diff --git a/src/MqttHandleHuawei.cpp b/src/MqttHandleHuawei.cpp index 4330dc7c2..1f0f7ddb7 100644 --- a/src/MqttHandleHuawei.cpp +++ b/src/MqttHandleHuawei.cpp @@ -75,6 +75,7 @@ void MqttHandleHuaweiClass::loop() MqttSettings.publish("huawei/input_temp", String(rp->input_temp)); MqttSettings.publish("huawei/output_temp", String(rp->output_temp)); MqttSettings.publish("huawei/efficiency", String(rp->efficiency)); + MqttSettings.publish("huawei/mode", String(HuaweiCan.getMode())); yield(); @@ -158,4 +159,4 @@ void MqttHandleHuaweiClass::onMqttMessage(Topic t, } break; } -} \ No newline at end of file +} diff --git a/src/MqttHandlePowerLimiter.cpp b/src/MqttHandlePowerLimiter.cpp index 411fa3f1a..95f90db2f 100644 --- a/src/MqttHandlePowerLimiter.cpp +++ b/src/MqttHandlePowerLimiter.cpp @@ -76,6 +76,8 @@ void MqttHandlePowerLimiterClass::loop() auto val = static_cast(PowerLimiter.getMode()); MqttSettings.publish("powerlimiter/status/mode", String(val)); + MqttSettings.publish("powerlimiter/status/inverter_update_timeouts", String(PowerLimiter.getInverterUpdateTimeouts())); + // no thresholds are relevant for setups without a battery if (config.PowerLimiter.IsInverterSolarPowered) { return; } diff --git a/src/MqttHandlePowerLimiterHass.cpp b/src/MqttHandlePowerLimiterHass.cpp index 9576a84ad..6699fe9ea 100644 --- a/src/MqttHandlePowerLimiterHass.cpp +++ b/src/MqttHandlePowerLimiterHass.cpp @@ -112,10 +112,7 @@ void MqttHandlePowerLimiterHassClass::publishSelect( const String cmdTopic = MqttSettings.getPrefix() + "powerlimiter/cmd/" + commandTopic; const String statTopic = MqttSettings.getPrefix() + "powerlimiter/status/" + stateTopic; - DynamicJsonDocument root(1024); - if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { - return; - } + JsonDocument root; root["name"] = caption; root["uniq_id"] = selectId; @@ -125,15 +122,17 @@ void MqttHandlePowerLimiterHassClass::publishSelect( root["ent_cat"] = category; root["cmd_t"] = cmdTopic; root["stat_t"] = statTopic; - JsonArray options = root.createNestedArray("options"); + JsonArray options = root["options"].to(); options.add("0"); options.add("1"); options.add("2"); - JsonObject deviceObj = root.createNestedObject("dev"); + JsonObject deviceObj = root["dev"].to(); createDeviceInfo(deviceObj); - if (Utils::checkJsonOverflow(root, __FUNCTION__, __LINE__)) { return; } + if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { + return; + } String buffer; serializeJson(root, buffer); @@ -155,10 +154,7 @@ void MqttHandlePowerLimiterHassClass::publishNumber( const String cmdTopic = MqttSettings.getPrefix() + "powerlimiter/cmd/" + commandTopic; const String statTopic = MqttSettings.getPrefix() + "powerlimiter/status/" + stateTopic; - DynamicJsonDocument root(1024); - if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { - return; - } + JsonDocument root; root["name"] = caption; root["uniq_id"] = numberId; @@ -178,10 +174,12 @@ void MqttHandlePowerLimiterHassClass::publishNumber( root["exp_aft"] = config.Mqtt.PublishInterval * 3; } - JsonObject deviceObj = root.createNestedObject("dev"); + JsonObject deviceObj = root["dev"].to(); createDeviceInfo(deviceObj); - if (Utils::checkJsonOverflow(root, __FUNCTION__, __LINE__)) { return; } + if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { + return; + } String buffer; serializeJson(root, buffer); diff --git a/src/MqttHandleVedirect.cpp b/src/MqttHandleVedirect.cpp index 8cfd6efce..0ed47e87a 100644 --- a/src/MqttHandleVedirect.cpp +++ b/src/MqttHandleVedirect.cpp @@ -59,21 +59,13 @@ void MqttHandleVedirectClass::loop() #endif for (int idx = 0; idx < VictronMppt.controllerAmount(); ++idx) { - if (!VictronMppt.isDataValid(idx)) { - continue; - } - - std::optional spOptMpptData = VictronMppt.getData(idx); - if (!spOptMpptData.has_value()) { - continue; - } + std::optional optMpptData = VictronMppt.getData(idx); + if (!optMpptData.has_value()) { continue; } - VeDirectMpptController::spData_t &spMpptData = spOptMpptData.value(); - - VeDirectMpptController::veMpptStruct _kvFrame = _kvFrames[spMpptData->SER]; - publish_mppt_data(spMpptData, _kvFrame); + auto const& kvFrame = _kvFrames[optMpptData->serialNr_SER]; + publish_mppt_data(*optMpptData, kvFrame); if (!_PublishFull) { - _kvFrames[spMpptData->SER] = *spMpptData; + _kvFrames[optMpptData->serialNr_SER] = *optMpptData; } } @@ -104,79 +96,48 @@ void MqttHandleVedirectClass::loop() } } -void MqttHandleVedirectClass::publish_mppt_data(const VeDirectMpptController::spData_t &spMpptData, - VeDirectMpptController::veMpptStruct &frame) const { +void MqttHandleVedirectClass::publish_mppt_data(const VeDirectMpptController::data_t ¤tData, + const VeDirectMpptController::data_t &previousData) const { String value; String topic = "victron/"; - topic.concat(spMpptData->SER); + topic.concat(currentData.serialNr_SER); topic.concat("/"); - if (_PublishFull || spMpptData->PID != frame.PID) - MqttSettings.publish(topic + "PID", spMpptData->getPidAsString().data()); - if (_PublishFull || strcmp(spMpptData->SER, frame.SER) != 0) - MqttSettings.publish(topic + "SER", spMpptData->SER ); - if (_PublishFull || strcmp(spMpptData->FW, frame.FW) != 0) - MqttSettings.publish(topic + "FW", spMpptData->FW); - if (_PublishFull || spMpptData->LOAD != frame.LOAD) - MqttSettings.publish(topic + "LOAD", spMpptData->LOAD ? "ON" : "OFF"); - if (_PublishFull || spMpptData->CS != frame.CS) - MqttSettings.publish(topic + "CS", spMpptData->getCsAsString().data()); - if (_PublishFull || spMpptData->ERR != frame.ERR) - MqttSettings.publish(topic + "ERR", spMpptData->getErrAsString().data()); - if (_PublishFull || spMpptData->OR != frame.OR) - MqttSettings.publish(topic + "OR", spMpptData->getOrAsString().data()); - if (_PublishFull || spMpptData->MPPT != frame.MPPT) - MqttSettings.publish(topic + "MPPT", spMpptData->getMpptAsString().data()); - if (_PublishFull || spMpptData->HSDS != frame.HSDS) { - value = spMpptData->HSDS; - MqttSettings.publish(topic + "HSDS", value); - } - if (_PublishFull || spMpptData->V != frame.V) { - value = spMpptData->V; - MqttSettings.publish(topic + "V", value); - } - if (_PublishFull || spMpptData->I != frame.I) { - value = spMpptData->I; - MqttSettings.publish(topic + "I", value); - } - if (_PublishFull || spMpptData->P != frame.P) { - value = spMpptData->P; - MqttSettings.publish(topic + "P", value); - } - if (_PublishFull || spMpptData->VPV != frame.VPV) { - value = spMpptData->VPV; - MqttSettings.publish(topic + "VPV", value); - } - if (_PublishFull || spMpptData->IPV != frame.IPV) { - value = spMpptData->IPV; - MqttSettings.publish(topic + "IPV", value); +#define PUBLISH(sm, t, val) \ + if (_PublishFull || currentData.sm != previousData.sm) { \ + MqttSettings.publish(topic + t, String(val)); \ } - if (_PublishFull || spMpptData->PPV != frame.PPV) { - value = spMpptData->PPV; - MqttSettings.publish(topic + "PPV", value); - } - if (_PublishFull || spMpptData->E != frame.E) { - value = spMpptData->E; - MqttSettings.publish(topic + "E", value); - } - if (_PublishFull || spMpptData->H19 != frame.H19) { - value = spMpptData->H19; - MqttSettings.publish(topic + "H19", value); - } - if (_PublishFull || spMpptData->H20 != frame.H20) { - value = spMpptData->H20; - MqttSettings.publish(topic + "H20", value); - } - if (_PublishFull || spMpptData->H21 != frame.H21) { - value = spMpptData->H21; - MqttSettings.publish(topic + "H21", value); - } - if (_PublishFull || spMpptData->H22 != frame.H22) { - value = spMpptData->H22; - MqttSettings.publish(topic + "H22", value); - } - if (_PublishFull || spMpptData->H23 != frame.H23) { - value = spMpptData->H23; - MqttSettings.publish(topic + "H23", value); + + PUBLISH(productID_PID, "PID", currentData.getPidAsString().data()); + PUBLISH(serialNr_SER, "SER", currentData.serialNr_SER); + PUBLISH(firmwareNr_FW, "FW", currentData.firmwareNr_FW); + PUBLISH(loadOutputState_LOAD, "LOAD", (currentData.loadOutputState_LOAD ? "ON" : "OFF")); + PUBLISH(currentState_CS, "CS", currentData.getCsAsString().data()); + PUBLISH(errorCode_ERR, "ERR", currentData.getErrAsString().data()); + PUBLISH(offReason_OR, "OR", currentData.getOrAsString().data()); + PUBLISH(stateOfTracker_MPPT, "MPPT", currentData.getMpptAsString().data()); + PUBLISH(daySequenceNr_HSDS, "HSDS", currentData.daySequenceNr_HSDS); + PUBLISH(batteryVoltage_V_mV, "V", currentData.batteryVoltage_V_mV / 1000.0); + PUBLISH(batteryCurrent_I_mA, "I", currentData.batteryCurrent_I_mA / 1000.0); + PUBLISH(batteryOutputPower_W, "P", currentData.batteryOutputPower_W); + PUBLISH(panelVoltage_VPV_mV, "VPV", currentData.panelVoltage_VPV_mV / 1000.0); + PUBLISH(panelCurrent_mA, "IPV", currentData.panelCurrent_mA / 1000.0); + PUBLISH(panelPower_PPV_W, "PPV", currentData.panelPower_PPV_W); + PUBLISH(mpptEfficiency_Percent, "E", currentData.mpptEfficiency_Percent); + PUBLISH(yieldTotal_H19_Wh, "H19", currentData.yieldTotal_H19_Wh / 1000.0); + PUBLISH(yieldToday_H20_Wh, "H20", currentData.yieldToday_H20_Wh / 1000.0); + PUBLISH(maxPowerToday_H21_W, "H21", currentData.maxPowerToday_H21_W); + PUBLISH(yieldYesterday_H22_Wh, "H22", currentData.yieldYesterday_H22_Wh / 1000.0); + PUBLISH(maxPowerYesterday_H23_W, "H23", currentData.maxPowerYesterday_H23_W); +#undef PUBLILSH + +#define PUBLISH_OPT(sm, t, val) \ + if (currentData.sm.first != 0 && (_PublishFull || currentData.sm.second != previousData.sm.second)) { \ + MqttSettings.publish(topic + t, String(val)); \ } + + PUBLISH_OPT(NetworkTotalDcInputPowerMilliWatts, "NetworkTotalDcInputPower", currentData.NetworkTotalDcInputPowerMilliWatts.second / 1000.0); + PUBLISH_OPT(MpptTemperatureMilliCelsius, "MpptTemperature", currentData.MpptTemperatureMilliCelsius.second / 1000.0); + PUBLISH_OPT(SmartBatterySenseTemperatureMilliCelsius, "SmartBatterySenseTemperature", currentData.SmartBatterySenseTemperatureMilliCelsius.second / 1000.0); +#undef PUBLILSH_OPT } diff --git a/src/PinMapping.cpp b/src/PinMapping.cpp index 7c8bec186..a3c888861 100644 --- a/src/PinMapping.cpp +++ b/src/PinMapping.cpp @@ -8,8 +8,6 @@ #include #include -#define JSON_BUFFER_SIZE 6144 - #ifndef DISPLAY_TYPE #define DISPLAY_TYPE 0U #endif @@ -94,6 +92,14 @@ #define VICTRON_PIN_RX -1 #endif +#ifndef VICTRON_PIN_TX2 +#define VICTRON_PIN_TX2 -1 +#endif + +#ifndef VICTRON_PIN_RX2 +#define VICTRON_PIN_RX2 -1 +#endif + #ifndef BATTERY_PIN_RX #define BATTERY_PIN_RX -1 #endif @@ -234,7 +240,7 @@ bool PinMappingClass::init(const String& deviceMapping) return false; } - DynamicJsonDocument doc(JSON_BUFFER_SIZE); + JsonDocument doc; // Deserialize the JSON document DeserializationError error = deserializeJson(doc, f); if (error) { @@ -284,8 +290,8 @@ bool PinMappingClass::init(const String& deviceMapping) // OpenDTU-OnBattery-specific pins below _pinMapping.victron_rx = doc[i]["victron"]["rx"] | VICTRON_PIN_RX; _pinMapping.victron_tx = doc[i]["victron"]["tx"] | VICTRON_PIN_TX; - _pinMapping.victron_rx2 = doc[i]["victron"]["rx2"] | VICTRON_PIN_RX; - _pinMapping.victron_tx2 = doc[i]["victron"]["tx2"] | VICTRON_PIN_TX; + _pinMapping.victron_rx2 = doc[i]["victron"]["rx2"] | VICTRON_PIN_RX2; + _pinMapping.victron_tx2 = doc[i]["victron"]["tx2"] | VICTRON_PIN_TX2; _pinMapping.battery_rx = doc[i]["battery"]["rx"] | BATTERY_PIN_RX; _pinMapping.battery_rxen = doc[i]["battery"]["rxen"] | BATTERY_PIN_RXEN; diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index b2ce2392a..7ce0d8bd9 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -3,6 +3,7 @@ * Copyright (C) 2022 Thomas Basler and others */ +#include "Utils.h" #include "Battery.h" #include "PowerMeter.h" #include "PowerLimiter.h" @@ -31,13 +32,11 @@ frozen::string const& PowerLimiterClass::getStatusText(PowerLimiterClass::Status { static const frozen::string missing = "programmer error: missing status text"; - static const frozen::map texts = { + static const frozen::map texts = { { Status::Initializing, "initializing (should not see me)" }, { Status::DisabledByConfig, "disabled by configuration" }, { Status::DisabledByMqtt, "disabled by MQTT" }, { Status::WaitingForValidTimestamp, "waiting for valid date and time to be available" }, - { Status::PowerMeterDisabled, "no power meter is configured/enabled" }, - { Status::PowerMeterTimeout, "power meter readings are outdated" }, { Status::PowerMeterPending, "waiting for sufficiently recent power meter reading" }, { Status::InverterInvalid, "invalid inverter selection/configuration" }, { Status::InverterChanged, "target inverter changed" }, @@ -47,7 +46,7 @@ frozen::string const& PowerLimiterClass::getStatusText(PowerLimiterClass::Status { Status::InverterPowerCmdPending, "waiting for a start/stop/restart command to complete" }, { Status::InverterDevInfoPending, "waiting for inverter device information to be available" }, { Status::InverterStatsPending, "waiting for sufficiently recent inverter data" }, - { Status::CalculatedLimitBelowMinLimit, "calculated limit is less than lower power limit" }, + { Status::CalculatedLimitBelowMinLimit, "calculated limit is less than minimum power limit" }, { Status::UnconditionalSolarPassthrough, "unconditionally passing through all solar power (MQTT override)" }, { Status::NoVeDirect, "VE.Direct disabled, connection broken, or data outdated" }, { Status::NoEnergy, "no energy source available to power the inverter from" }, @@ -82,8 +81,7 @@ void PowerLimiterClass::announceStatus(PowerLimiterClass::Status status) /** * returns true if the inverter state was changed or is about to change, i.e., * if it is actually in need of a shutdown. returns false otherwise, i.e., the - * inverter is already shut down and the inverter limit is set to the configured - * lower power limit. + * inverter is already shut down. */ bool PowerLimiterClass::shutdown(PowerLimiterClass::Status status) { @@ -93,14 +91,6 @@ bool PowerLimiterClass::shutdown(PowerLimiterClass::Status status) _oTargetPowerState = false; - auto const& config = Configuration.get(); - if ( (Status::PowerMeterTimeout == status || - Status::CalculatedLimitBelowMinLimit == status) - && config.PowerLimiter.IsInverterSolarPowered) { - _oTargetPowerState = true; - } - - _oTargetPowerLimitWatts = config.PowerLimiter.LowerPowerLimit; return updateInverter(); } @@ -184,29 +174,34 @@ void PowerLimiterClass::loop() return unconditionalSolarPassthrough(_inverter); } - // the normal mode of operation requires a valid - // power meter reading to calculate a power limit - if (!config.PowerMeter.Enabled) { - shutdown(Status::PowerMeterDisabled); - return; - } - - if (millis() - PowerMeter.getLastPowerMeterUpdate() > (30 * 1000)) { - shutdown(Status::PowerMeterTimeout); - return; - } - // concerns both power limits and start/stop/restart commands and is // only updated if a respective response was received from the inverter auto lastUpdateCmd = std::max( _inverter->SystemConfigPara()->getLastUpdateCommand(), _inverter->PowerCommand()->getLastUpdateCommand()); - if (_inverter->Statistics()->getLastUpdate() <= lastUpdateCmd) { - return announceStatus(Status::InverterStatsPending); + // we need inverter stats younger than the last update command + if (_oInverterStatsMillis.has_value() && lastUpdateCmd > *_oInverterStatsMillis) { + _oInverterStatsMillis = std::nullopt; } - if (PowerMeter.getLastPowerMeterUpdate() <= lastUpdateCmd) { + if (!_oInverterStatsMillis.has_value()) { + auto lastStats = _inverter->Statistics()->getLastUpdate(); + if (lastStats <= lastUpdateCmd) { + return announceStatus(Status::InverterStatsPending); + } + + _oInverterStatsMillis = lastStats; + } + + // if the power meter is being used, i.e., if its data is valid, we want to + // wait for a new reading after adjusting the inverter limit. otherwise, we + // proceed as we will use a fallback limit independent of the power meter. + // the power meter reading is expected to be at most 2 seconds old when it + // arrives. this can be the case for readings provided by networked meter + // readers, where a packet needs to travel through the network for some + // time after the actual measurement was done by the reader. + if (PowerMeter.isDataValid() && PowerMeter.getLastPowerMeterUpdate() <= (*_oInverterStatsMillis + 2000)) { return announceStatus(Status::PowerMeterPending); } @@ -352,7 +347,7 @@ int32_t PowerLimiterClass::inverterPowerDcToAc(std::shared_ptr CONFIG_T& config = Configuration.get(); float inverterEfficiencyPercent = inverter->Statistics()->getChannelFieldValue( - TYPE_AC, CH0, FLD_EFF); + TYPE_INV, CH0, FLD_EFF); // fall back to hoymiles peak efficiency as per datasheet if inverter // is currently not producing (efficiency is zero in that case) @@ -369,15 +364,30 @@ int32_t PowerLimiterClass::inverterPowerDcToAc(std::shared_ptr * can currently only be set using MQTT. in this mode of operation, the * inverter shall behave as if it was connected to the solar panels directly, * i.e., all solar power (and only solar power) is fed to the AC side, - * independent from the power meter reading. + * independent from the power meter reading. if the inverter is actually + * already connected to solar modules rather than a battery, the upper power + * limit is set as the inverter limit. */ void PowerLimiterClass::unconditionalSolarPassthrough(std::shared_ptr inverter) { + if ((millis() - _lastCalculation) < _calculationBackoffMs) { return; } + _lastCalculation = millis(); + + auto const& config = Configuration.get(); + + if (config.PowerLimiter.IsInverterSolarPowered) { + _calculationBackoffMs = 10 * 1000; + setNewPowerLimit(inverter, config.PowerLimiter.UpperPowerLimit); + announceStatus(Status::UnconditionalSolarPassthrough); + return; + } + if (!VictronMppt.isDataValid()) { shutdown(Status::NoVeDirect); return; } + _calculationBackoffMs = 1 * 1000; int32_t solarPower = VictronMppt.getPowerOutputWatts(); setNewPowerLimit(inverter, inverterPowerDcToAc(inverter, solarPower)); announceStatus(Status::UnconditionalSolarPassthrough); @@ -403,13 +413,12 @@ uint8_t PowerLimiterClass::getPowerLimiterState() { return PL_UI_STATE_INACTIVE; } -// Logic table -// | Case # | batteryPower | solarPower > 0 | useFullSolarPassthrough | Result | -// | 1 | false | false | doesn't matter | PL = 0 | -// | 2 | false | true | doesn't matter | PL = Victron Power | -// | 3 | true | doesn't matter | false | PL = PowerMeter value (Battery can supply unlimited energy) | -// | 4 | true | false | true | PL = PowerMeter value | -// | 5 | true | true | true | PL = max(PowerMeter value, Victron Power) | +// Logic table ("PowerMeter value" can be "base load setting" as a fallback) +// | Case # | batteryPower | solarPower | useFullSolarPassthrough | Resulting inverter limit | +// | 1 | false | < 20 W | doesn't matter | 0 (inverter off) | +// | 2 | false | >= 20 W | doesn't matter | min(PowerMeter value, solarPower) | +// | 3 | true | doesn't matter | false | PowerMeter value (Battery can supply unlimited energy) | +// | 4 | true | fully passed | true | max(PowerMeter value, solarPower) | bool PowerLimiterClass::calcPowerLimit(std::shared_ptr inverter, int32_t solarPowerDC, bool batteryPower) { @@ -418,6 +427,7 @@ bool PowerLimiterClass::calcPowerLimit(std::shared_ptr inverte (batteryPower?"allowed":"prevented"), solarPowerDC); } + // Case 1: if (solarPowerDC <= 0 && !batteryPower) { return shutdown(Status::NoEnergy); } @@ -431,38 +441,52 @@ bool PowerLimiterClass::calcPowerLimit(std::shared_ptr inverte return shutdown(Status::HuaweiPsu); } - auto powerMeter = static_cast(PowerMeter.getPowerTotal()); + auto meterValid = PowerMeter.isDataValid(); + + auto meterValue = static_cast(PowerMeter.getPowerTotal()); + // We don't use FLD_PAC from the statistics, because that data might be too + // old and unreliable. TODO(schlimmchen): is this comment outdated? auto inverterOutput = static_cast(inverter->Statistics()->getChannelFieldValue(TYPE_AC, CH0, FLD_PAC)); auto solarPowerAC = inverterPowerDcToAc(inverter, solarPowerDC); auto const& config = Configuration.get(); + auto targetConsumption = config.PowerLimiter.TargetPowerConsumption; + auto baseLoad = config.PowerLimiter.BaseLoadLimit; + bool meterIncludesInv = config.PowerLimiter.IsInverterBehindPowerMeter; if (_verboseLogging) { - MessageOutput.printf("[DPL::calcPowerLimit] power meter: %d W, " - "target consumption: %d W, inverter output: %d W, solar power (AC): %d\r\n", - powerMeter, - config.PowerLimiter.TargetPowerConsumption, + MessageOutput.printf("[DPL::calcPowerLimit] target consumption: %d W, " + "base load: %d W, power meter does %sinclude inverter output\r\n", + targetConsumption, + baseLoad, + (meterIncludesInv?"":"NOT ")); + + MessageOutput.printf("[DPL::calcPowerLimit] power meter value: %d W, " + "power meter valid: %s, inverter output: %d W, solar power (AC): %d W\r\n", + meterValue, + (meterValid?"yes":"no"), inverterOutput, solarPowerAC); } - auto newPowerLimit = powerMeter; + auto newPowerLimit = baseLoad; - if (config.PowerLimiter.IsInverterBehindPowerMeter) { - // If the inverter the behind the power meter (part of measurement), - // the produced power of this inverter has also to be taken into account. - // We don't use FLD_PAC from the statistics, because that - // data might be too old and unreliable. - newPowerLimit += inverterOutput; - } + if (meterValid) { + newPowerLimit = meterValue; - // We're not trying to hit 0 exactly but take an offset into account - // This means we never fully compensate the used power with the inverter - // Case 3 - newPowerLimit -= config.PowerLimiter.TargetPowerConsumption; + if (meterIncludesInv) { + // If the inverter is wired behind the power meter, i.e., if its + // output is part of the power meter measurement, the produced + // power of this inverter has to be taken into account. + newPowerLimit += inverterOutput; + } + newPowerLimit -= targetConsumption; + } + + // Case 2: if (!batteryPower) { newPowerLimit = std::min(newPowerLimit, solarPowerAC); @@ -476,6 +500,7 @@ bool PowerLimiterClass::calcPowerLimit(std::shared_ptr inverte return setNewPowerLimit(inverter, newPowerLimit); } + // Case 4: // convert all solar power if full solar-passthrough is active if (useFullSolarPassthrough()) { newPowerLimit = std::max(newPowerLimit, solarPowerAC); @@ -489,10 +514,11 @@ bool PowerLimiterClass::calcPowerLimit(std::shared_ptr inverte } if (_verboseLogging) { - MessageOutput.printf("[DPL::calcPowerLimit] match power meter with limit of %d W\r\n", + MessageOutput.printf("[DPL::calcPowerLimit] match household consumption with limit of %d W\r\n", newPowerLimit); } + // Case 3: return setNewPowerLimit(inverter, newPowerLimit); } @@ -512,15 +538,39 @@ bool PowerLimiterClass::updateInverter() if (nullptr == _inverter) { return reset(); } + // do not reset _inverterUpdateTimeouts below if no state change requested + if (!_oTargetPowerState.has_value() && !_oTargetPowerLimitWatts.has_value()) { + return reset(); + } + if (!_oUpdateStartMillis.has_value()) { _oUpdateStartMillis = millis(); } if ((millis() - *_oUpdateStartMillis) > 30 * 1000) { - MessageOutput.printf("[DPL::updateInverter] timeout, " + ++_inverterUpdateTimeouts; + MessageOutput.printf("[DPL::updateInverter] timeout (%d in succession), " "state transition pending: %s, limit pending: %s\r\n", + _inverterUpdateTimeouts, (_oTargetPowerState.has_value()?"yes":"no"), (_oTargetPowerLimitWatts.has_value()?"yes":"no")); + + // NOTE that this is not always 5 minutes, since this counts timeouts, + // not absolute time. after any timeout, an update cycle ends. a new + // timeout can only happen after starting a new update cycle, which in + // turn is only started if the DPL did calculate a new limit, which in + // turn does not happen while the inverter is unreachable, no matter + // how long (a whole night) that might be. + if (_inverterUpdateTimeouts >= 10) { + MessageOutput.println("[DPL::loop] issuing inverter restart command after update timed out repeatedly"); + _inverter->sendRestartControlRequest(); + } + + if (_inverterUpdateTimeouts >= 20) { + MessageOutput.println("[DPL::loop] restarting system since inverter is unresponsive"); + Utils::restartDtu(); + } + return reset(); } @@ -623,6 +673,8 @@ bool PowerLimiterClass::updateInverter() // enable power production only after setting the desired limit if (switchPowerState(true)) { return true; } + _inverterUpdateTimeouts = 0; + return reset(); } @@ -691,12 +743,18 @@ bool PowerLimiterClass::setNewPowerLimit(std::shared_ptr inver if (_verboseLogging) { MessageOutput.printf("[DPL::setNewPowerLimit] input limit: %d W, " - "lower limit: %d W, upper limit: %d W, hysteresis: %d W\r\n", + "min limit: %d W, max limit: %d W, hysteresis: %d W\r\n", newPowerLimit, lowerLimit, upperLimit, hysteresis); } if (newPowerLimit < lowerLimit) { - return shutdown(Status::CalculatedLimitBelowMinLimit); + if (!config.PowerLimiter.IsInverterSolarPowered) { + return shutdown(Status::CalculatedLimitBelowMinLimit); + } + + MessageOutput.println("[DPL::setNewPowerLimit] keep solar-powered " + "inverter running at min limit"); + newPowerLimit = lowerLimit; } // enforce configured upper power limit diff --git a/src/PowerMeter.cpp b/src/PowerMeter.cpp index 72526b7d7..63cce4216 100644 --- a/src/PowerMeter.cpp +++ b/src/PowerMeter.cpp @@ -136,6 +136,23 @@ uint32_t PowerMeterClass::getLastPowerMeterUpdate() return _lastPowerMeterUpdate; } +bool PowerMeterClass::isDataValid() +{ + auto const& config = Configuration.get(); + + std::lock_guard l(_mutex); + + bool valid = config.PowerMeter.Enabled && + _lastPowerMeterUpdate > 0 && + ((millis() - _lastPowerMeterUpdate) < (30 * 1000)); + + // reset if timed out to avoid glitch once + // (millis() - _lastPowerMeterUpdate) overflows + if (!valid) { _lastPowerMeterUpdate = 0; } + + return valid; +} + void PowerMeterClass::mqtt() { if (!MqttSettings.getConnected()) { return; } diff --git a/src/Utils.cpp b/src/Utils.cpp index 938b002da..6bedd2cbd 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -69,9 +69,9 @@ void Utils::restartDtu() ESP.restart(); } -bool Utils::checkJsonAlloc(const DynamicJsonDocument& doc, const char* function, const uint16_t line) +bool Utils::checkJsonAlloc(const JsonDocument& doc, const char* function, const uint16_t line) { - if (doc.capacity() == 0) { + if (doc.overflowed()) { MessageOutput.printf("Alloc failed: %s, %d\r\n", function, line); return false; } @@ -79,16 +79,6 @@ bool Utils::checkJsonAlloc(const DynamicJsonDocument& doc, const char* function, return true; } -bool Utils::checkJsonOverflow(const DynamicJsonDocument& doc, const char* function, const uint16_t line) -{ - if (doc.overflowed()) { - MessageOutput.printf("DynamicJsonDocument overflowed: %s, %d\r\n", function, line); - return true; - } - - return false; -} - /// @brief Remove all files but the PINMAPPING_FILENAME void Utils::removeAllFiles() { diff --git a/src/VictronMppt.cpp b/src/VictronMppt.cpp index e39cf3aad..770be014b 100644 --- a/src/VictronMppt.cpp +++ b/src/VictronMppt.cpp @@ -119,7 +119,7 @@ uint32_t VictronMpptClass::getDataAgeMillis(size_t idx) const return millis() - _controllers[idx]->getLastUpdate(); } -std::optional VictronMpptClass::getData(size_t idx) const +std::optional VictronMpptClass::getData(size_t idx) const { std::lock_guard lock(_mutex); @@ -129,7 +129,9 @@ std::optional VictronMpptClass::getData(size_t return std::nullopt; } - return std::optional{_controllers[idx]->getData()}; + if (!_controllers[idx]->isDataValid()) { return std::nullopt; } + + return _controllers[idx]->getData(); } int32_t VictronMpptClass::getPowerOutputWatts() const @@ -138,7 +140,18 @@ int32_t VictronMpptClass::getPowerOutputWatts() const for (const auto& upController : _controllers) { if (!upController->isDataValid()) { continue; } - sum += upController->getData()->P; + + // if any charge controller is part of a VE.Smart network, and if the + // charge controller is connected in a way that allows to send + // requests, we should have the "network total DC input power" + // available. if so, to estimate the output power, we multiply by + // the calculated efficiency of the connected charge controller. + auto networkPower = upController->getData().NetworkTotalDcInputPowerMilliWatts; + if (networkPower.first > 0) { + return static_cast(networkPower.second / 1000.0 * upController->getData().mpptEfficiency_Percent / 100); + } + + sum += upController->getData().batteryOutputPower_W; } return sum; @@ -150,43 +163,52 @@ int32_t VictronMpptClass::getPanelPowerWatts() const for (const auto& upController : _controllers) { if (!upController->isDataValid()) { continue; } - sum += upController->getData()->PPV; + + // if any charge controller is part of a VE.Smart network, and if the + // charge controller is connected in a way that allows to send + // requests, we should have the "network total DC input power" available. + auto networkPower = upController->getData().NetworkTotalDcInputPowerMilliWatts; + if (networkPower.first > 0) { + return static_cast(networkPower.second / 1000.0); + } + + sum += upController->getData().panelPower_PPV_W; } return sum; } -double VictronMpptClass::getYieldTotal() const +float VictronMpptClass::getYieldTotal() const { - double sum = 0; + float sum = 0; for (const auto& upController : _controllers) { if (!upController->isDataValid()) { continue; } - sum += upController->getData()->H19; + sum += upController->getData().yieldTotal_H19_Wh / 1000.0; } return sum; } -double VictronMpptClass::getYieldDay() const +float VictronMpptClass::getYieldDay() const { - double sum = 0; + float sum = 0; for (const auto& upController : _controllers) { if (!upController->isDataValid()) { continue; } - sum += upController->getData()->H20; + sum += upController->getData().yieldToday_H20_Wh / 1000.0; } return sum; } -double VictronMpptClass::getOutputVoltage() const +float VictronMpptClass::getOutputVoltage() const { - double min = -1; + float min = -1; for (const auto& upController : _controllers) { if (!upController->isDataValid()) { continue; } - double volts = upController->getData()->V; + float volts = upController->getData().batteryVoltage_V_mV / 1000.0; if (min == -1) { min = volts; } min = std::min(min, volts); } diff --git a/src/VictronSmartShunt.cpp b/src/VictronSmartShunt.cpp index 7b6da145a..e3da07fa4 100644 --- a/src/VictronSmartShunt.cpp +++ b/src/VictronSmartShunt.cpp @@ -31,6 +31,6 @@ void VictronSmartShunt::loop() if (VeDirectShunt.getLastUpdate() <= _lastUpdate) { return; } - _stats->updateFrom(VeDirectShunt.veFrame); + _stats->updateFrom(VeDirectShunt.getData()); _lastUpdate = VeDirectShunt.getLastUpdate(); } diff --git a/src/WebApi.cpp b/src/WebApi.cpp index d00f08702..c2dcf3f10 100644 --- a/src/WebApi.cpp +++ b/src/WebApi.cpp @@ -4,6 +4,7 @@ */ #include "WebApi.h" #include "Configuration.h" +#include "MessageOutput.h" #include "defaults.h" #include @@ -93,4 +94,58 @@ void WebApiClass::writeConfig(JsonVariant& retMsg, const WebApiError code, const } } +bool WebApiClass::parseRequestData(AsyncWebServerRequest* request, AsyncJsonResponse* response, JsonDocument& json_document) +{ + auto& retMsg = response->getRoot(); + retMsg["type"] = "warning"; + + if (!request->hasParam("data", true)) { + retMsg["message"] = "No values found!"; + retMsg["code"] = WebApiError::GenericNoValueFound; + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); + return false; + } + + const String json = request->getParam("data", true)->value(); + const DeserializationError error = deserializeJson(json_document, json); + if (error) { + retMsg["message"] = "Failed to parse data!"; + retMsg["code"] = WebApiError::GenericParseError; + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); + return false; + } + + return true; +} + +uint64_t WebApiClass::parseSerialFromRequest(AsyncWebServerRequest* request, String param_name) +{ + if (request->hasParam(param_name)) { + String s = request->getParam(param_name)->value(); + return strtoll(s.c_str(), NULL, 16); + } + + return 0; +} + +bool WebApiClass::sendJsonResponse(AsyncWebServerRequest* request, AsyncJsonResponse* response, const char* function, const uint16_t line) +{ + bool ret_val = true; + if (response->overflowed()) { + auto& root = response->getRoot(); + + root.clear(); + root["message"] = String("500 Internal Server Error: ") + function + ", " + line; + root["code"] = WebApiError::GenericInternalServerError; + root["type"] = "danger"; + response->setCode(500); + MessageOutput.printf("WebResponse failed: %s, %d\r\n", function, line); + ret_val = false; + } + + response->setLength(); + request->send(response); + return ret_val; +} + WebApiClass WebApi; diff --git a/src/WebApi_Huawei.cpp b/src/WebApi_Huawei.cpp index 778833d11..da6b38b39 100644 --- a/src/WebApi_Huawei.cpp +++ b/src/WebApi_Huawei.cpp @@ -72,40 +72,16 @@ void WebApiHuaweiClass::onPost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } - DynamicJsonDocument root(1024); - DeserializationError error = deserializeJson(root, json); float value; uint8_t online = true; float minimal_voltage; - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); - return; - } + auto& retMsg = response->getRoot(); if (root.containsKey("online")) { online = root["online"].as(); @@ -164,12 +140,9 @@ void WebApiHuaweiClass::onPost(AsyncWebServerRequest* request) } } - retMsg["type"] = "success"; - retMsg["message"] = "Settings saved!"; - retMsg["code"] = WebApiError::GenericSuccess; + WebApi.writeConfig(retMsg); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } @@ -186,12 +159,17 @@ void WebApiHuaweiClass::onAdminGet(AsyncWebServerRequest* request) const CONFIG_T& config = Configuration.get(); root["enabled"] = config.Huawei.Enabled; + root["verbose_logging"] = config.Huawei.VerboseLogging; root["can_controller_frequency"] = config.Huawei.CAN_Controller_Frequency; root["auto_power_enabled"] = config.Huawei.Auto_Power_Enabled; + root["auto_power_batterysoc_limits_enabled"] = config.Huawei.Auto_Power_BatterySoC_Limits_Enabled; + root["emergency_charge_enabled"] = config.Huawei.Emergency_Charge_Enabled; root["voltage_limit"] = static_cast(config.Huawei.Auto_Power_Voltage_Limit * 100) / 100.0; root["enable_voltage_limit"] = static_cast(config.Huawei.Auto_Power_Enable_Voltage_Limit * 100) / 100.0; root["lower_power_limit"] = config.Huawei.Auto_Power_Lower_Power_Limit; root["upper_power_limit"] = config.Huawei.Auto_Power_Upper_Power_Limit; + root["stop_batterysoc_threshold"] = config.Huawei.Auto_Power_Stop_BatterySoC_Threshold; + root["target_power_consumption"] = config.Huawei.Auto_Power_Target_Power_Consumption; response->setLength(); request->send(response); @@ -202,43 +180,19 @@ void WebApiHuaweiClass::onAdminPost(AsyncWebServerRequest* request) if (!WebApi.checkCredentials(request)) { return; } - - AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - String json = request->getParam("data", true)->value(); - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } - DynamicJsonDocument root(1024); - DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); - return; - } + auto& retMsg = response->getRoot(); if (!(root.containsKey("enabled")) || !(root.containsKey("can_controller_frequency")) || !(root.containsKey("auto_power_enabled")) || + !(root.containsKey("emergency_charge_enabled")) || !(root.containsKey("voltage_limit")) || !(root.containsKey("lower_power_limit")) || !(root.containsKey("upper_power_limit"))) { @@ -251,17 +205,21 @@ void WebApiHuaweiClass::onAdminPost(AsyncWebServerRequest* request) CONFIG_T& config = Configuration.get(); config.Huawei.Enabled = root["enabled"].as(); + config.Huawei.VerboseLogging = root["verbose_logging"]; config.Huawei.CAN_Controller_Frequency = root["can_controller_frequency"].as(); config.Huawei.Auto_Power_Enabled = root["auto_power_enabled"].as(); + config.Huawei.Auto_Power_BatterySoC_Limits_Enabled = root["auto_power_batterysoc_limits_enabled"].as(); + config.Huawei.Emergency_Charge_Enabled = root["emergency_charge_enabled"].as(); config.Huawei.Auto_Power_Voltage_Limit = root["voltage_limit"].as(); config.Huawei.Auto_Power_Enable_Voltage_Limit = root["enable_voltage_limit"].as(); config.Huawei.Auto_Power_Lower_Power_Limit = root["lower_power_limit"].as(); config.Huawei.Auto_Power_Upper_Power_Limit = root["upper_power_limit"].as(); - + config.Huawei.Auto_Power_Stop_BatterySoC_Threshold = root["stop_batterysoc_threshold"]; + config.Huawei.Auto_Power_Target_Power_Consumption = root["target_power_consumption"]; + WebApi.writeConfig(retMsg); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); // TODO(schlimmchen): HuaweiCan has no real concept of the fact that the // config might change. at least not regarding CAN parameters. until that diff --git a/src/WebApi_battery.cpp b/src/WebApi_battery.cpp index 3f26d83cc..798957d3b 100644 --- a/src/WebApi_battery.cpp +++ b/src/WebApi_battery.cpp @@ -59,43 +59,17 @@ void WebApiBatteryClass::onAdminPost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } - DynamicJsonDocument root(1024); - DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); - return; - } + auto& retMsg = response->getRoot(); if (!root.containsKey("enabled") || !root.containsKey("provider")) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -110,8 +84,7 @@ void WebApiBatteryClass::onAdminPost(AsyncWebServerRequest* request) WebApi.writeConfig(retMsg); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); Battery.updateSettings(); MqttHandleBatteryHass.forceUpdate(); diff --git a/src/WebApi_config.cpp b/src/WebApi_config.cpp index 29f353192..759b6b243 100644 --- a/src/WebApi_config.cpp +++ b/src/WebApi_config.cpp @@ -40,6 +40,7 @@ void WebApiConfigClass::onConfigGet(AsyncWebServerRequest* request) requestFile = name; } else { request->send(404); + return; } } @@ -53,51 +54,24 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - const String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } - DynamicJsonDocument root(1024); - const DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); - return; - } + auto& retMsg = response->getRoot(); if (!(root.containsKey("delete"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["delete"].as() == false) { retMsg["message"] = "Not deleted anything!"; retMsg["code"] = WebApiError::ConfigNotDeleted; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -105,8 +79,7 @@ void WebApiConfigClass::onConfigDelete(AsyncWebServerRequest* request) retMsg["message"] = "Configuration resettet. Rebooting now..."; retMsg["code"] = WebApiError::ConfigSuccess; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); Utils::removeAllFiles(); Utils::restartDtu(); @@ -120,7 +93,7 @@ void WebApiConfigClass::onConfigListGet(AsyncWebServerRequest* request) AsyncJsonResponse* response = new AsyncJsonResponse(); auto& root = response->getRoot(); - auto data = root.createNestedArray("configs"); + auto data = root["configs"].to(); File rootfs = LittleFS.open("/"); File file = rootfs.openNextFile(); @@ -128,15 +101,14 @@ void WebApiConfigClass::onConfigListGet(AsyncWebServerRequest* request) if (file.isDirectory()) { continue; } - JsonObject obj = data.createNestedObject(); + JsonObject obj = data.add(); obj["name"] = String(file.name()); file = rootfs.openNextFile(); } file.close(); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiConfigClass::onConfigUploadFinish(AsyncWebServerRequest* request) diff --git a/src/WebApi_device.cpp b/src/WebApi_device.cpp index 9ab8d4fa2..e6c3170a8 100644 --- a/src/WebApi_device.cpp +++ b/src/WebApi_device.cpp @@ -26,15 +26,15 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request) return; } - AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE); + AsyncJsonResponse* response = new AsyncJsonResponse(); auto& root = response->getRoot(); const CONFIG_T& config = Configuration.get(); const PinMapping_t& pin = PinMapping.get(); - auto curPin = root.createNestedObject("curPin"); + auto curPin = root["curPin"].to(); curPin["name"] = config.Dev_PinMapping; - auto nrfPinObj = curPin.createNestedObject("nrf24"); + auto nrfPinObj = curPin["nrf24"].to(); nrfPinObj["clk"] = pin.nrf24_clk; nrfPinObj["cs"] = pin.nrf24_cs; nrfPinObj["en"] = pin.nrf24_en; @@ -42,7 +42,7 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request) nrfPinObj["miso"] = pin.nrf24_miso; nrfPinObj["mosi"] = pin.nrf24_mosi; - auto cmtPinObj = curPin.createNestedObject("cmt"); + auto cmtPinObj = curPin["cmt"].to(); cmtPinObj["clk"] = pin.cmt_clk; cmtPinObj["cs"] = pin.cmt_cs; cmtPinObj["fcs"] = pin.cmt_fcs; @@ -50,7 +50,7 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request) cmtPinObj["gpio2"] = pin.cmt_gpio2; cmtPinObj["gpio3"] = pin.cmt_gpio3; - auto ethPinObj = curPin.createNestedObject("eth"); + auto ethPinObj = curPin["eth"].to(); ethPinObj["enabled"] = pin.eth_enabled; ethPinObj["phy_addr"] = pin.eth_phy_addr; ethPinObj["power"] = pin.eth_power; @@ -59,19 +59,19 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request) ethPinObj["type"] = pin.eth_type; ethPinObj["clk_mode"] = pin.eth_clk_mode; - auto displayPinObj = curPin.createNestedObject("display"); + auto displayPinObj = curPin["display"].to(); displayPinObj["type"] = pin.display_type; displayPinObj["data"] = pin.display_data; displayPinObj["clk"] = pin.display_clk; displayPinObj["cs"] = pin.display_cs; displayPinObj["reset"] = pin.display_reset; - auto ledPinObj = curPin.createNestedObject("led"); + auto ledPinObj = curPin["led"].to(); for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) { ledPinObj["led" + String(i)] = pin.led[i]; } - auto display = root.createNestedObject("display"); + auto display = root["display"].to(); display["rotation"] = config.Display.Rotation; display["power_safe"] = config.Display.PowerSafe; display["screensaver"] = config.Display.ScreenSaver; @@ -80,25 +80,25 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request) display["diagramduration"] = config.Display.Diagram.Duration; display["diagrammode"] = config.Display.Diagram.Mode; - auto leds = root.createNestedArray("led"); + auto leds = root["led"].to(); for (uint8_t i = 0; i < PINMAPPING_LED_COUNT; i++) { - auto led = leds.createNestedObject(); + auto led = leds.add(); led["brightness"] = config.Led_Single[i].Brightness; } - auto victronPinObj = curPin.createNestedObject("victron"); + auto victronPinObj = curPin["victron"].to(); victronPinObj["rx"] = pin.victron_rx; victronPinObj["tx"] = pin.victron_tx; victronPinObj["rx2"] = pin.victron_rx2; victronPinObj["tx2"] = pin.victron_tx2; - JsonObject batteryPinObj = curPin.createNestedObject("battery"); + auto batteryPinObj = curPin["battery"].to(); batteryPinObj["rx"] = pin.battery_rx; batteryPinObj["rxen"] = pin.battery_rxen; batteryPinObj["tx"] = pin.battery_tx; batteryPinObj["txen"] = pin.battery_txen; - JsonObject huaweiPinObj = curPin.createNestedObject("huawei"); + auto huaweiPinObj = curPin["huawei"].to(); huaweiPinObj["miso"] = pin.huawei_miso; huaweiPinObj["mosi"] = pin.huawei_mosi; huaweiPinObj["clk"] = pin.huawei_clk; @@ -106,8 +106,7 @@ void WebApiDeviceClass::onDeviceAdminGet(AsyncWebServerRequest* request) huaweiPinObj["cs"] = pin.huawei_cs; huaweiPinObj["power"] = pin.huawei_power; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request) @@ -116,45 +115,19 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request) return; } - AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - const String json = request->getParam("data", true)->value(); - - if (json.length() > MQTT_JSON_DOC_SIZE) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } - DynamicJsonDocument root(MQTT_JSON_DOC_SIZE); - const DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); - return; - } + auto& retMsg = response->getRoot(); if (!(root.containsKey("curPin") || root.containsKey("display"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -162,8 +135,7 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request) retMsg["message"] = "Pin mapping must between 1 and " STR(DEV_MAX_MAPPING_NAME_STRLEN) " characters long!"; retMsg["code"] = WebApiError::HardwarePinMappingLength; retMsg["param"]["max"] = DEV_MAX_MAPPING_NAME_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -194,8 +166,7 @@ void WebApiDeviceClass::onDeviceAdminPost(AsyncWebServerRequest* request) WebApi.writeConfig(retMsg); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); if (performRestart) { Utils::restartDtu(); diff --git a/src/WebApi_devinfo.cpp b/src/WebApi_devinfo.cpp index 212a7f7d5..449cd1772 100644 --- a/src/WebApi_devinfo.cpp +++ b/src/WebApi_devinfo.cpp @@ -23,13 +23,7 @@ void WebApiDevInfoClass::onDevInfoStatus(AsyncWebServerRequest* request) AsyncJsonResponse* response = new AsyncJsonResponse(); auto& root = response->getRoot(); - - uint64_t serial = 0; - if (request->hasParam("inv")) { - String s = request->getParam("inv")->value(); - serial = strtoll(s.c_str(), NULL, 16); - } - + auto serial = WebApi.parseSerialFromRequest(request); auto inv = Hoymiles.getInverterBySerial(serial); if (inv != nullptr) { @@ -43,6 +37,5 @@ void WebApiDevInfoClass::onDevInfoStatus(AsyncWebServerRequest* request) root["fw_build_datetime"] = inv->DevInfo()->getFwBuildDateTimeStr(); } - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } diff --git a/src/WebApi_dtu.cpp b/src/WebApi_dtu.cpp index 2ccb20b27..8e587f852 100644 --- a/src/WebApi_dtu.cpp +++ b/src/WebApi_dtu.cpp @@ -63,10 +63,10 @@ void WebApiDtuClass::onDtuAdminGet(AsyncWebServerRequest* request) root["cmt_country"] = config.Dtu.Cmt.CountryMode; root["cmt_chan_width"] = Hoymiles.getRadioCmt()->getChannelWidth(); - auto data = root.createNestedArray("country_def"); + auto data = root["country_def"].to(); auto countryDefs = Hoymiles.getRadioCmt()->getCountryFrequencyList(); for (const auto& definition : countryDefs) { - auto obj = data.createNestedObject(); + auto obj = data.add(); obj["freq_default"] = definition.definition.Freq_Default; obj["freq_min"] = definition.definition.Freq_Min; obj["freq_max"] = definition.definition.Freq_Max; @@ -74,8 +74,7 @@ void WebApiDtuClass::onDtuAdminGet(AsyncWebServerRequest* request) obj["freq_legal_max"] = definition.definition.Freq_Legal_Max; } - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) @@ -85,37 +84,12 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - const String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } - DynamicJsonDocument root(1024); - const DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); - return; - } + auto& retMsg = response->getRoot(); if (!(root.containsKey("serial") && root.containsKey("pollinterval") @@ -126,8 +100,7 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) && root.containsKey("cmt_country"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -137,40 +110,35 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) if (serial == 0) { retMsg["message"] = "Serial cannot be zero!"; retMsg["code"] = WebApiError::DtuSerialZero; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["pollinterval"].as() == 0) { retMsg["message"] = "Poll interval must be greater zero!"; retMsg["code"] = WebApiError::DtuPollZero; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["nrf_palevel"].as() > 3) { retMsg["message"] = "Invalid power level setting!"; retMsg["code"] = WebApiError::DtuInvalidPowerLevel; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["cmt_palevel"].as() < -10 || root["cmt_palevel"].as() > 20) { retMsg["message"] = "Invalid power level setting!"; retMsg["code"] = WebApiError::DtuInvalidPowerLevel; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["cmt_country"].as() >= CountryModeId_t::CountryModeId_Max) { retMsg["message"] = "Invalid country setting!"; retMsg["code"] = WebApiError::DtuInvalidCmtCountry; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -183,8 +151,7 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) retMsg["code"] = WebApiError::DtuInvalidCmtFrequency; retMsg["param"]["min"] = FrequencyDefinition.Freq_Min; retMsg["param"]["max"] = FrequencyDefinition.Freq_Max; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -200,8 +167,8 @@ void WebApiDtuClass::onDtuAdminPost(AsyncWebServerRequest* request) WebApi.writeConfig(retMsg); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); _applyDataTask.enable(); + _applyDataTask.restart(); } diff --git a/src/WebApi_eventlog.cpp b/src/WebApi_eventlog.cpp index 51e85affa..ec8b78c30 100644 --- a/src/WebApi_eventlog.cpp +++ b/src/WebApi_eventlog.cpp @@ -20,14 +20,9 @@ void WebApiEventlogClass::onEventlogStatus(AsyncWebServerRequest* request) return; } - AsyncJsonResponse* response = new AsyncJsonResponse(false, 2048); + AsyncJsonResponse* response = new AsyncJsonResponse(); auto& root = response->getRoot(); - - uint64_t serial = 0; - if (request->hasParam("inv")) { - String s = request->getParam("inv")->value(); - serial = strtoll(s.c_str(), NULL, 16); - } + auto serial = WebApi.parseSerialFromRequest(request); AlarmMessageLocale_t locale = AlarmMessageLocale_t::EN; if (request->hasParam("locale")) { @@ -47,10 +42,10 @@ void WebApiEventlogClass::onEventlogStatus(AsyncWebServerRequest* request) uint8_t logEntryCount = inv->EventLog()->getEntryCount(); root["count"] = logEntryCount; - JsonArray eventsArray = root.createNestedArray("events"); + JsonArray eventsArray = root["events"].to(); for (uint8_t logEntry = 0; logEntry < logEntryCount; logEntry++) { - JsonObject eventsObject = eventsArray.createNestedObject(); + JsonObject eventsObject = eventsArray.add(); AlarmLogEntry_t entry; inv->EventLog()->getLogEntry(logEntry, entry, locale); @@ -62,6 +57,5 @@ void WebApiEventlogClass::onEventlogStatus(AsyncWebServerRequest* request) } } - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } diff --git a/src/WebApi_gridprofile.cpp b/src/WebApi_gridprofile.cpp index 60c340fa0..9fc05b032 100644 --- a/src/WebApi_gridprofile.cpp +++ b/src/WebApi_gridprofile.cpp @@ -21,32 +21,26 @@ void WebApiGridProfileClass::onGridProfileStatus(AsyncWebServerRequest* request) return; } - AsyncJsonResponse* response = new AsyncJsonResponse(false, 8192); + AsyncJsonResponse* response = new AsyncJsonResponse(); auto& root = response->getRoot(); - - uint64_t serial = 0; - if (request->hasParam("inv")) { - String s = request->getParam("inv")->value(); - serial = strtoll(s.c_str(), NULL, 16); - } - + auto serial = WebApi.parseSerialFromRequest(request); auto inv = Hoymiles.getInverterBySerial(serial); if (inv != nullptr) { root["name"] = inv->GridProfile()->getProfileName(); root["version"] = inv->GridProfile()->getProfileVersion(); - auto jsonSections = root.createNestedArray("sections"); + auto jsonSections = root["sections"].to(); auto profSections = inv->GridProfile()->getProfile(); for (auto &profSection : profSections) { - auto jsonSection = jsonSections.createNestedObject(); + auto jsonSection = jsonSections.add(); jsonSection["name"] = profSection.SectionName; - auto jsonItems = jsonSection.createNestedArray("items"); + auto jsonItems = jsonSection["items"].to(); for (auto &profItem : profSection.items) { - auto jsonItem = jsonItems.createNestedObject(); + auto jsonItem = jsonItems.add(); jsonItem["n"] = profItem.Name; jsonItem["u"] = profItem.Unit; @@ -55,8 +49,7 @@ void WebApiGridProfileClass::onGridProfileStatus(AsyncWebServerRequest* request) } } - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiGridProfileClass::onGridProfileRawdata(AsyncWebServerRequest* request) @@ -65,24 +58,17 @@ void WebApiGridProfileClass::onGridProfileRawdata(AsyncWebServerRequest* request return; } - AsyncJsonResponse* response = new AsyncJsonResponse(false, 4096); + AsyncJsonResponse* response = new AsyncJsonResponse(); auto& root = response->getRoot(); - - uint64_t serial = 0; - if (request->hasParam("inv")) { - String s = request->getParam("inv")->value(); - serial = strtoll(s.c_str(), NULL, 16); - } - + auto serial = WebApi.parseSerialFromRequest(request); auto inv = Hoymiles.getInverterBySerial(serial); if (inv != nullptr) { - auto raw = root.createNestedArray("raw"); + auto raw = root["raw"].to(); auto data = inv->GridProfile()->getRawData(); copyArray(&data[0], data.size(), raw); } - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } diff --git a/src/WebApi_inverter.cpp b/src/WebApi_inverter.cpp index 5f5e41016..2d9a56344 100644 --- a/src/WebApi_inverter.cpp +++ b/src/WebApi_inverter.cpp @@ -29,15 +29,15 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request) return; } - AsyncJsonResponse* response = new AsyncJsonResponse(false, 768 * INV_MAX_COUNT); + AsyncJsonResponse* response = new AsyncJsonResponse(); auto& root = response->getRoot(); - JsonArray data = root.createNestedArray("inverter"); + JsonArray data = root["inverter"].to(); const CONFIG_T& config = Configuration.get(); for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { if (config.Inverter[i].Serial > 0) { - JsonObject obj = data.createNestedObject(); + JsonObject obj = data.add(); obj["id"] = i; obj["name"] = String(config.Inverter[i].Name); obj["order"] = config.Inverter[i].Order; @@ -67,9 +67,9 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request) max_channels = inv->Statistics()->getChannelsByType(TYPE_DC).size(); } - JsonArray channel = obj.createNestedArray("channel"); + JsonArray channel = obj["channel"].to(); for (uint8_t c = 0; c < max_channels; c++) { - JsonObject chanData = channel.createNestedObject(); + JsonObject chanData = channel.add(); chanData["name"] = config.Inverter[i].channel[c].Name; chanData["max_power"] = config.Inverter[i].channel[c].MaxChannelPower; chanData["yield_total_offset"] = config.Inverter[i].channel[c].YieldTotalOffset; @@ -77,8 +77,7 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request) } } - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) @@ -88,44 +87,18 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } - const String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); - return; - } - - DynamicJsonDocument root(1024); - const DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); - return; - } + auto& retMsg = response->getRoot(); if (!(root.containsKey("serial") && root.containsKey("name"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -135,8 +108,7 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) if (serial == 0) { retMsg["message"] = "Serial must be a number > 0!"; retMsg["code"] = WebApiError::InverterSerialZero; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -144,8 +116,7 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) retMsg["message"] = "Name must between 1 and " STR(INV_MAX_NAME_STRLEN) " characters long!"; retMsg["code"] = WebApiError::InverterNameLength; retMsg["param"]["max"] = INV_MAX_NAME_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -155,8 +126,7 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) retMsg["message"] = "Only " STR(INV_MAX_COUNT) " inverters are supported!"; retMsg["code"] = WebApiError::InverterCount; retMsg["param"]["max"] = INV_MAX_COUNT; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -167,8 +137,7 @@ void WebApiInverterClass::onInverterAdd(AsyncWebServerRequest* request) WebApi.writeConfig(retMsg, WebApiError::InverterAdded, "Inverter created!"); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); auto inv = Hoymiles.addInverter(inverter->Name, inverter->Serial); @@ -188,51 +157,24 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - const String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } - DynamicJsonDocument root(1024); - const DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); - return; - } + auto& retMsg = response->getRoot(); if (!(root.containsKey("id") && root.containsKey("serial") && root.containsKey("name") && root.containsKey("channel"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["id"].as() > INV_MAX_COUNT - 1) { retMsg["message"] = "Invalid ID specified!"; retMsg["code"] = WebApiError::InverterInvalidId; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -242,8 +184,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) if (serial == 0) { retMsg["message"] = "Serial must be a number > 0!"; retMsg["code"] = WebApiError::InverterSerialZero; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -251,8 +192,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) retMsg["message"] = "Name must between 1 and " STR(INV_MAX_NAME_STRLEN) " characters long!"; retMsg["code"] = WebApiError::InverterNameLength; retMsg["param"]["max"] = INV_MAX_NAME_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -260,8 +200,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) if (channelArray.size() == 0 || channelArray.size() > INV_MAX_CHAN_COUNT) { retMsg["message"] = "Invalid amount of max channel setting given!"; retMsg["code"] = WebApiError::InverterInvalidMaxChannel; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -293,8 +232,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) WebApi.writeConfig(retMsg, WebApiError::InverterChanged, "Inverter changed!"); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); std::shared_ptr inv = Hoymiles.getInverterBySerial(old_serial); @@ -333,51 +271,24 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - const String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } - DynamicJsonDocument root(1024); - const DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); - return; - } + auto& retMsg = response->getRoot(); if (!(root.containsKey("id"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["id"].as() > INV_MAX_COUNT - 1) { retMsg["message"] = "Invalid ID specified!"; retMsg["code"] = WebApiError::InverterInvalidId; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -390,8 +301,7 @@ void WebApiInverterClass::onInverterDelete(AsyncWebServerRequest* request) WebApi.writeConfig(retMsg, WebApiError::InverterDeleted, "Inverter deleted!"); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); MqttHandleHass.forceUpdate(); } @@ -403,43 +313,17 @@ void WebApiInverterClass::onInverterOrder(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - const String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } - DynamicJsonDocument root(1024); - const DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); - return; - } + auto& retMsg = response->getRoot(); if (!(root.containsKey("order"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -457,6 +341,5 @@ void WebApiInverterClass::onInverterOrder(AsyncWebServerRequest* request) WebApi.writeConfig(retMsg, WebApiError::InverterOrdered, "Inverter order saved!"); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } diff --git a/src/WebApi_limit.cpp b/src/WebApi_limit.cpp index b5b9e1726..9a622deae 100644 --- a/src/WebApi_limit.cpp +++ b/src/WebApi_limit.cpp @@ -47,8 +47,7 @@ void WebApiLimitClass::onLimitStatus(AsyncWebServerRequest* request) root[serial]["limit_set_status"] = limitStatus; } - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) @@ -58,45 +57,19 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - const String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } - DynamicJsonDocument root(1024); - const DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); - return; - } + auto& retMsg = response->getRoot(); if (!(root.containsKey("serial") && root.containsKey("limit_value") && root.containsKey("limit_type"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -106,8 +79,7 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) if (serial == 0) { retMsg["message"] = "Serial must be a number > 0!"; retMsg["code"] = WebApiError::LimitSerialZero; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -115,8 +87,7 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) retMsg["message"] = "Limit must between 0 and " STR(MAX_INVERTER_LIMIT) "!"; retMsg["code"] = WebApiError::LimitInvalidLimit; retMsg["param"]["max"] = MAX_INVERTER_LIMIT; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -127,8 +98,7 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) retMsg["message"] = "Invalid type specified!"; retMsg["code"] = WebApiError::LimitInvalidType; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -139,8 +109,7 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) if (inv == nullptr) { retMsg["message"] = "Invalid inverter specified!"; retMsg["code"] = WebApiError::LimitInvalidInverter; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -150,6 +119,5 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) retMsg["message"] = "Settings saved!"; retMsg["code"] = WebApiError::GenericSuccess; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } diff --git a/src/WebApi_maintenance.cpp b/src/WebApi_maintenance.cpp index ba257efa8..1504f9d75 100644 --- a/src/WebApi_maintenance.cpp +++ b/src/WebApi_maintenance.cpp @@ -22,44 +22,18 @@ void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request) return; } - AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - const String json = request->getParam("data", true)->value(); - - if (json.length() > MQTT_JSON_DOC_SIZE) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } - DynamicJsonDocument root(MQTT_JSON_DOC_SIZE); - const DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); - return; - } + auto& retMsg = response->getRoot(); if (!(root.containsKey("reboot"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -68,14 +42,12 @@ void WebApiMaintenanceClass::onRebootPost(AsyncWebServerRequest* request) retMsg["message"] = "Reboot triggered!"; retMsg["code"] = WebApiError::MaintenanceRebootTriggered; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); Utils::restartDtu(); } else { retMsg["message"] = "Reboot cancled!"; retMsg["code"] = WebApiError::MaintenanceRebootCancled; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } } diff --git a/src/WebApi_mqtt.cpp b/src/WebApi_mqtt.cpp index 9e7411bfe..a032a34db 100644 --- a/src/WebApi_mqtt.cpp +++ b/src/WebApi_mqtt.cpp @@ -30,7 +30,7 @@ void WebApiMqttClass::onMqttStatus(AsyncWebServerRequest* request) return; } - AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE); + AsyncJsonResponse* response = new AsyncJsonResponse(); auto& root = response->getRoot(); const CONFIG_T& config = Configuration.get(); @@ -55,8 +55,7 @@ void WebApiMqttClass::onMqttStatus(AsyncWebServerRequest* request) root["mqtt_hass_topic"] = config.Mqtt.Hass.Topic; root["mqtt_hass_individualpanels"] = config.Mqtt.Hass.IndividualPanels; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiMqttClass::onMqttAdminGet(AsyncWebServerRequest* request) @@ -65,7 +64,7 @@ void WebApiMqttClass::onMqttAdminGet(AsyncWebServerRequest* request) return; } - AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE); + AsyncJsonResponse* response = new AsyncJsonResponse(); auto& root = response->getRoot(); const CONFIG_T& config = Configuration.get(); @@ -94,8 +93,7 @@ void WebApiMqttClass::onMqttAdminGet(AsyncWebServerRequest* request) root["mqtt_hass_topic"] = config.Mqtt.Hass.Topic; root["mqtt_hass_individualpanels"] = config.Mqtt.Hass.IndividualPanels; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) @@ -104,38 +102,13 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) return; } - AsyncJsonResponse* response = new AsyncJsonResponse(false, MQTT_JSON_DOC_SIZE); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - const String json = request->getParam("data", true)->value(); - - if (json.length() > MQTT_JSON_DOC_SIZE) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } - DynamicJsonDocument root(MQTT_JSON_DOC_SIZE); - const DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); - return; - } + auto& retMsg = response->getRoot(); if (!(root.containsKey("mqtt_enabled") && root.containsKey("mqtt_verbose_logging") @@ -162,8 +135,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) && root.containsKey("mqtt_hass_individualpanels"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -172,8 +144,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) retMsg["message"] = "MqTT Server must between 1 and " STR(MQTT_MAX_HOSTNAME_STRLEN) " characters long!"; retMsg["code"] = WebApiError::MqttHostnameLength; retMsg["param"]["max"] = MQTT_MAX_HOSTNAME_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -181,48 +152,42 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) retMsg["message"] = "Username must not be longer than " STR(MQTT_MAX_USERNAME_STRLEN) " characters!"; retMsg["code"] = WebApiError::MqttUsernameLength; retMsg["param"]["max"] = MQTT_MAX_USERNAME_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["mqtt_password"].as().length() > MQTT_MAX_PASSWORD_STRLEN) { retMsg["message"] = "Password must not be longer than " STR(MQTT_MAX_PASSWORD_STRLEN) " characters!"; retMsg["code"] = WebApiError::MqttPasswordLength; retMsg["param"]["max"] = MQTT_MAX_PASSWORD_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["mqtt_topic"].as().length() > MQTT_MAX_TOPIC_STRLEN) { retMsg["message"] = "Topic must not be longer than " STR(MQTT_MAX_TOPIC_STRLEN) " characters!"; retMsg["code"] = WebApiError::MqttTopicLength; retMsg["param"]["max"] = MQTT_MAX_TOPIC_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["mqtt_topic"].as().indexOf(' ') != -1) { retMsg["message"] = "Topic must not contain space characters!"; retMsg["code"] = WebApiError::MqttTopicCharacter; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (!root["mqtt_topic"].as().endsWith("/")) { retMsg["message"] = "Topic must end with a slash (/)!"; retMsg["code"] = WebApiError::MqttTopicTrailingSlash; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["mqtt_port"].as() == 0 || root["mqtt_port"].as() > 65535) { retMsg["message"] = "Port must be a number between 1 and 65535!"; retMsg["code"] = WebApiError::MqttPort; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -232,8 +197,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) retMsg["message"] = "Certificates must not be longer than " STR(MQTT_MAX_CERT_STRLEN) " characters!"; retMsg["code"] = WebApiError::MqttCertificateLength; retMsg["param"]["max"] = MQTT_MAX_CERT_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -241,16 +205,14 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) retMsg["message"] = "LWT topic must not be longer than " STR(MQTT_MAX_TOPIC_STRLEN) " characters!"; retMsg["code"] = WebApiError::MqttLwtTopicLength; retMsg["param"]["max"] = MQTT_MAX_TOPIC_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["mqtt_lwt_topic"].as().indexOf(' ') != -1) { retMsg["message"] = "LWT topic must not contain space characters!"; retMsg["code"] = WebApiError::MqttLwtTopicCharacter; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -258,8 +220,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) retMsg["message"] = "LWT online value must not be longer than " STR(MQTT_MAX_LWTVALUE_STRLEN) " characters!"; retMsg["code"] = WebApiError::MqttLwtOnlineLength; retMsg["param"]["max"] = MQTT_MAX_LWTVALUE_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -267,8 +228,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) retMsg["message"] = "LWT offline value must not be longer than " STR(MQTT_MAX_LWTVALUE_STRLEN) " characters!"; retMsg["code"] = WebApiError::MqttLwtOfflineLength; retMsg["param"]["max"] = MQTT_MAX_LWTVALUE_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -276,8 +236,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) retMsg["message"] = "LWT QoS must not be greater than " STR(2) "!"; retMsg["code"] = WebApiError::MqttLwtQos; retMsg["param"]["max"] = 2; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -286,8 +245,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) retMsg["code"] = WebApiError::MqttPublishInterval; retMsg["param"]["min"] = 5; retMsg["param"]["max"] = 65535; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -296,16 +254,14 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) retMsg["message"] = "Hass topic must not be longer than " STR(MQTT_MAX_TOPIC_STRLEN) " characters!"; retMsg["code"] = WebApiError::MqttHassTopicLength; retMsg["param"]["max"] = MQTT_MAX_TOPIC_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["mqtt_hass_topic"].as().indexOf(' ') != -1) { retMsg["message"] = "Hass topic must not contain space characters!"; retMsg["code"] = WebApiError::MqttHassTopicCharacter; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } } @@ -339,8 +295,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) WebApi.writeConfig(retMsg); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); MqttSettings.performReconnect(); MqttHandleHass.forceUpdate(); diff --git a/src/WebApi_network.cpp b/src/WebApi_network.cpp index 12f637adc..7fec44b2a 100644 --- a/src/WebApi_network.cpp +++ b/src/WebApi_network.cpp @@ -46,8 +46,7 @@ void WebApiNetworkClass::onNetworkStatus(AsyncWebServerRequest* request) root["ap_mac"] = WiFi.softAPmacAddress(); root["ap_stationnum"] = WiFi.softAPgetStationNum(); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiNetworkClass::onNetworkAdminGet(AsyncWebServerRequest* request) @@ -72,8 +71,7 @@ void WebApiNetworkClass::onNetworkAdminGet(AsyncWebServerRequest* request) root["aptimeout"] = config.WiFi.ApTimeout; root["mdnsenabled"] = config.Mdns.Enabled; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request) @@ -83,37 +81,12 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - const String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } - DynamicJsonDocument root(1024); - const DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); - return; - } + auto& retMsg = response->getRoot(); if (!(root.containsKey("ssid") && root.containsKey("password") @@ -127,8 +100,7 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request) && root.containsKey("aptimeout"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -136,68 +108,59 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request) if (!ipaddress.fromString(root["ipaddress"].as())) { retMsg["message"] = "IP address is invalid!"; retMsg["code"] = WebApiError::NetworkIpInvalid; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } IPAddress netmask; if (!netmask.fromString(root["netmask"].as())) { retMsg["message"] = "Netmask is invalid!"; retMsg["code"] = WebApiError::NetworkNetmaskInvalid; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } IPAddress gateway; if (!gateway.fromString(root["gateway"].as())) { retMsg["message"] = "Gateway is invalid!"; retMsg["code"] = WebApiError::NetworkGatewayInvalid; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } IPAddress dns1; if (!dns1.fromString(root["dns1"].as())) { retMsg["message"] = "DNS Server IP 1 is invalid!"; retMsg["code"] = WebApiError::NetworkDns1Invalid; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } IPAddress dns2; if (!dns2.fromString(root["dns2"].as())) { retMsg["message"] = "DNS Server IP 2 is invalid!"; retMsg["code"] = WebApiError::NetworkDns2Invalid; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["hostname"].as().length() == 0 || root["hostname"].as().length() > WIFI_MAX_HOSTNAME_STRLEN) { retMsg["message"] = "Hostname must between 1 and " STR(WIFI_MAX_HOSTNAME_STRLEN) " characters long!"; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (NetworkSettings.NetworkMode() == network_mode::WiFi) { if (root["ssid"].as().length() == 0 || root["ssid"].as().length() > WIFI_MAX_SSID_STRLEN) { retMsg["message"] = "SSID must between 1 and " STR(WIFI_MAX_SSID_STRLEN) " characters long!"; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } } if (root["password"].as().length() > WIFI_MAX_PASSWORD_STRLEN - 1) { retMsg["message"] = "Password must not be longer than " STR(WIFI_MAX_PASSWORD_STRLEN) " characters long!"; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } if (root["aptimeout"].as() > 99999) { retMsg["message"] = "ApTimeout must be a number between 0 and 99999!"; retMsg["code"] = WebApiError::NetworkApTimeoutInvalid; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -235,8 +198,7 @@ void WebApiNetworkClass::onNetworkAdminPost(AsyncWebServerRequest* request) WebApi.writeConfig(retMsg); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); NetworkSettings.enableAdminMode(); NetworkSettings.applyConfig(); diff --git a/src/WebApi_ntp.cpp b/src/WebApi_ntp.cpp index 02bbfb105..d50e0f02f 100644 --- a/src/WebApi_ntp.cpp +++ b/src/WebApi_ntp.cpp @@ -63,8 +63,7 @@ void WebApiNtpClass::onNtpStatus(AsyncWebServerRequest* request) root["sun_isSunsetAvailable"] = SunPosition.isSunsetAvailable(); root["sun_isDayPeriod"] = SunPosition.isDayPeriod(); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiNtpClass::onNtpAdminGet(AsyncWebServerRequest* request) @@ -84,8 +83,7 @@ void WebApiNtpClass::onNtpAdminGet(AsyncWebServerRequest* request) root["latitude"] = config.Ntp.Latitude; root["sunsettype"] = config.Ntp.SunsetType; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) @@ -95,37 +93,12 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - const String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } - DynamicJsonDocument root(1024); - const DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); - return; - } + auto& retMsg = response->getRoot(); if (!(root.containsKey("ntp_server") && root.containsKey("ntp_timezone") @@ -134,8 +107,7 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) && root.containsKey("sunsettype"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -143,8 +115,7 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) retMsg["message"] = "NTP Server must between 1 and " STR(NTP_MAX_SERVER_STRLEN) " characters long!"; retMsg["code"] = WebApiError::NtpServerLength; retMsg["param"]["max"] = NTP_MAX_SERVER_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -152,8 +123,7 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) retMsg["message"] = "Timezone must between 1 and " STR(NTP_MAX_TIMEZONE_STRLEN) " characters long!"; retMsg["code"] = WebApiError::NtpTimezoneLength; retMsg["param"]["max"] = NTP_MAX_TIMEZONE_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -161,8 +131,7 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) retMsg["message"] = "Timezone description must between 1 and " STR(NTP_MAX_TIMEZONEDESCR_STRLEN) " characters long!"; retMsg["code"] = WebApiError::NtpTimezoneDescriptionLength; retMsg["param"]["max"] = NTP_MAX_TIMEZONEDESCR_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -176,8 +145,7 @@ void WebApiNtpClass::onNtpAdminPost(AsyncWebServerRequest* request) WebApi.writeConfig(retMsg); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); NtpSettings.setServer(); NtpSettings.setTimezone(); @@ -208,8 +176,7 @@ void WebApiNtpClass::onNtpTimeGet(AsyncWebServerRequest* request) root["minute"] = timeinfo.tm_min; root["second"] = timeinfo.tm_sec; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) @@ -219,37 +186,12 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - const String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } - DynamicJsonDocument root(1024); - const DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); - return; - } + auto& retMsg = response->getRoot(); if (!(root.containsKey("year") && root.containsKey("month") @@ -259,8 +201,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) && root.containsKey("second"))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -269,8 +210,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) retMsg["code"] = WebApiError::NtpYearInvalid; retMsg["param"]["min"] = 2022; retMsg["param"]["max"] = 2100; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -279,8 +219,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) retMsg["code"] = WebApiError::NtpMonthInvalid; retMsg["param"]["min"] = 1; retMsg["param"]["max"] = 12; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -289,8 +228,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) retMsg["code"] = WebApiError::NtpDayInvalid; retMsg["param"]["min"] = 1; retMsg["param"]["max"] = 31; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -299,8 +237,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) retMsg["code"] = WebApiError::NtpHourInvalid; retMsg["param"]["min"] = 0; retMsg["param"]["max"] = 23; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -309,8 +246,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) retMsg["code"] = WebApiError::NtpMinuteInvalid; retMsg["param"]["min"] = 0; retMsg["param"]["max"] = 59; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -319,8 +255,7 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) retMsg["code"] = WebApiError::NtpSecondInvalid; retMsg["param"]["min"] = 0; retMsg["param"]["max"] = 59; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -341,6 +276,5 @@ void WebApiNtpClass::onNtpTimePost(AsyncWebServerRequest* request) retMsg["message"] = "Time updated!"; retMsg["code"] = WebApiError::NtpTimeUpdated; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } diff --git a/src/WebApi_power.cpp b/src/WebApi_power.cpp index 08fe9c051..b2b2ce42e 100644 --- a/src/WebApi_power.cpp +++ b/src/WebApi_power.cpp @@ -40,8 +40,7 @@ void WebApiPowerClass::onPowerStatus(AsyncWebServerRequest* request) root[inv->serialString()]["power_set_status"] = limitStatus; } - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) @@ -51,45 +50,19 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - const String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } - DynamicJsonDocument root(1024); - const DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); - return; - } + auto& retMsg = response->getRoot(); if (!(root.containsKey("serial") && (root.containsKey("power") || root.containsKey("restart")))) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -99,8 +72,7 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) if (serial == 0) { retMsg["message"] = "Serial must be a number > 0!"; retMsg["code"] = WebApiError::PowerSerialZero; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -108,8 +80,7 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) if (inv == nullptr) { retMsg["message"] = "Invalid inverter specified!"; retMsg["code"] = WebApiError::PowerInvalidInverter; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -126,6 +97,5 @@ void WebApiPowerClass::onPowerPost(AsyncWebServerRequest* request) retMsg["message"] = "Settings saved!"; retMsg["code"] = WebApiError::GenericSuccess; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } diff --git a/src/WebApi_powerlimiter.cpp b/src/WebApi_powerlimiter.cpp index 81987a231..114ced773 100644 --- a/src/WebApi_powerlimiter.cpp +++ b/src/WebApi_powerlimiter.cpp @@ -27,10 +27,9 @@ void WebApiPowerLimiterClass::init(AsyncWebServer& server, Scheduler& scheduler) void WebApiPowerLimiterClass::onStatus(AsyncWebServerRequest* request) { - auto const& config = Configuration.get(); - - AsyncJsonResponse* response = new AsyncJsonResponse(false, 512); + AsyncJsonResponse* response = new AsyncJsonResponse(); auto& root = response->getRoot(); + auto const& config = Configuration.get(); root["enabled"] = config.PowerLimiter.Enabled; root["verbose_logging"] = config.PowerLimiter.VerboseLogging; @@ -44,6 +43,7 @@ void WebApiPowerLimiterClass::onStatus(AsyncWebServerRequest* request) root["target_power_consumption"] = config.PowerLimiter.TargetPowerConsumption; root["target_power_consumption_hysteresis"] = config.PowerLimiter.TargetPowerConsumptionHysteresis; root["lower_power_limit"] = config.PowerLimiter.LowerPowerLimit; + root["base_load_limit"] = config.PowerLimiter.BaseLoadLimit; root["upper_power_limit"] = config.PowerLimiter.UpperPowerLimit; root["ignore_soc"] = config.PowerLimiter.IgnoreSoc; root["battery_soc_start_threshold"] = config.PowerLimiter.BatterySocStartThreshold; @@ -56,8 +56,7 @@ void WebApiPowerLimiterClass::onStatus(AsyncWebServerRequest* request) root["full_solar_passthrough_start_voltage"] = static_cast(config.PowerLimiter.FullSolarPassThroughStartVoltage * 100 + 0.5) / 100.0; root["full_solar_passthrough_stop_voltage"] = static_cast(config.PowerLimiter.FullSolarPassThroughStopVoltage * 100 + 0.5) / 100.0; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiPowerLimiterClass::onMetaData(AsyncWebServerRequest* request) @@ -71,14 +70,14 @@ void WebApiPowerLimiterClass::onMetaData(AsyncWebServerRequest* request) if (config.Inverter[i].Serial != 0) { ++invAmount; } } - AsyncJsonResponse* response = new AsyncJsonResponse(false, 256 + 256 * invAmount); + AsyncJsonResponse* response = new AsyncJsonResponse(); auto& root = response->getRoot(); root["power_meter_enabled"] = config.PowerMeter.Enabled; root["battery_enabled"] = config.Battery.Enabled; root["charge_controller_enabled"] = config.Vedirect.Enabled; - JsonObject inverters = root.createNestedObject("inverters"); + JsonObject inverters = root["inverters"].to(); for (uint8_t i = 0; i < INV_MAX_COUNT; i++) { if (config.Inverter[i].Serial == 0) { continue; } @@ -86,7 +85,7 @@ void WebApiPowerLimiterClass::onMetaData(AsyncWebServerRequest* request) // rather than the hex represenation as used when handling the inverter // serial elsewhere in the web application, because in this case, the // serial is actually not displayed but only used as a value/index. - JsonObject obj = inverters.createNestedObject(String(config.Inverter[i].Serial)); + JsonObject obj = inverters[String(config.Inverter[i].Serial)].to(); obj["pos"] = i; obj["name"] = String(config.Inverter[i].Name); obj["poll_enable"] = config.Inverter[i].Poll_Enable; @@ -104,8 +103,7 @@ void WebApiPowerLimiterClass::onMetaData(AsyncWebServerRequest* request) } } - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiPowerLimiterClass::onAdminGet(AsyncWebServerRequest* request) @@ -124,34 +122,12 @@ void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - response->setLength(); - request->send(response); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } - String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - response->setLength(); - request->send(response); - return; - } - - DynamicJsonDocument root(1024); - DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - response->setLength(); - request->send(response); - return; - } + auto& retMsg = response->getRoot(); // we were not actually checking for all the keys we (unconditionally) // access below for a long time, and it is technically not needed if users @@ -188,6 +164,7 @@ void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request) config.PowerLimiter.TargetPowerConsumption = root["target_power_consumption"].as(); config.PowerLimiter.TargetPowerConsumptionHysteresis = root["target_power_consumption_hysteresis"].as(); config.PowerLimiter.LowerPowerLimit = root["lower_power_limit"].as(); + config.PowerLimiter.BaseLoadLimit = root["base_load_limit"].as(); config.PowerLimiter.UpperPowerLimit = root["upper_power_limit"].as(); if (config.Battery.Enabled) { diff --git a/src/WebApi_powermeter.cpp b/src/WebApi_powermeter.cpp index 137168baf..8ca492b01 100644 --- a/src/WebApi_powermeter.cpp +++ b/src/WebApi_powermeter.cpp @@ -28,9 +28,24 @@ void WebApiPowerMeterClass::init(AsyncWebServer& server, Scheduler& scheduler) _server->on("/api/powermeter/testhttprequest", HTTP_POST, std::bind(&WebApiPowerMeterClass::onTestHttpRequest, this, _1)); } +void WebApiPowerMeterClass::decodeJsonPhaseConfig(JsonObject const& json, PowerMeterHttpConfig& config) const +{ + config.Enabled = json["enabled"].as(); + strlcpy(config.Url, json["url"].as().c_str(), sizeof(config.Url)); + config.AuthType = json["auth_type"].as(); + strlcpy(config.Username, json["username"].as().c_str(), sizeof(config.Username)); + strlcpy(config.Password, json["password"].as().c_str(), sizeof(config.Password)); + strlcpy(config.HeaderKey, json["header_key"].as().c_str(), sizeof(config.HeaderKey)); + strlcpy(config.HeaderValue, json["header_value"].as().c_str(), sizeof(config.HeaderValue)); + config.Timeout = json["timeout"].as(); + strlcpy(config.JsonPath, json["json_path"].as().c_str(), sizeof(config.JsonPath)); + config.PowerUnit = json["unit"].as(); + config.SignInverted = json["sign_inverted"].as(); +} + void WebApiPowerMeterClass::onStatus(AsyncWebServerRequest* request) { - AsyncJsonResponse* response = new AsyncJsonResponse(false, 2048); + AsyncJsonResponse* response = new AsyncJsonResponse(); auto& root = response->getRoot(); const CONFIG_T& config = Configuration.get(); @@ -45,11 +60,11 @@ void WebApiPowerMeterClass::onStatus(AsyncWebServerRequest* request) root["sdmaddress"] = config.PowerMeter.SdmAddress; root["http_individual_requests"] = config.PowerMeter.HttpIndividualRequests; - JsonArray httpPhases = root.createNestedArray("http_phases"); - + auto httpPhases = root["http_phases"].to(); + for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) { - JsonObject phaseObject = httpPhases.createNestedObject(); - + auto phaseObject = httpPhases.add(); + phaseObject["index"] = i + 1; phaseObject["enabled"] = config.PowerMeter.Http_Phase[i].Enabled; phaseObject["url"] = String(config.PowerMeter.Http_Phase[i].Url); @@ -58,12 +73,13 @@ void WebApiPowerMeterClass::onStatus(AsyncWebServerRequest* request) phaseObject["password"] = String(config.PowerMeter.Http_Phase[i].Password); phaseObject["header_key"] = String(config.PowerMeter.Http_Phase[i].HeaderKey); phaseObject["header_value"] = String(config.PowerMeter.Http_Phase[i].HeaderValue); - phaseObject["json_path"] = String(config.PowerMeter.Http_Phase[i].JsonPath); phaseObject["timeout"] = config.PowerMeter.Http_Phase[i].Timeout; + phaseObject["json_path"] = String(config.PowerMeter.Http_Phase[i].JsonPath); + phaseObject["unit"] = config.PowerMeter.Http_Phase[i].PowerUnit; + phaseObject["sign_inverted"] = config.PowerMeter.Http_Phase[i].SignInverted; } - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiPowerMeterClass::onAdminGet(AsyncWebServerRequest* request) @@ -82,34 +98,12 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - response->setLength(); - request->send(response); - return; - } - - String json = request->getParam("data", true)->value(); - - if (json.length() > 4096) { - retMsg["message"] = "Data too large!"; - response->setLength(); - request->send(response); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } - DynamicJsonDocument root(4096); - DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - response->setLength(); - request->send(response); - return; - } + auto& retMsg = response->getRoot(); if (!(root.containsKey("enabled") && root.containsKey("source"))) { retMsg["message"] = "Values are missing!"; @@ -137,7 +131,7 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request) return; } - if ((phase["auth_type"].as() != Auth::none) + if ((phase["auth_type"].as() != PowerMeterHttpConfig::Auth::None) && ( phase["username"].as().length() == 0 || phase["password"].as().length() == 0)) { retMsg["message"] = "Username or password must not be empty!"; response->setLength(); @@ -178,23 +172,14 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request) JsonArray http_phases = root["http_phases"]; for (uint8_t i = 0; i < http_phases.size(); i++) { - JsonObject phase = http_phases[i].as(); - - config.PowerMeter.Http_Phase[i].Enabled = (i == 0 ? true : phase["enabled"].as()); - strlcpy(config.PowerMeter.Http_Phase[i].Url, phase["url"].as().c_str(), sizeof(config.PowerMeter.Http_Phase[i].Url)); - config.PowerMeter.Http_Phase[i].AuthType = phase["auth_type"].as(); - strlcpy(config.PowerMeter.Http_Phase[i].Username, phase["username"].as().c_str(), sizeof(config.PowerMeter.Http_Phase[i].Username)); - strlcpy(config.PowerMeter.Http_Phase[i].Password, phase["password"].as().c_str(), sizeof(config.PowerMeter.Http_Phase[i].Password)); - strlcpy(config.PowerMeter.Http_Phase[i].HeaderKey, phase["header_key"].as().c_str(), sizeof(config.PowerMeter.Http_Phase[i].HeaderKey)); - strlcpy(config.PowerMeter.Http_Phase[i].HeaderValue, phase["header_value"].as().c_str(), sizeof(config.PowerMeter.Http_Phase[i].HeaderValue)); - config.PowerMeter.Http_Phase[i].Timeout = phase["timeout"].as(); - strlcpy(config.PowerMeter.Http_Phase[i].JsonPath, phase["json_path"].as().c_str(), sizeof(config.PowerMeter.Http_Phase[i].JsonPath)); + decodeJsonPhaseConfig(http_phases[i].as(), config.PowerMeter.Http_Phase[i]); } + config.PowerMeter.Http_Phase[0].Enabled = true; WebApi.writeConfig(retMsg); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); + // reboot requiered as per https://github.com/helgeerbe/OpenDTU-OnBattery/issues/565#issuecomment-1872552559 yield(); @@ -210,34 +195,12 @@ void WebApiPowerMeterClass::onTestHttpRequest(AsyncWebServerRequest* request) } AsyncJsonResponse* asyncJsonResponse = new AsyncJsonResponse(); - auto& retMsg = asyncJsonResponse->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - asyncJsonResponse->setLength(); - request->send(asyncJsonResponse); - return; - } - - String json = request->getParam("data", true)->value(); - - if (json.length() > 2048) { - retMsg["message"] = "Data too large!"; - asyncJsonResponse->setLength(); - request->send(asyncJsonResponse); + JsonDocument root; + if (!WebApi.parseRequestData(request, asyncJsonResponse, root)) { return; } - DynamicJsonDocument root(2048); - DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - asyncJsonResponse->setLength(); - request->send(asyncJsonResponse); - return; - } + auto& retMsg = asyncJsonResponse->getRoot(); if (!root.containsKey("url") || !root.containsKey("auth_type") || !root.containsKey("username") || !root.containsKey("password") || !root.containsKey("header_key") || !root.containsKey("header_value") @@ -252,11 +215,10 @@ void WebApiPowerMeterClass::onTestHttpRequest(AsyncWebServerRequest* request) char response[256]; int phase = 0;//"absuing" index 0 of the float power[3] in HttpPowerMeter to store the result - if (HttpPowerMeter.queryPhase(phase, root[F("url")].as().c_str(), - root[F("auth_type")].as(), root[F("username")].as().c_str(), root[F("password")].as().c_str(), - root[F("header_key")].as().c_str(), root[F("header_value")].as().c_str(), root[F("timeout")].as(), - root[F("json_path")].as().c_str())) { - retMsg[F("type")] = F("success"); + PowerMeterHttpConfig phaseConfig; + decodeJsonPhaseConfig(root.as(), phaseConfig); + if (HttpPowerMeter.queryPhase(phase, phaseConfig)) { + retMsg["type"] = "success"; snprintf_P(response, sizeof(response), "Success! Power: %5.2fW", HttpPowerMeter.getPower(phase + 1)); } else { snprintf_P(response, sizeof(response), "%s", HttpPowerMeter.httpPowerMeterError); diff --git a/src/WebApi_security.cpp b/src/WebApi_security.cpp index b95ebb299..eb0f27d20 100644 --- a/src/WebApi_security.cpp +++ b/src/WebApi_security.cpp @@ -31,8 +31,7 @@ void WebApiSecurityClass::onSecurityGet(AsyncWebServerRequest* request) root["password"] = config.Security.Password; root["allow_readonly"] = config.Security.AllowReadonly; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request) @@ -42,44 +41,18 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - const String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } - DynamicJsonDocument root(1024); - const DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); - return; - } + auto& retMsg = response->getRoot(); if (!root.containsKey("password") && root.containsKey("allow_readonly")) { retMsg["message"] = "Values are missing!"; retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -87,8 +60,7 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request) retMsg["message"] = "Password must between 8 and " STR(WIFI_MAX_PASSWORD_STRLEN) " characters long!"; retMsg["code"] = WebApiError::SecurityPasswordLength; retMsg["param"]["max"] = WIFI_MAX_PASSWORD_STRLEN; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); return; } @@ -98,8 +70,7 @@ void WebApiSecurityClass::onSecurityPost(AsyncWebServerRequest* request) WebApi.writeConfig(retMsg); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } void WebApiSecurityClass::onAuthenticateGet(AsyncWebServerRequest* request) @@ -114,6 +85,5 @@ void WebApiSecurityClass::onAuthenticateGet(AsyncWebServerRequest* request) retMsg["message"] = "Authentication successful!"; retMsg["code"] = WebApiError::SecurityAuthSuccess; - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } diff --git a/src/WebApi_sysstatus.cpp b/src/WebApi_sysstatus.cpp index b8c366b35..62495063b 100644 --- a/src/WebApi_sysstatus.cpp +++ b/src/WebApi_sysstatus.cpp @@ -81,6 +81,5 @@ void WebApiSysstatusClass::onSystemStatus(AsyncWebServerRequest* request) root["cmt_configured"] = PinMapping.isValidCmt2300Config(); root["cmt_connected"] = Hoymiles.getRadioCmt()->isConnected(); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } diff --git a/src/WebApi_vedirect.cpp b/src/WebApi_vedirect.cpp index 4e1e352b2..2499ebed3 100644 --- a/src/WebApi_vedirect.cpp +++ b/src/WebApi_vedirect.cpp @@ -66,37 +66,12 @@ void WebApiVedirectClass::onVedirectAdminPost(AsyncWebServerRequest* request) } AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& retMsg = response->getRoot(); - retMsg["type"] = "warning"; - - if (!request->hasParam("data", true)) { - retMsg["message"] = "No values found!"; - retMsg["code"] = WebApiError::GenericNoValueFound; - response->setLength(); - request->send(response); - return; - } - - String json = request->getParam("data", true)->value(); - - if (json.length() > 1024) { - retMsg["message"] = "Data too large!"; - retMsg["code"] = WebApiError::GenericDataTooLarge; - response->setLength(); - request->send(response); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { return; } - DynamicJsonDocument root(1024); - DeserializationError error = deserializeJson(root, json); - - if (error) { - retMsg["message"] = "Failed to parse data!"; - retMsg["code"] = WebApiError::GenericParseError; - response->setLength(); - request->send(response); - return; - } + auto& retMsg = response->getRoot(); if (!root.containsKey("vedirect_enabled") || !root.containsKey("verbose_logging") || @@ -115,8 +90,8 @@ void WebApiVedirectClass::onVedirectAdminPost(AsyncWebServerRequest* request) WebApi.writeConfig(retMsg); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); + VictronMppt.updateSettings(); diff --git a/src/WebApi_ws_Huawei.cpp b/src/WebApi_ws_Huawei.cpp index c674e05d6..e8e23615a 100644 --- a/src/WebApi_ws_Huawei.cpp +++ b/src/WebApi_ws_Huawei.cpp @@ -59,22 +59,15 @@ void WebApiWsHuaweiLiveClass::sendDataTaskCb() try { std::lock_guard lock(_mutex); - DynamicJsonDocument root(1024); - if (Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { - JsonVariant var = root; - generateJsonResponse(var); + JsonDocument root; + JsonVariant var = root; - if (Utils::checkJsonOverflow(root, __FUNCTION__, __LINE__)) { return; } + generateCommonJsonResponse(var); + if (Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { String buffer; serializeJson(root, buffer); - if (Configuration.get().Security.AllowReadonly) { - _ws.setAuthentication("", ""); - } else { - _ws.setAuthentication(AUTH_USERNAME, Configuration.get().Security.Password); - } - _ws.textAll(buffer); } } catch (std::bad_alloc& bad_alloc) { @@ -84,7 +77,7 @@ void WebApiWsHuaweiLiveClass::sendDataTaskCb() } } -void WebApiWsHuaweiLiveClass::generateJsonResponse(JsonVariant& root) +void WebApiWsHuaweiLiveClass::generateCommonJsonResponse(JsonVariant& root) { const RectifierParameters_t * rp = HuaweiCan.get(); @@ -134,13 +127,13 @@ void WebApiWsHuaweiLiveClass::onLivedataStatus(AsyncWebServerRequest* request) } try { std::lock_guard lock(_mutex); - AsyncJsonResponse* response = new AsyncJsonResponse(false, 1024U); + AsyncJsonResponse* response = new AsyncJsonResponse(); auto& root = response->getRoot(); - generateJsonResponse(root); + generateCommonJsonResponse(root); + + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); - response->setLength(); - request->send(response); } catch (std::bad_alloc& bad_alloc) { MessageOutput.printf("Calling /api/huaweilivedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what()); WebApi.sendTooManyRequests(request); diff --git a/src/WebApi_ws_battery.cpp b/src/WebApi_ws_battery.cpp index 39aaf7289..466540228 100644 --- a/src/WebApi_ws_battery.cpp +++ b/src/WebApi_ws_battery.cpp @@ -62,12 +62,12 @@ void WebApiWsBatteryLiveClass::sendDataTaskCb() try { std::lock_guard lock(_mutex); - DynamicJsonDocument root(_responseSize); - if (Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { - JsonVariant var = root; - generateJsonResponse(var); + JsonDocument root; + JsonVariant var = root; + + generateCommonJsonResponse(var); - if (Utils::checkJsonOverflow(root, __FUNCTION__, __LINE__)) { return; } + if (Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { // battery provider does not generate a card, e.g., MQTT provider if (root.isNull()) { return; } @@ -90,7 +90,7 @@ void WebApiWsBatteryLiveClass::sendDataTaskCb() } } -void WebApiWsBatteryLiveClass::generateJsonResponse(JsonVariant& root) +void WebApiWsBatteryLiveClass::generateCommonJsonResponse(JsonVariant& root) { Battery.getStats()->getLiveViewData(root); } @@ -111,12 +111,11 @@ void WebApiWsBatteryLiveClass::onLivedataStatus(AsyncWebServerRequest* request) } try { std::lock_guard lock(_mutex); - AsyncJsonResponse* response = new AsyncJsonResponse(false, _responseSize); + AsyncJsonResponse* response = new AsyncJsonResponse(); auto& root = response->getRoot(); - generateJsonResponse(root); + generateCommonJsonResponse(root); - response->setLength(); - request->send(response); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } catch (std::bad_alloc& bad_alloc) { MessageOutput.printf("Calling /api/batterylivedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what()); WebApi.sendTooManyRequests(request); diff --git a/src/WebApi_ws_live.cpp b/src/WebApi_ws_live.cpp index 7f42bf443..ab54d479f 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -61,11 +61,11 @@ void WebApiWsLiveClass::generateOnBatteryJsonResponse(JsonVariant& root, bool al auto victronAge = VictronMppt.getDataAgeMillis(); if (all || (victronAge > 0 && (millis() - _lastPublishVictron) > victronAge)) { - JsonObject vedirectObj = root.createNestedObject("vedirect"); + auto vedirectObj = root["vedirect"].to(); vedirectObj["enabled"] = config.Vedirect.Enabled; if (config.Vedirect.Enabled) { - JsonObject totalVeObj = vedirectObj.createNestedObject("total"); + auto totalVeObj = vedirectObj["total"].to(); addTotalField(totalVeObj, "Power", VictronMppt.getPanelPowerWatts(), "W", 1); addTotalField(totalVeObj, "YieldDay", VictronMppt.getYieldDay() * 1000, "Wh", 0); addTotalField(totalVeObj, "YieldTotal", VictronMppt.getYieldTotal(), "kWh", 2); @@ -75,12 +75,12 @@ void WebApiWsLiveClass::generateOnBatteryJsonResponse(JsonVariant& root, bool al } if (all || (HuaweiCan.getLastUpdate() - _lastPublishHuawei) < halfOfAllMillis ) { - JsonObject huaweiObj = root.createNestedObject("huawei"); + auto huaweiObj = root["huawei"].to(); huaweiObj["enabled"] = config.Huawei.Enabled; if (config.Huawei.Enabled) { const RectifierParameters_t * rp = HuaweiCan.get(); - addTotalField(huaweiObj, "Power", rp->output_power, "W", 2); + addTotalField(huaweiObj, "Power", rp->input_power, "W", 2); } if (!all) { _lastPublishHuawei = millis(); } @@ -88,7 +88,7 @@ void WebApiWsLiveClass::generateOnBatteryJsonResponse(JsonVariant& root, bool al auto spStats = Battery.getStats(); if (all || spStats->updateAvailable(_lastPublishBattery)) { - JsonObject batteryObj = root.createNestedObject("battery"); + auto batteryObj = root["battery"].to(); batteryObj["enabled"] = config.Battery.Enabled; if (config.Battery.Enabled) { @@ -99,7 +99,7 @@ void WebApiWsLiveClass::generateOnBatteryJsonResponse(JsonVariant& root, bool al } if (all || (PowerMeter.getLastPowerMeterUpdate() - _lastPublishPowerMeter) < halfOfAllMillis) { - JsonObject powerMeterObj = root.createNestedObject("power_meter"); + auto powerMeterObj = root["power_meter"].to(); powerMeterObj["enabled"] = config.PowerMeter.Enabled; if (config.PowerMeter.Enabled) { @@ -112,9 +112,7 @@ void WebApiWsLiveClass::generateOnBatteryJsonResponse(JsonVariant& root, bool al void WebApiWsLiveClass::sendOnBatteryStats() { - DynamicJsonDocument root(1024); - if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { return; } - + JsonDocument root; JsonVariant var = root; bool all = (millis() - _lastPublishOnBatteryFull) > 10 * 1000; @@ -123,12 +121,12 @@ void WebApiWsLiveClass::sendOnBatteryStats() if (root.isNull()) { return; } - if (Utils::checkJsonOverflow(root, __FUNCTION__, __LINE__)) { return; } - - String buffer; - serializeJson(root, buffer); + if (Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { + String buffer; + serializeJson(root, buffer); - _ws.textAll(buffer); + _ws.textAll(buffer);; + } } void WebApiWsLiveClass::sendDataTaskCb() @@ -156,19 +154,20 @@ void WebApiWsLiveClass::sendDataTaskCb() try { std::lock_guard lock(_mutex); - DynamicJsonDocument root(4096); - if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { - continue; - } + JsonDocument root; JsonVariant var = root; - auto invArray = var.createNestedArray("inverters"); - auto invObject = invArray.createNestedObject(); + auto invArray = var["inverters"].to(); + auto invObject = invArray.add(); generateCommonJsonResponse(var); generateInverterCommonJsonResponse(invObject, inv); generateInverterChannelJsonResponse(invObject, inv); + if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { + continue; + } + String buffer; serializeJson(root, buffer); @@ -184,12 +183,12 @@ void WebApiWsLiveClass::sendDataTaskCb() void WebApiWsLiveClass::generateCommonJsonResponse(JsonVariant& root) { - JsonObject totalObj = root.createNestedObject("total"); + auto totalObj = root["total"].to(); addTotalField(totalObj, "Power", Datastore.getTotalAcPowerEnabled(), "W", Datastore.getTotalAcPowerDigits()); addTotalField(totalObj, "YieldDay", Datastore.getTotalAcYieldDayEnabled(), "Wh", Datastore.getTotalAcYieldDayDigits()); addTotalField(totalObj, "YieldTotal", Datastore.getTotalAcYieldTotalEnabled(), "kWh", Datastore.getTotalAcYieldTotalDigits()); - JsonObject hintObj = root.createNestedObject("hints"); + JsonObject hintObj = root["hints"].to(); 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())); @@ -227,7 +226,7 @@ void WebApiWsLiveClass::generateInverterChannelJsonResponse(JsonObject& root, st // Loop all channels for (auto& t : inv->Statistics()->getChannelTypes()) { - JsonObject chanTypeObj = root.createNestedObject(inv->Statistics()->getChannelTypeName(t)); + auto chanTypeObj = root[inv->Statistics()->getChannelTypeName(t)].to(); for (auto& c : inv->Statistics()->getChannelsByType(t)) { if (t == TYPE_DC) { chanTypeObj[String(static_cast(c))]["name"]["u"] = inv_cfg->channel[c].Name; @@ -304,21 +303,15 @@ void WebApiWsLiveClass::onLivedataStatus(AsyncWebServerRequest* request) try { std::lock_guard lock(_mutex); - AsyncJsonResponse* response = new AsyncJsonResponse(false, 4096); + AsyncJsonResponse* response = new AsyncJsonResponse(); auto& root = response->getRoot(); - - 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); - } + auto invArray = root["inverters"].to(); + auto serial = WebApi.parseSerialFromRequest(request); if (serial > 0) { auto inv = Hoymiles.getInverterBySerial(serial); if (inv != nullptr) { - JsonObject invObject = invArray.createNestedObject(); + JsonObject invObject = invArray.add(); generateInverterCommonJsonResponse(invObject, inv); generateInverterChannelJsonResponse(invObject, inv); } @@ -330,7 +323,7 @@ void WebApiWsLiveClass::onLivedataStatus(AsyncWebServerRequest* request) continue; } - JsonObject invObject = invArray.createNestedObject(); + JsonObject invObject = invArray.add(); generateInverterCommonJsonResponse(invObject, inv); } } @@ -339,9 +332,10 @@ void WebApiWsLiveClass::onLivedataStatus(AsyncWebServerRequest* request) generateOnBatteryJsonResponse(root, true); - response->setLength(); - request->send(response); + generateOnBatteryJsonResponse(root, true); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); + } 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()); WebApi.sendTooManyRequests(request); diff --git a/src/WebApi_ws_vedirect_live.cpp b/src/WebApi_ws_vedirect_live.cpp index 967372ccc..9a92b363d 100644 --- a/src/WebApi_ws_vedirect_live.cpp +++ b/src/WebApi_ws_vedirect_live.cpp @@ -86,25 +86,17 @@ void WebApiWsVedirectLiveClass::sendDataTaskCb() if (fullUpdate || updateAvailable) { try { std::lock_guard lock(_mutex); - DynamicJsonDocument root(responseSize()); - if (Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { - JsonVariant var = root; - generateJsonResponse(var, fullUpdate); + JsonDocument root; + JsonVariant var = root; - if (Utils::checkJsonOverflow(root, __FUNCTION__, __LINE__)) { return; } + generateCommonJsonResponse(var, fullUpdate); + if (Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { String buffer; serializeJson(root, buffer); - if (Configuration.get().Security.AllowReadonly) { - _ws.setAuthentication("", ""); - } else { - _ws.setAuthentication(AUTH_USERNAME, Configuration.get().Security.Password); - } - - _ws.textAll(buffer); + _ws.textAll(buffer);; } - } catch (std::bad_alloc& bad_alloc) { MessageOutput.printf("Calling /api/vedirectlivedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what()); } catch (const std::exception& exc) { @@ -117,30 +109,27 @@ void WebApiWsVedirectLiveClass::sendDataTaskCb() } } -void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root, bool fullUpdate) +void WebApiWsVedirectLiveClass::generateCommonJsonResponse(JsonVariant& root, bool fullUpdate) { - const JsonObject &array = root["vedirect"].createNestedObject("instances"); + auto array = root["vedirect"]["instances"].to(); root["vedirect"]["full_update"] = fullUpdate; for (size_t idx = 0; idx < VictronMppt.controllerAmount(); ++idx) { - std::optional spOptMpptData = VictronMppt.getData(idx); - if (!spOptMpptData.has_value()) { - continue; - } + auto optMpptData = VictronMppt.getData(idx); + if (!optMpptData.has_value()) { continue; } if (!fullUpdate && !hasUpdate(idx)) { continue; } - VeDirectMpptController::spData_t &spMpptData = spOptMpptData.value(); - - String serial(spMpptData->SER); + String serial(optMpptData->serialNr_SER); if (serial.isEmpty()) { continue; } // serial required as index - const JsonObject &nested = array.createNestedObject(serial); + JsonObject nested = array[serial].to(); nested["data_age_ms"] = VictronMppt.getDataAgeMillis(idx); - populateJson(nested, spMpptData); - _lastPublish = millis(); + populateJson(nested, *optMpptData); } + _lastPublish = millis(); + // power limiter state root["dpl"]["PLSTATE"] = -1; if (Configuration.get().PowerLimiter.Enabled) @@ -148,58 +137,70 @@ void WebApiWsVedirectLiveClass::generateJsonResponse(JsonVariant& root, bool ful root["dpl"]["PLLIMIT"] = PowerLimiter.getLastRequestedPowerLimit(); } -void WebApiWsVedirectLiveClass::populateJson(const JsonObject &root, const VeDirectMpptController::spData_t &spMpptData) { - // device info - root["device"]["PID"] = spMpptData->getPidAsString(); - root["device"]["SER"] = spMpptData->SER; - root["device"]["FW"] = spMpptData->FW; - root["device"]["LOAD"] = spMpptData->LOAD ? "ON" : "OFF"; - root["device"]["CS"] = spMpptData->getCsAsString(); - root["device"]["ERR"] = spMpptData->getErrAsString(); - root["device"]["OR"] = spMpptData->getOrAsString(); - root["device"]["MPPT"] = spMpptData->getMpptAsString(); - root["device"]["HSDS"]["v"] = spMpptData->HSDS; - root["device"]["HSDS"]["u"] = "d"; - - // battery info - root["output"]["P"]["v"] = spMpptData->P; - root["output"]["P"]["u"] = "W"; - root["output"]["P"]["d"] = 0; - root["output"]["V"]["v"] = spMpptData->V; - root["output"]["V"]["u"] = "V"; - root["output"]["V"]["d"] = 2; - root["output"]["I"]["v"] = spMpptData->I; - root["output"]["I"]["u"] = "A"; - root["output"]["I"]["d"] = 2; - root["output"]["E"]["v"] = spMpptData->E; - root["output"]["E"]["u"] = "%"; - root["output"]["E"]["d"] = 1; - - // panel info - root["input"]["PPV"]["v"] = spMpptData->PPV; - root["input"]["PPV"]["u"] = "W"; - root["input"]["PPV"]["d"] = 0; - root["input"]["VPV"]["v"] = spMpptData->VPV; - root["input"]["VPV"]["u"] = "V"; - root["input"]["VPV"]["d"] = 2; - root["input"]["IPV"]["v"] = spMpptData->IPV; - root["input"]["IPV"]["u"] = "A"; - root["input"]["IPV"]["d"] = 2; - root["input"]["YieldToday"]["v"] = spMpptData->H20; - root["input"]["YieldToday"]["u"] = "kWh"; - root["input"]["YieldToday"]["d"] = 3; - root["input"]["YieldYesterday"]["v"] = spMpptData->H22; - root["input"]["YieldYesterday"]["u"] = "kWh"; - root["input"]["YieldYesterday"]["d"] = 3; - root["input"]["YieldTotal"]["v"] = spMpptData->H19; - root["input"]["YieldTotal"]["u"] = "kWh"; - root["input"]["YieldTotal"]["d"] = 3; - root["input"]["MaximumPowerToday"]["v"] = spMpptData->H21; - root["input"]["MaximumPowerToday"]["u"] = "W"; - root["input"]["MaximumPowerToday"]["d"] = 0; - root["input"]["MaximumPowerYesterday"]["v"] = spMpptData->H23; - root["input"]["MaximumPowerYesterday"]["u"] = "W"; - root["input"]["MaximumPowerYesterday"]["d"] = 0; +void WebApiWsVedirectLiveClass::populateJson(const JsonObject &root, const VeDirectMpptController::data_t &mpptData) { + root["product_id"] = mpptData.getPidAsString(); + root["firmware_version"] = String(mpptData.firmwareNr_FW); + + const JsonObject values = root["values"].to(); + + const JsonObject device = values["device"].to(); + device["LOAD"] = mpptData.loadOutputState_LOAD ? "ON" : "OFF"; + device["CS"] = mpptData.getCsAsString(); + device["MPPT"] = mpptData.getMpptAsString(); + device["OR"] = mpptData.getOrAsString(); + device["ERR"] = mpptData.getErrAsString(); + device["HSDS"]["v"] = mpptData.daySequenceNr_HSDS; + device["HSDS"]["u"] = "d"; + if (mpptData.MpptTemperatureMilliCelsius.first > 0) { + device["MpptTemperature"]["v"] = mpptData.MpptTemperatureMilliCelsius.second / 1000.0; + device["MpptTemperature"]["u"] = "°C"; + device["MpptTemperature"]["d"] = "1"; + } + + const JsonObject output = values["output"].to(); + output["P"]["v"] = mpptData.batteryOutputPower_W; + output["P"]["u"] = "W"; + output["P"]["d"] = 0; + output["V"]["v"] = mpptData.batteryVoltage_V_mV / 1000.0; + output["V"]["u"] = "V"; + output["V"]["d"] = 2; + output["I"]["v"] = mpptData.batteryCurrent_I_mA / 1000.0; + output["I"]["u"] = "A"; + output["I"]["d"] = 2; + output["E"]["v"] = mpptData.mpptEfficiency_Percent; + output["E"]["u"] = "%"; + output["E"]["d"] = 1; + + const JsonObject input = values["input"].to(); + if (mpptData.NetworkTotalDcInputPowerMilliWatts.first > 0) { + input["NetworkPower"]["v"] = mpptData.NetworkTotalDcInputPowerMilliWatts.second / 1000.0; + input["NetworkPower"]["u"] = "W"; + input["NetworkPower"]["d"] = "0"; + } + input["PPV"]["v"] = mpptData.panelPower_PPV_W; + input["PPV"]["u"] = "W"; + input["PPV"]["d"] = 0; + input["VPV"]["v"] = mpptData.panelVoltage_VPV_mV / 1000.0; + input["VPV"]["u"] = "V"; + input["VPV"]["d"] = 2; + input["IPV"]["v"] = mpptData.panelCurrent_mA / 1000.0; + input["IPV"]["u"] = "A"; + input["IPV"]["d"] = 2; + input["YieldToday"]["v"] = mpptData.yieldToday_H20_Wh / 1000.0; + input["YieldToday"]["u"] = "kWh"; + input["YieldToday"]["d"] = 2; + input["YieldYesterday"]["v"] = mpptData.yieldYesterday_H22_Wh / 1000.0; + input["YieldYesterday"]["u"] = "kWh"; + input["YieldYesterday"]["d"] = 2; + input["YieldTotal"]["v"] = mpptData.yieldTotal_H19_Wh / 1000.0; + input["YieldTotal"]["u"] = "kWh"; + input["YieldTotal"]["d"] = 2; + input["MaximumPowerToday"]["v"] = mpptData.maxPowerToday_H21_W; + input["MaximumPowerToday"]["u"] = "W"; + input["MaximumPowerToday"]["d"] = 0; + input["MaximumPowerYesterday"]["v"] = mpptData.maxPowerYesterday_H23_W; + input["MaximumPowerYesterday"]["u"] = "W"; + input["MaximumPowerYesterday"]["d"] = 0; } void WebApiWsVedirectLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len) @@ -224,14 +225,12 @@ void WebApiWsVedirectLiveClass::onLivedataStatus(AsyncWebServerRequest* request) } try { std::lock_guard lock(_mutex); - AsyncJsonResponse* response = new AsyncJsonResponse(false, responseSize()); + AsyncJsonResponse* response = new AsyncJsonResponse(); auto& root = response->getRoot(); - generateJsonResponse(root, true/*fullUpdate*/); - - response->setLength(); - request->send(response); + generateCommonJsonResponse(root, true/*fullUpdate*/); + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } catch (std::bad_alloc& bad_alloc) { MessageOutput.printf("Calling /api/vedirectlivedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what()); WebApi.sendTooManyRequests(request); diff --git a/webapp/.eslintrc.cjs b/webapp/.eslintrc.cjs deleted file mode 100644 index ade85716e..000000000 --- a/webapp/.eslintrc.cjs +++ /dev/null @@ -1,14 +0,0 @@ -/* eslint-env node */ -require('@rushstack/eslint-patch/modern-module-resolution') - -module.exports = { - root: true, - 'extends': [ - 'plugin:vue/vue3-essential', - 'eslint:recommended', - '@vue/eslint-config-typescript' - ], - parserOptions: { - ecmaVersion: 'latest' - } -} diff --git a/webapp/eslint.config.js b/webapp/eslint.config.js new file mode 100644 index 000000000..91657b987 --- /dev/null +++ b/webapp/eslint.config.js @@ -0,0 +1,36 @@ +/* eslint-env node */ +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { FlatCompat } from "@eslint/eslintrc"; +import js from "@eslint/js"; +import pluginVue from 'eslint-plugin-vue' + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, +}); + +export default [ + js.configs.recommended, + ...pluginVue.configs['flat/essential'], + ...compat.extends("@vue/eslint-config-typescript/recommended"), + { + files: [ + "**/*.vue", + "**/*.js", + "**/*.jsx", + "**/*.cjs", + "**/*.mjs", + "**/*.ts", + "**/*.tsx", + "**/*.cts", + "**/*.mts", + ], + languageOptions: { + ecmaVersion: 'latest' + }, + } + ] diff --git a/webapp/package.json b/webapp/package.json index 5088ac7fb..fbba65f1e 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -9,7 +9,7 @@ "preview": "vite preview --port 4173", "build-only": "vite build", "type-check": "vue-tsc --noEmit", - "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore" + "lint": "eslint ." }, "dependencies": { "@popperjs/core": "^2.11.8", @@ -18,32 +18,31 @@ "mitt": "^3.0.1", "sortablejs": "^1.15.2", "spark-md5": "^3.0.2", - "vue": "^3.4.21", - "vue-i18n": "^9.10.2", - "vue-router": "^4.3.0" + "vue": "^3.4.25", + "vue-i18n": "^9.13.1", + "vue-router": "^4.3.2" }, "devDependencies": { - "@intlify/unplugin-vue-i18n": "^3.0.1", - "@rushstack/eslint-patch": "^1.8.0", - "@tsconfig/node18": "^18.2.2", + "@intlify/unplugin-vue-i18n": "^4.0.0", + "@tsconfig/node18": "^18.2.4", "@types/bootstrap": "^5.2.10", - "@types/node": "^20.11.30", + "@types/node": "^20.12.7", "@types/pulltorefreshjs": "^0.1.7", "@types/sortablejs": "^1.15.8", "@types/spark-md5": "^3.0.4", "@vitejs/plugin-vue": "^5.0.4", "@vue/eslint-config-typescript": "^13.0.0", "@vue/tsconfig": "^0.5.1", - "eslint": "^8.57.0", - "eslint-plugin-vue": "^9.23.0", + "eslint": "^9.1.1", + "eslint-plugin-vue": "^9.25.0", "npm-run-all": "^4.1.5", "pulltorefreshjs": "^0.1.22", - "sass": "^1.72.0", - "terser": "^5.29.2", - "typescript": "^5.4.3", - "vite": "^5.2.3", + "sass": "^1.75.0", + "terser": "^5.30.4", + "typescript": "^5.4.5", + "vite": "^5.2.10", "vite-plugin-compression": "^0.5.1", "vite-plugin-css-injected-by-js": "^3.5.0", - "vue-tsc": "^2.0.7" + "vue-tsc": "^2.0.14" } } diff --git a/webapp/public/zones.json b/webapp/public/zones.json index 2d449fe26..ad90ea014 100644 --- a/webapp/public/zones.json +++ b/webapp/public/zones.json @@ -180,7 +180,7 @@ "America/Santiago":"<-04>4<-03>,M9.1.6/24,M4.1.6/24", "America/Santo_Domingo":"AST4", "America/Sao_Paulo":"<-03>3", -"America/Scoresbysund":"<-01>1<+00>,M3.5.0/0,M10.5.0/1", +"America/Scoresbysund":"<-02>2<-01>,M3.5.0/-1,M10.5.0/0", "America/Sitka":"AKST9AKDT,M3.2.0,M11.1.0", "America/St_Barthelemy":"AST4", "America/St_Johns":"NST3:30NDT,M3.2.0,M11.1.0", @@ -200,7 +200,7 @@ "America/Winnipeg":"CST6CDT,M3.2.0,M11.1.0", "America/Yakutat":"AKST9AKDT,M3.2.0,M11.1.0", "America/Yellowknife":"MST7MDT,M3.2.0,M11.1.0", -"Antarctica/Casey":"<+11>-11", +"Antarctica/Casey":"<+08>-8", "Antarctica/Davis":"<+07>-7", "Antarctica/DumontDUrville":"<+10>-10", "Antarctica/Macquarie":"AEST-10AEDT,M10.1.0,M4.1.0/3", @@ -210,10 +210,10 @@ "Antarctica/Rothera":"<-03>3", "Antarctica/Syowa":"<+03>-3", "Antarctica/Troll":"<+00>0<+02>-2,M3.5.0/1,M10.5.0/3", -"Antarctica/Vostok":"<+06>-6", +"Antarctica/Vostok":"<+05>-5", "Arctic/Longyearbyen":"CET-1CEST,M3.5.0,M10.5.0/3", "Asia/Aden":"<+03>-3", -"Asia/Almaty":"<+06>-6", +"Asia/Almaty":"<+05>-5", "Asia/Amman":"<+03>-3", "Asia/Anadyr":"<+12>-12", "Asia/Aqtau":"<+05>-5", diff --git a/webapp/src/components/BasePage.vue b/webapp/src/components/BasePage.vue index 0ec43d36f..3ca9c12b5 100644 --- a/webapp/src/components/BasePage.vue +++ b/webapp/src/components/BasePage.vue @@ -48,15 +48,14 @@ export default defineComponent({ showReload: { type: Boolean, required: false, default: false }, }, mounted() { - var self = this; console.log("init"); PullToRefresh.init({ mainElement: 'body', // above which element? instructionsPullToRefresh: this.$t('base.Pull'), instructionsReleaseToRefresh: this.$t('base.Release'), instructionsRefreshing: this.$t('base.Refreshing'), - onRefresh: function() { - self.$emit('reload'); + onRefresh: () => { + this.$emit('reload'); } }); }, diff --git a/webapp/src/components/BootstrapAlert.vue b/webapp/src/components/BootstrapAlert.vue index df96fb620..a629863db 100644 --- a/webapp/src/components/BootstrapAlert.vue +++ b/webapp/src/components/BootstrapAlert.vue @@ -52,7 +52,7 @@ export default defineComponent({ _countDownTimeout = undefined; }; - var countDown = ref(); + const countDown = ref(); watch(() => props.modelValue, () => { countDown.value = parseCountDown(props.modelValue); }); @@ -116,4 +116,4 @@ export default defineComponent({ }; }, }); - \ No newline at end of file + diff --git a/webapp/src/components/FirmwareInfo.vue b/webapp/src/components/FirmwareInfo.vue index 531187872..af6c95c05 100644 --- a/webapp/src/components/FirmwareInfo.vue +++ b/webapp/src/components/FirmwareInfo.vue @@ -87,10 +87,10 @@ export default defineComponent({ }, computed: { modelAllowVersionInfo: { - get(): any { + get(): boolean { return !!this.allowVersionInfo; }, - set(value: any) { + set(value: boolean) { this.$emit('update:allowVersionInfo', value); }, }, diff --git a/webapp/src/components/InputElement.vue b/webapp/src/components/InputElement.vue index eff8e9f66..f12a11720 100644 --- a/webapp/src/components/InputElement.vue +++ b/webapp/src/components/InputElement.vue @@ -83,10 +83,12 @@ export default defineComponent({ }, computed: { model: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any get(): any { if (this.type === 'checkbox') return !!this.modelValue; return this.modelValue; }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any set(value: any) { this.$emit('update:modelValue', value); }, @@ -112,4 +114,4 @@ export default defineComponent({ } }, }); - \ No newline at end of file + diff --git a/webapp/src/components/InputSerial.vue b/webapp/src/components/InputSerial.vue index 9f5ee343b..3669da622 100644 --- a/webapp/src/components/InputSerial.vue +++ b/webapp/src/components/InputSerial.vue @@ -28,9 +28,11 @@ export default defineComponent({ }, computed: { model: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any get(): any { return this.modelValue; }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any set(value: any) { this.$emit('update:modelValue', value); }, diff --git a/webapp/src/components/NavBar.vue b/webapp/src/components/NavBar.vue index c8cf8a337..6577a4a6d 100644 --- a/webapp/src/components/NavBar.vue +++ b/webapp/src/components/NavBar.vue @@ -168,8 +168,8 @@ export default defineComponent({ }, isEaster() { const easter = this.getEasterSunday(this.now.getFullYear()); - var easterStart = new Date(easter); - var easterEnd = new Date(easter); + const easterStart = new Date(easter); + const easterEnd = new Date(easter); easterStart.setDate(easterStart.getDate() - 2); easterEnd.setDate(easterEnd.getDate() + 1); return this.now >= easterStart && this.now < easterEnd; @@ -192,18 +192,18 @@ export default defineComponent({ this.$refs.navbarCollapse && (this.$refs.navbarCollapse as HTMLElement).classList.remove("show"); }, getEasterSunday(year: number): Date { - var f = Math.floor; - var G = year % 19; - var C = f(year / 100); - var H = (C - f(C / 4) - f((8 * C + 13) / 25) + 19 * G + 15) % 30; - var I = H - f(H / 28) * (1 - f(29 / (H + 1)) * f((21 - G) / 11)); - var J = (year + f(year / 4) + I + 2 - C + f(C / 4)) % 7; - var L = I - J; - var month = 3 + f((L + 40) / 44); - var day = L + 28 - 31 * f(month / 4); + const f = Math.floor; + const G = year % 19; + const C = f(year / 100); + const H = (C - f(C / 4) - f((8 * C + 13) / 25) + 19 * G + 15) % 30; + const I = H - f(H / 28) * (1 - f(29 / (H + 1)) * f((21 - G) / 11)); + const J = (year + f(year / 4) + I + 2 - C + f(C / 4)) % 7; + const L = I - J; + const month = 3 + f((L + 40) / 44); + const day = L + 28 - 31 * f(month / 4); return new Date(year, month - 1, day); } }, }); - \ No newline at end of file + diff --git a/webapp/src/components/PinInfo.vue b/webapp/src/components/PinInfo.vue index 3d4616adb..c1e84b810 100644 --- a/webapp/src/components/PinInfo.vue +++ b/webapp/src/components/PinInfo.vue @@ -84,9 +84,11 @@ export default defineComponent({ let comCur = 999999; if (this.selectedPinAssignment && category in this.selectedPinAssignment) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any comSel = (this.selectedPinAssignment as any)[category][prop]; } if (this.currentPinAssignment && category in this.currentPinAssignment) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any comCur = (this.currentPinAssignment as any)[category][prop]; } diff --git a/webapp/src/components/VedirectView.vue b/webapp/src/components/VedirectView.vue index 91efeaebc..a1ed9bc3c 100644 --- a/webapp/src/components/VedirectView.vue +++ b/webapp/src/components/VedirectView.vue @@ -18,13 +18,13 @@
- {{ item.device.PID }} + {{ item.product_id }}
- {{ $t('vedirecthome.SerialNumber') }} {{ item.device.SER }} + {{ $t('vedirecthome.SerialNumber') }} {{ serial }}
- {{ $t('vedirecthome.FirmwareNumber') }} {{ item.device.FW }} + {{ $t('vedirecthome.FirmwareNumber') }} {{ item.firmware_version }}
{{ $t('vedirecthome.DataAge') }} {{ $t('vedirecthome.Seconds', {'val': Math.floor(item.data_age_ms / 1000)}) }} @@ -55,9 +55,9 @@
-
-
-
{{ $t('vedirecthome.DeviceInfo') }}
+
+
+
{{ $t('vedirecthome.section_' + section) }}
@@ -69,95 +69,21 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ $t('vedirecthome.LoadOutputState') }}{{item.device.LOAD}}
{{ $t('vedirecthome.StateOfOperation') }}{{item.device.CS}}
{{ $t('vedirecthome.TrackerOperationMode') }}{{item.device.MPPT}}
{{ $t('vedirecthome.OffReason') }}{{item.device.OR}}
{{ $t('vedirecthome.ErrorCode') }}{{item.device.ERR}}
{{ $t('vedirecthome.DaySequenceNumber') }}{{item.device.HSDS.v}}{{item.device.HSDS.u}}
-
-
-
-
-
-
-
{{ $t('vedirecthome.Battery') }}
-
-
- - - - - - - - - - - - - - - -
{{ $t('vedirecthome.Property') }}{{ $t('vedirecthome.Value') }}{{ $t('vedirecthome.Unit') }}
{{ $t('vedirecthome.output.' + key) }} - {{ $n(prop.v, 'decimal', { - minimumFractionDigits: prop.d, - maximumFractionDigits: prop.d}) - }} - {{prop.u}}
-
-
-
-
-
-
-
{{ $t('vedirecthome.Panel') }}
-
-
- - - - - - - - - - - + + - + +
{{ $t('vedirecthome.Property') }}{{ $t('vedirecthome.Value') }}{{ $t('vedirecthome.Unit') }}
{{ $t('vedirecthome.input.' + key) }}
{{ $t('vedirecthome.' + section + '.' + key) }} - {{ $n(prop.v, 'decimal', { + + {{prop.u}}{{prop.u}}
@@ -238,7 +164,7 @@ export default defineComponent({ this.socket.onmessage = (event) => { console.log(event); - var root = JSON.parse(event.data); + const root = JSON.parse(event.data); this.dplData = root["dpl"]; if (root["vedirect"]["full_update"] === true) { this.vedirect = root["vedirect"]; @@ -266,7 +192,7 @@ export default defineComponent({ clearTimeout(this.dataAgeTimers[serial]); } - var nextMs = 1000 - (this.vedirect.instances[serial].data_age_ms % 1000); + const nextMs = 1000 - (this.vedirect.instances[serial].data_age_ms % 1000); this.dataAgeTimers[serial] = setTimeout(() => { this.doDataAging(serial); }, nextMs); diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 568476407..c9e363d38 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -41,6 +41,9 @@ "Release": "Loslassen zum Aktualisieren", "Close": "Schließen" }, + "Error": { + "Oops": "Oops!" + }, "localeswitcher": { "Dark": "Dunkel", "Light": "Hell", @@ -153,25 +156,29 @@ "FirmwareNumber": "Firmware Version: ", "DataAge": "letzte Aktualisierung: ", "Seconds": "vor {val} Sekunden", - "DeviceInfo": "Geräteinformation", "Property": "Eigenschaft", "Value": "Wert", "Unit": "Einheit", - "LoadOutputState": "Status Ladeausgang", - "StateOfOperation": "Betriebszustand", - "TrackerOperationMode": "Betriebszustand des Trackers", - "OffReason": "Grund für das Ausschalten", - "ErrorCode": "Fehlerbeschreibung", - "DaySequenceNumber": "Anzahl der Tage (0..364)", - "Battery": "Ausgang (Batterie)", + "section_device": "Geräteinformation", + "device": { + "LOAD": "Status Ladeausgang", + "CS": "Betriebszustand", + "MPPT": "Betriebszustand des Trackers", + "OR": "Grund für das Ausschalten", + "ERR": "Fehlerbeschreibung", + "HSDS": "Anzahl der Tage (0..364)", + "MpptTemperature": "Ladereglertemperatur" + }, + "section_output": "Ausgang (Batterie)", "output": { "P": "Leistung (berechnet)", "V": "Spannung", "I": "Strom", "E": "Effizienz (berechnet)" }, - "Panel": "Eingang (Solarpanele)", + "section_input": "Eingang (Solarpanele)", "input": { + "NetworkPower": "VE.Smart Netzwerk Gesamtleistung", "PPV": "Leistung", "VPV": "Spannung", "IPV": "Strom (berechnet)", @@ -570,7 +577,10 @@ "httpHeaderKeyDescription": "Ein individueller HTTP request header kann hier definiert werden. Das kann z.B. verwendet werden um einen eigenen Authorization header mitzugeben.", "httpHeaderValue": "Optional: HTTP request header - Wert", "httpJsonPath": "JSON Pfad", - "httpJsonPathDescription": "JSON Pfad um den Leistungswert zu finden. Es verwendet die Selektions-Syntax von mobizt/FirebaseJson. Beispiele gibt es unten.", + "httpJsonPathDescription": "JSON Pfad um den Leistungswert zu finden. Es verwendet die Selektions-Syntax von mobizt/FirebaseJson. Beispiele gibt es oben.", + "httpUnit": "Einheit", + "httpSignInverted": "Vorzeichen umkehren", + "httpSignInvertedHint": "Positive Werte werden als Leistungsabnahme aus dem Netz interpretiert. Diese Option muss aktiviert werden, wenn das Vorzeichen des Wertes die gegenteilige Bedeutung hat.", "httpTimeout": "Timeout", "testHttpRequest": "Testen" }, @@ -581,7 +591,7 @@ "ConfigHintRequirement": "Erforderlich", "ConfigHintOptional": "Optional", "ConfigHintsIntro": "Folgende Hinweise zur Konfiguration des Dynamic Power Limiter (DPL) sollen beachtet werden:", - "ConfigHintPowerMeterDisabled": "Zum Betrieb des DPL muss der Power Meter konfiguriert sein und Daten liefern.", + "ConfigHintPowerMeterDisabled": "Der DPL stellt ohne Stromzählerschnittstelle lediglich die konfigurierte Grundlast als Limit am Wechselrichter ein (Ausnahme: (Full) Solar-Passthrough).", "ConfigHintNoInverter": "Vor dem Festlegen von Einstellungen des DPL muss mindestens ein Inverter konfiguriert sein.", "ConfigHintInverterCommunication": "Das Abrufen von Daten und Senden von Kommandos muss für den zu regelnden Wechselrichter aktiviert sein.", "ConfigHintNoChargeController": "Die Solar-Passthrough Funktion kann nur mit aktivierter VE.Direct Schnittstelle genutzt werden.", @@ -602,9 +612,13 @@ "TargetPowerConsumption": "Angestrebter Netzbezug", "TargetPowerConsumptionHint": "Angestrebter erlaubter Stromverbrauch aus dem Netz. Wert darf negativ sein.", "TargetPowerConsumptionHysteresis": "Hysterese", - "TargetPowerConsumptionHysteresisHint": "Neu berechnetes Limit nur dann an den Inverter senden, wenn es vom zuletzt gesendeten Limit um mindestens diesen Betrag abweicht.", - "LowerPowerLimit": "Unteres Leistungslimit", - "UpperPowerLimit": "Oberes Leistungslimit", + "TargetPowerConsumptionHysteresisHint": "Neu berechnetes Limit nur dann an den Inverter senden, wenn es vom zurückgemeldeten Limit um mindestens diesen Betrag abweicht.", + "LowerPowerLimit": "Minmales Leistungslimit", + "LowerPowerLimitHint": "Dieser Wert muss so gewählt werden, dass ein stabiler Betrieb mit diesem Limit möglich ist. Falls der Wechselrichter nur mit einem kleineren Limit betrieben werden könnte, wird er stattdessen in Standby versetzt.", + "BaseLoadLimit": "Grundlast", + "BaseLoadLimitHint": "Relevant beim Betrieb ohne oder beim Ausfall des Stromzählers. Solange es die sonstigen Bedinungen zulassen (insb. Batterieladung) wird dieses Limit am Wechselrichter eingestellt.", + "UpperPowerLimit": "Maximales Leistungslimit", + "UpperPowerLimitHint": "Der Wechselrichter wird stets so eingestellt, dass höchstens diese Ausgangsleistung erreicht wird. Dieser Wert muss so gewählt werden, dass die Strombelastbarkeit der AC-Anschlussleitungen eingehalten wird.", "SocThresholds": "Batterie State of Charge (SoC) Schwellwerte", "IgnoreSoc": "Batterie SoC ignorieren", "StartThreshold": "Batterienutzung Start-Schwellwert", @@ -831,15 +845,25 @@ "ChargerSettings": "AC Ladegerät Einstellungen", "Configuration": "AC Ladegerät Konfiguration", "EnableHuawei": "Huawei R4850G2 an CAN Bus Interface aktiv", + "VerboseLogging": "@:base.VerboseLogging", "CanControllerFrequency": "Frequenz des Quarzes am CAN Controller", "EnableAutoPower": "Automatische Leistungssteuerung", + "EnableBatterySoCLimits": "Ladezustand einer angeschlossenen Batterie berücksichtigen", "Limits": "Limits", + "BatterySoCLimits": "Batterie SoC-Limits", "VoltageLimit": "Ladespannungslimit", "enableVoltageLimit": "Start Spannungslimit", + "stopVoltageLimitHint": "Maximal Spannung des Ladegeräts. Entspricht der geünschten Ladeschlussspannung der Batterie. Verwendet für die Automatische Leistungssteuerung und beim Notfallladen", "enableVoltageLimitHint": "Die automatische Leistungssteuerung wird deaktiviert wenn die Ausgangsspannung über diesem Wert liegt und wenn gleichzeitig die Ausgangsleistung unter die minimale Leistung fällt.\nDie automatische Leistungssteuerung wird re-aktiveiert wenn die Batteriespannung unter diesen Wert fällt.", + "upperPowerLimitHint": "Maximale Ausgangsleistung. Verwendet für die Automatische Leistungssteuerung und beim Notfallladen", "lowerPowerLimit": "Minimale Leistung", "upperPowerLimit": "Maximale Leistung", - "Seconds": "@:base.Seconds" + "StopBatterySoCThreshold": "Laden bei SoC beenden", + "StopBatterySoCThresholdHint": "Zur Verlängerung der Akku-Lebensdauer kann der Ladevorgang bei einem bestimmten SoC gestoppt werden.\nHinweis: Manche LiFePO-Akkus müssen gelegentlich voll geladen werden, um die SoC-Anzeige akkurat zu halten.", + "Seconds": "@:base.Seconds", + "EnableEmergencyCharge": "Notfallladen: Batterie wird mit maximaler Leistung geladen wenn durch das Batterie BMS angefordert", + "targetPowerConsumption": "Angestrebter Netzbezug", + "targetPowerConsumptionHint": "Bei postiven Werten wird die eingestellte Leistung aus dem Stromnetz bezogen. Bei negativen Werten wird das Netzteil vorzeitig abgeschaltet." }, "battery": { "battery": "Batterie", @@ -910,7 +934,7 @@ "chargedEnergy": "Geladene Energie", "dischargedEnergy": "Entladene Energie", "instantaneousPower": "Aktuelle Leistung", - "consumedAmpHours": "Verbrauche Amperestunden", + "consumedAmpHours": "Verbrauchte Amperestunden", "lastFullCharge": "Letztes mal Vollgeladen" } } diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index f7e7da98b..c779dc43c 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -41,6 +41,9 @@ "Release": "Release to refresh", "Close": "Close" }, + "Error": { + "Oops": "Oops!" + }, "localeswitcher": { "Dark": "Dark", "Light": "Light", @@ -153,25 +156,29 @@ "FirmwareNumber": "Firmware Number: ", "DataAge": "Data Age: ", "Seconds": "{val} seconds", - "DeviceInfo": "Device Info", "Property": "Property", "Value": "Value", "Unit": "Unit", - "LoadOutputState": "Load output state", - "StateOfOperation": "State of operation", - "TrackerOperationMode": "Tracker operation mode", - "OffReason": "Off reason", - "ErrorCode": "Error code", - "DaySequenceNumber": "Day sequence number (0..364)", - "Battery": "Output (Battery)", + "section_device": "Device Info", + "device": { + "LOAD": "Load output state", + "CS": "State of operation", + "MPPT": "Tracker operation mode", + "OR": "Off reason", + "ERR": "Error code", + "HSDS": "Day sequence number (0..364)", + "MpptTemperature": "Charge controller temperature" + }, + "section_output": "Output (Battery)", "output": { "P": "Power (calculated)", "V": "Voltage", "I": "Current", "E": "Efficiency (calculated)" }, - "Panel": "Input (Solar Panels)", + "section_input": "Input (Solar Panels)", "input": { + "NetworkPower": "VE.Smart network total power", "PPV": "Power", "VPV": "Voltage", "IPV": "Current (calculated)", @@ -574,8 +581,11 @@ "httpHeaderKey": "Optional: HTTP request header - Key", "httpHeaderKeyDescription": "A custom HTTP request header can be defined. Might be useful if you have to send something like a custom Authorization header.", "httpHeaderValue": "Optional: HTTP request header - Value", - "httpJsonPath": "Json path", - "httpJsonPathDescription": "JSON path to find the power value in the response. This uses the JSON path query syntax from mobizt/FirebaseJson. See below for some examples.", + "httpJsonPath": "JSON path", + "httpJsonPathDescription": "JSON path to find the power value in the response. This uses the JSON path query syntax from mobizt/FirebaseJson. See above for examples.", + "httpUnit": "Unit", + "httpSignInverted": "Change Sign", + "httpSignInvertedHint": "Is is expected that positive values denote power usage from the grid. Check this option if the sign of this value has the opposite meaning.", "httpTimeout": "Timeout", "testHttpRequest": "Run test", "milliSeconds": "ms" @@ -587,7 +597,7 @@ "ConfigHintRequirement": "Required", "ConfigHintOptional": "Optional", "ConfigHintsIntro": "The following notes regarding the Dynamic Power Limiter (DPL) configuration shall be considered:", - "ConfigHintPowerMeterDisabled": "Operating the DPL requires the Power Meter being configured and delivering data.", + "ConfigHintPowerMeterDisabled": "Without a power meter interface, the inverter limit the DPL will configure equals the configured base load (exception: (full) solar-passthrough).", "ConfigHintNoInverter": "At least one inverter must be configured prior to setting up the DPL.", "ConfigHintInverterCommunication": "Polling data from and sending commands to the target inverter must be enabled.", "ConfigHintNoChargeController": "The solar-passthrough feature can only be used if the VE.Direct interface is configured.", @@ -608,9 +618,13 @@ "TargetPowerConsumption": "Target Grid Consumption", "TargetPowerConsumptionHint": "Grid power consumption the limiter tries to achieve. Value may be negative.", "TargetPowerConsumptionHysteresis": "Hysteresis", - "TargetPowerConsumptionHysteresisHint": "Only send a newly calculated power limit to the inverter if the absolute difference to the last sent power limit matches or exceeds this amount.", - "LowerPowerLimit": "Lower Power Limit", - "UpperPowerLimit": "Upper Power Limit", + "TargetPowerConsumptionHysteresisHint": "Only send a newly calculated power limit to the inverter if the absolute difference to the last reported power limit exceeds this amount.", + "LowerPowerLimit": "Minimum Power Limit", + "LowerPowerLimitHint": "This value must be selected so that stable operation is possible at this limit. If the inverter could only be operated with a lower limit, it is put into standby instead.", + "BaseLoadLimit": "Base Load", + "BaseLoadLimitHint": "Relevant for operation without power meter or when the power meter fails. As long as the other conditions allow (in particular battery charge), this limit is set on the inverter.", + "UpperPowerLimit": "Maximum Power Limit", + "UpperPowerLimitHint": "The inverter is always set such that no more than this output power is achieved. This value must be selected to comply with the current carrying capacity of the AC connection cables.", "SocThresholds": "Battery State of Charge (SoC) Thresholds", "IgnoreSoc": "Ignore Battery SoC", "StartThreshold": "Start Threshold for Battery Discharging", @@ -838,15 +852,25 @@ "ChargerSettings": "AC Charger Settings", "Configuration": "AC Charger Configuration", "EnableHuawei": "Enable Huawei R4850G2 on CAN Bus Interface", + "VerboseLogging": "@:base.VerboseLogging", "CanControllerFrequency": "CAN controller quarz frequency", "EnableAutoPower": "Automatic power control", + "EnableBatterySoCLimits": "Use SoC data of a connected battery", "Limits": "Limits", + "BatterySoCLimits": "Battery SoC Limits", "VoltageLimit": "Charge Voltage limit", "enableVoltageLimit": "Re-enable voltage limit", + "stopVoltageLimitHint": "Maximum charger voltage. Equals battery charge voltage limit. Used for automatic power control and when emergency charging", "enableVoltageLimitHint": "Automatic power control is disabled if the output voltage is higher then this value and if the output power drops below the minimum output power limit (set below).\nAutomatic power control is re-enabled if the battery voltage drops below the value set in this field.", + "upperPowerLimitHint": "Maximum output power. Used for automatic power control and when emergency charging", "lowerPowerLimit": "Minimum output power", "upperPowerLimit": "Maximum output power", - "Seconds": "@:base.Seconds" + "StopBatterySoCThreshold": "Stop charging at SoC", + "StopBatterySoCThresholdHint": "To prolong the battery's lifespan, charging can be stopped at a certain SoC level.\nHint: In order to keep the SoC reading accurate, some LiFePO cells must be charged to full capacity regularly.", + "Seconds": "@:base.Seconds", + "EnableEmergencyCharge": "Emergency charge. Battery charged with maximum power if requested by Battery BMS", + "targetPowerConsumption": "Target power consumption", + "targetPowerConsumptionHint": "Postitive values use grid power to charge the battery. Negative values result in early shutdown" }, "battery": { "battery": "Battery", diff --git a/webapp/src/locales/fr.json b/webapp/src/locales/fr.json index df56ac072..972f11932 100644 --- a/webapp/src/locales/fr.json +++ b/webapp/src/locales/fr.json @@ -41,6 +41,9 @@ "Release": "Release to refresh", "Close": "Fermer" }, + "Error": { + "Oops": "Oops!" + }, "localeswitcher": { "Dark": "Sombre", "Light": "Clair", @@ -153,25 +156,29 @@ "FirmwareNumber": "Firmware Number: ", "DataAge": "Data Age: ", "Seconds": "{val} seconds", - "DeviceInfo": "Device Info", "Property": "Property", "Value": "Value", "Unit": "Unit", - "LoadOutputState": "Load output state", - "StateOfOperation": "State of operation", - "TrackerOperationMode": "Tracker operation mode", - "OffReason": "Off reason", - "ErrorCode": "Error code", - "DaySequenceNumber": "Day sequence number (0..364)", - "Battery": "Output (Battery)", + "section_device": "Device Info", + "device": { + "LOAD": "Load output state", + "CS": "State of operation", + "MPPT": "Tracker operation mode", + "OR": "Off reason", + "ERR": "Error code", + "HSDS": "Day sequence number (0..364)", + "MpptTemperature": "Charge controller temperature" + }, + "section_output": "Output (Battery)", "output": { "P": "Power (calculated)", "V": "Voltage", "I": "Current", "E": "Efficiency (calculated)" }, - "Panel": "Input (Solar Panels)", + "section_input": "Input (Solar Panels)", "input": { + "NetworkPower": "VE.Smart network total power", "PPV": "Power", "VPV": "Voltage", "IPV": "Current (calculated)", @@ -669,7 +676,7 @@ "ConfigHintRequirement": "Required", "ConfigHintOptional": "Optional", "ConfigHintsIntro": "The following notes regarding the Dynamic Power Limiter (DPL) configuration shall be considered:", - "ConfigHintPowerMeterDisabled": "Operating the DPL requires the Power Meter being configured and delivering data.", + "ConfigHintPowerMeterDisabled": "Without a power meter interface, the inverter limit the DPL will configure equals the configured base load (exception: (full) solar-passthrough).", "ConfigHintNoInverter": "At least one inverter must be configured prior to setting up the DPL.", "ConfigHintInverterCommunication": "Polling data from and sending commands to the target inverter must be enabled.", "ConfigHintNoChargeController": "The solar-passthrough feature can only be used if the VE.Direct interface is configured.", @@ -690,9 +697,13 @@ "TargetPowerConsumption": "Target Grid Consumption", "TargetPowerConsumptionHint": "Grid power consumption the limiter tries to achieve. Value may be negative.", "TargetPowerConsumptionHysteresis": "Hysteresis", - "TargetPowerConsumptionHysteresisHint": "Only send a newly calculated power limit to the inverter if the absolute difference to the last sent power limit matches or exceeds this amount.", - "LowerPowerLimit": "Lower Power Limit", - "UpperPowerLimit": "Upper Power Limit", + "TargetPowerConsumptionHysteresisHint": "Only send a newly calculated power limit to the inverter if the absolute difference to the last reported power limit exceeds this amount.", + "LowerPowerLimit": "Minimum Power Limit", + "LowerPowerLimitHint": "This value must be selected so that stable operation is possible at this limit. If the inverter could only be operated with a lower limit, it is put into standby instead.", + "BaseLoadLimit": "Base Load", + "BaseLoadLimitHint": "Relevant for operation without power meter or when the power meter fails. As long as the other conditions allow (in particular battery charge), this limit is set on the inverter.", + "UpperPowerLimit": "Maximum Power Limit", + "UpperPowerLimitHint": "The inverter is always set such that no more than this output power is achieved. This value must be selected to comply with the current carrying capacity of the AC connection cables.", "SocThresholds": "Battery State of Charge (SoC) Thresholds", "IgnoreSoc": "Ignore Battery SoC", "StartThreshold": "Start Threshold for Battery Discharging", @@ -829,15 +840,25 @@ "ChargerSettings": "AC Charger Settings", "Configuration": "AC Charger Configuration", "EnableHuawei": "Enable Huawei R4850G2 on CAN Bus Interface", + "VerboseLogging": "@:base.VerboseLogging", "CanControllerFrequency": "CAN controller quarz frequency", "EnableAutoPower": "Automatic power control", + "EnableBatterySoCLimits": "Use SoC data of a connected battery", "Limits": "Limits", + "BatterySoCLimits": "Battery SoC Limits", "VoltageLimit": "Charge Voltage limit", "enableVoltageLimit": "Re-enable voltage limit", + "stopVoltageLimitHint": "Maximum charger voltage. Equals battery charge voltage limit. Used for automatic power control and when emergency charging", "enableVoltageLimitHint": "Automatic power control is disabled if the output voltage is higher then this value and if the output power drops below the minimum output power limit (set below).\nAutomatic power control is re-enabled if the battery voltage drops below the value set in this field.", + "upperPowerLimitHint": "Maximum output power. Used for automatic power control and when emergency charging", "lowerPowerLimit": "Minimum output power", "upperPowerLimit": "Maximum output power", - "Seconds": "@:base.Seconds" + "StopBatterySoCThreshold": "Stop charging at SoC", + "StopBatterySoCThresholdHint": "To prolong the battery's lifespan, charging can be stopped at a certain SoC level.\nHint: In order to keep the SoC reading accurate, some LiFePO cells must be charged to full capacity regularly.", + "Seconds": "@:base.Seconds", + "EnableEmergencyCharge": "Emergency charge. Battery charged with maximum power if requested by Battery BMS", + "targetPowerConsumption": "Target power consumption", + "targetPowerConsumptionHint": "Postitive values use grid power to charge the battery. Negative values result in early shutdown" }, "battery": { "battery": "Battery", diff --git a/webapp/src/router/index.ts b/webapp/src/router/index.ts index 6fe0493d2..4e584fdcd 100644 --- a/webapp/src/router/index.ts +++ b/webapp/src/router/index.ts @@ -5,6 +5,7 @@ import ConfigAdminView from '@/views/ConfigAdminView.vue'; import ConsoleInfoView from '@/views/ConsoleInfoView.vue'; import DeviceAdminView from '@/views/DeviceAdminView.vue' import DtuAdminView from '@/views/DtuAdminView.vue'; +import ErrorView from '@/views/ErrorView.vue'; import FirmwareUpgradeView from '@/views/FirmwareUpgradeView.vue'; import HomeView from '@/views/HomeView.vue'; import VedirectAdminView from '@/views/VedirectAdminView.vue' @@ -38,6 +39,11 @@ const router = createRouter({ name: 'Login', component: LoginView }, + { + path: '/error?status=:status&message=:message', + name: 'Error', + component: ErrorView + }, { path: '/about', name: 'About', @@ -151,4 +157,4 @@ const router = createRouter({ ] }); -export default router; \ No newline at end of file +export default router; diff --git a/webapp/src/types/AcChargerConfig.ts b/webapp/src/types/AcChargerConfig.ts index 2e1b9ca89..80625d8e1 100644 --- a/webapp/src/types/AcChargerConfig.ts +++ b/webapp/src/types/AcChargerConfig.ts @@ -1,9 +1,14 @@ export interface AcChargerConfig { enabled: boolean; + verbose_logging: boolean; can_controller_frequency: number; auto_power_enabled: boolean; + auto_power_batterysoc_limits_enabled: boolean; voltage_limit: number; enable_voltage_limit: number; lower_power_limit: number; upper_power_limit: number; + emergency_charge_enabled: boolean; + stop_batterysoc_threshold: number; + target_power_consumption: number; } diff --git a/webapp/src/types/PowerLimiterConfig.ts b/webapp/src/types/PowerLimiterConfig.ts index c6edde8d6..0d7ad388d 100644 --- a/webapp/src/types/PowerLimiterConfig.ts +++ b/webapp/src/types/PowerLimiterConfig.ts @@ -31,6 +31,7 @@ export interface PowerLimiterConfig { target_power_consumption: number; target_power_consumption_hysteresis: number; lower_power_limit: number; + base_load_limit: number; upper_power_limit: number; ignore_soc: boolean; battery_soc_start_threshold: number; diff --git a/webapp/src/types/PowerMeterConfig.ts b/webapp/src/types/PowerMeterConfig.ts index d6eb9ee7d..a8ceb4f78 100644 --- a/webapp/src/types/PowerMeterConfig.ts +++ b/webapp/src/types/PowerMeterConfig.ts @@ -9,6 +9,8 @@ export interface PowerMeterHttpPhaseConfig { header_value: string; json_path: string; timeout: number; + unit: number; + sign_inverted: boolean; } export interface PowerMeterConfig { diff --git a/webapp/src/types/VedirectLiveDataStatus.ts b/webapp/src/types/VedirectLiveDataStatus.ts index 73b78a454..0cdb996fb 100644 --- a/webapp/src/types/VedirectLiveDataStatus.ts +++ b/webapp/src/types/VedirectLiveDataStatus.ts @@ -10,39 +10,11 @@ export interface Vedirect { instances: { [key: string]: VedirectInstance }; } +type MpptData = (ValueObject | string)[]; + export interface VedirectInstance { data_age_ms: number; - device: VedirectDevice; - output: VedirectOutput; - input: VedirectInput; -} - -export interface VedirectDevice { - SER: string; - PID: string; - FW: string; - LOAD: ValueObject; - CS: ValueObject; - MPPT: ValueObject; - OR: ValueObject; - ERR: ValueObject; - HSDS: ValueObject; -} - -export interface VedirectOutput { - P: ValueObject; - V: ValueObject; - I: ValueObject; - E: ValueObject; -} - -export interface VedirectInput { - PPV: ValueObject; - VPV: ValueObject; - IPV: ValueObject; - H19: ValueObject; - H20: ValueObject; - H21: ValueObject; - H22: ValueObject; - H23: ValueObject; + product_id: string; + firmware_version: string; + values: { [key: string]: MpptData }; } diff --git a/webapp/src/utils/authentication.ts b/webapp/src/utils/authentication.ts index d1f87e3d8..0f1debd03 100644 --- a/webapp/src/utils/authentication.ts +++ b/webapp/src/utils/authentication.ts @@ -41,7 +41,7 @@ export function isLoggedIn(): boolean { return (localStorage.getItem('user') != null); } -export function login(username: String, password: String) { +export function login(username: string, password: string) { const requestOptions = { method: 'GET', headers: { @@ -65,7 +65,7 @@ export function login(username: String, password: String) { }); } -export function handleResponse(response: Response, emitter: Emitter>, router: Router) { +export function handleResponse(response: Response, emitter: Emitter>, router: Router, ignore_error: boolean = false) { return response.text().then(text => { const data = text && JSON.parse(text); if (!response.ok) { @@ -74,9 +74,13 @@ export function handleResponse(response: Response, emitter: Emitter
+ + + + + + + v-show="acChargerConfigList.auto_power_enabled || acChargerConfigList.emergency_charge_enabled">
- +
W
- +
+ aria-describedby="upperPowerLimitDescription" min="100" max="3000" required/> W
+ +
+
+ + W +
+
+ +
+
+ +
+ +
+
+ + % +
+
diff --git a/webapp/src/views/ConfigAdminView.vue b/webapp/src/views/ConfigAdminView.vue index c0954396e..e0bb66dc8 100644 --- a/webapp/src/views/ConfigAdminView.vue +++ b/webapp/src/views/ConfigAdminView.vue @@ -188,8 +188,8 @@ export default defineComponent({ fetch("/api/config/get?file=" + this.backupFileSelect, { headers: authHeader() }) .then(res => res.blob()) .then(blob => { - var file = window.URL.createObjectURL(blob); - var a = document.createElement('a'); + const file = window.URL.createObjectURL(blob); + const a = document.createElement('a'); a.href = file; a.download = this.backupFileSelect; document.body.appendChild(a); diff --git a/webapp/src/views/ConsoleInfoView.vue b/webapp/src/views/ConsoleInfoView.vue index fb17f62de..eba1d533f 100644 --- a/webapp/src/views/ConsoleInfoView.vue +++ b/webapp/src/views/ConsoleInfoView.vue @@ -1,154 +1,154 @@ - - - - - \ No newline at end of file + + + + + diff --git a/webapp/src/views/DeviceAdminView.vue b/webapp/src/views/DeviceAdminView.vue index c74500873..82b461ab2 100644 --- a/webapp/src/views/DeviceAdminView.vue +++ b/webapp/src/views/DeviceAdminView.vue @@ -219,7 +219,7 @@ export default defineComponent({ getPinMappingList() { this.pinMappingLoading = true; fetch("/api/config/get?file=pin_mapping.json", { headers: authHeader() }) - .then((response) => handleResponse(response, this.$emitter, this.$router)) + .then((response) => handleResponse(response, this.$emitter, this.$router, true)) .then( (data) => { this.pinMappingList = data; @@ -246,6 +246,9 @@ export default defineComponent({ .then( (data) => { this.deviceConfigList = data; + if (this.deviceConfigList.curPin.name === "") { + this.deviceConfigList.curPin.name = "Default"; + } this.dataLoading = false; } ) diff --git a/webapp/src/views/ErrorView.vue b/webapp/src/views/ErrorView.vue new file mode 100644 index 000000000..e9cd84d84 --- /dev/null +++ b/webapp/src/views/ErrorView.vue @@ -0,0 +1,18 @@ + + + diff --git a/webapp/src/views/FirmwareUpgradeView.vue b/webapp/src/views/FirmwareUpgradeView.vue index 738460791..f7bf0d75f 100644 --- a/webapp/src/views/FirmwareUpgradeView.vue +++ b/webapp/src/views/FirmwareUpgradeView.vue @@ -191,7 +191,7 @@ export default defineComponent({ const remoteHostUrl = "/api/system/status"; // Use a simple fetch request to check if the remote host is reachable - fetch(remoteHostUrl, { method: 'HEAD' }) + fetch(remoteHostUrl, { method: 'GET' }) .then(response => { // Check if the response status is OK (200-299 range) if (response.ok) { diff --git a/webapp/src/views/HomeView.vue b/webapp/src/views/HomeView.vue index d223db1df..46b362917 100644 --- a/webapp/src/views/HomeView.vue +++ b/webapp/src/views/HomeView.vue @@ -5,14 +5,20 @@
@@ -487,17 +493,15 @@ export default defineComponent({ } }; - var self = this; - - this.socket.onopen = function (event) { + this.socket.onopen = (event) => { console.log(event); console.log("Successfully connected to the echo websocket server..."); - self.isWebsocketConnected = true; + this.isWebsocketConnected = true; }; - this.socket.onclose = function () { + this.socket.onclose = () => { console.log("Connection to websocket closed...") - self.isWebsocketConnected = false; + this.isWebsocketConnected = false; } // Listen to window events , When the window closes , Take the initiative to disconnect websocket Connect diff --git a/webapp/src/views/PowerLimiterAdminView.vue b/webapp/src/views/PowerLimiterAdminView.vue index 0a58b4a9d..500614c6c 100644 --- a/webapp/src/views/PowerLimiterAdminView.vue +++ b/webapp/src/views/PowerLimiterAdminView.vue @@ -34,7 +34,7 @@ v-model="powerLimiterConfigList.verbose_logging" type="checkbox" wide/> - + + - @@ -235,8 +244,8 @@ export default defineComponent({ }, watch: { 'powerLimiterConfigList.inverter_serial'(newVal) { - var cfg = this.powerLimiterConfigList; - var meta = this.powerLimiterMetaData; + const cfg = this.powerLimiterConfigList; + const meta = this.powerLimiterMetaData; if (newVal === "") { return; } // do not try to convert the placeholder value @@ -262,13 +271,12 @@ export default defineComponent({ }, methods: { getConfigHints() { - var cfg = this.powerLimiterConfigList; - var meta = this.powerLimiterMetaData; - var hints = []; + const cfg = this.powerLimiterConfigList; + const meta = this.powerLimiterMetaData; + const hints = []; if (meta.power_meter_enabled !== true) { - hints.push({severity: "requirement", subject: "PowerMeterDisabled"}); - this.configAlert = true; + hints.push({severity: "optional", subject: "PowerMeterDisabled"}); } if (typeof meta.inverters === "undefined" || Object.keys(meta.inverters).length == 0) { @@ -276,7 +284,7 @@ export default defineComponent({ this.configAlert = true; } else { - var inv = meta.inverters[cfg.inverter_serial]; + const inv = meta.inverters[cfg.inverter_serial]; if (inv !== undefined && !(inv.poll_enable && inv.command_enable && inv.poll_enable_night && inv.command_enable_night)) { hints.push({severity: "requirement", subject: "InverterCommunication"}); } @@ -297,20 +305,23 @@ export default defineComponent({ isEnabled() { return this.powerLimiterConfigList.enabled; }, + hasPowerMeter() { + return this.powerLimiterMetaData.power_meter_enabled; + }, canUseSolarPassthrough() { - var cfg = this.powerLimiterConfigList; - var meta = this.powerLimiterMetaData; - var canUse = this.isEnabled() && meta.charge_controller_enabled && !cfg.is_inverter_solar_powered; + const cfg = this.powerLimiterConfigList; + const meta = this.powerLimiterMetaData; + const canUse = this.isEnabled() && meta.charge_controller_enabled && !cfg.is_inverter_solar_powered; if (!canUse) { cfg.solar_passthrough_enabled = false; } return canUse; }, canUseSoCThresholds() { - var cfg = this.powerLimiterConfigList; - var meta = this.powerLimiterMetaData; + const cfg = this.powerLimiterConfigList; + const meta = this.powerLimiterMetaData; return this.isEnabled() && meta.battery_enabled && !cfg.is_inverter_solar_powered; }, canUseVoltageThresholds() { - var cfg = this.powerLimiterConfigList; + const cfg = this.powerLimiterConfigList; return this.isEnabled() && !cfg.is_inverter_solar_powered; }, isSolarPassthroughEnabled() { @@ -320,10 +331,10 @@ export default defineComponent({ return Array.from(Array(end).keys()); }, needsChannelSelection() { - var cfg = this.powerLimiterConfigList; - var meta = this.powerLimiterMetaData; + const cfg = this.powerLimiterConfigList; + const meta = this.powerLimiterMetaData; - var reset = function() { + const reset = function() { cfg.inverter_channel_id = 0; return false; }; @@ -332,7 +343,7 @@ export default defineComponent({ if (cfg.is_inverter_solar_powered) { return reset(); } - var inverter = meta.inverters[cfg.inverter_serial]; + const inverter = meta.inverters[cfg.inverter_serial]; if (inverter === undefined) { return reset(); } if (cfg.inverter_channel_id >= inverter.channels) { diff --git a/webapp/src/views/PowerMeterAdminView.vue b/webapp/src/views/PowerMeterAdminView.vue index b490b7c58..8371d4651 100644 --- a/webapp/src/views/PowerMeterAdminView.vue +++ b/webapp/src/views/PowerMeterAdminView.vue @@ -105,6 +105,24 @@ wide /> + + +
+ +
+ +
+
+ + +