diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 112e2b22e..5e73566e7 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -5,11 +5,18 @@ body: - type: markdown attributes: value: > - ### ✋ **This is bug tracker, not a support forum** + ### ⚠️ Please remember: issues are for *bugs* + That is, something you believe affects every single user of OpenDTU, not just you. If you're not sure, start with one of the other options below. + - type: markdown + attributes: + value: | + #### Have a question? 👉 [Start a new discussion](https://github.com/tbnobody/OpenDTU/discussions/new) or [ask in chat](https://discord.gg/WzhxEY62mB). - If something isn't working right, you have questions or need help, [**get in touch on the Discussions**](https://github.com/tbnobody/OpenDTU/discussions). + #### Before opening an issue, please double check: - Please quickly search existing issues first before submitting a bug. + - [Documentation](https://www.opendtu.solar). + - [The FAQs](https://www.opendtu.solar/firmware/faq/). + - [Existing issues and discussions](https://github.com/tbnobody/OpenDTU/search?q=&type=issues). - type: textarea id: what-happened attributes: @@ -65,4 +72,17 @@ body: Links? References? Anything that will give us more context about the issue you are encountering! Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. validations: - required: false \ No newline at end of file + required: false + - type: checkboxes + id: required-checks + attributes: + label: Please confirm the following + options: + - label: I believe this issue is a bug that affects all users of OpenDTU, not something specific to my installation. + required: true + - label: I have already searched for relevant existing issues and discussions before opening this report. + required: true + - label: I have updated the title field above with a concise description. + required: true + - label: I have double checked that my inverter does not contain a W in the model name (like HMS-xxxW) as they are not supported + required: true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4d4f5b9d1..0a1eaa780 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: OpenDTU-onBattery Build +name: OpenDTU-OnBattery Build on: push: @@ -8,7 +8,7 @@ on: branches: - master - development - tags-ignore: + tags-ignore: - 'v**' pull_request: paths-ignore: @@ -120,6 +120,7 @@ jobs: name: opendtu-onbattery-${{ matrix.environment }} path: | .pio/build/${{ matrix.environment }}/opendtu-onbattery-${{ matrix.environment }}.bin + !.pio/build/generic_esp32_4mb_no_ota/opendtu-onbattery-generic_esp32_4mb_no_ota.bin .pio/build/${{ matrix.environment }}/opendtu-onbattery-${{ matrix.environment }}.factory.bin release: @@ -130,14 +131,14 @@ jobs: steps: - name: Checkout uses: actions/checkout@v3 - + - name: Get tags run: git fetch --force --tags origin - name: Get openDTU core release run: | echo "OPEN_DTU_CORE_RELEASE=$(git for-each-ref --sort=creatordate --format '%(refname) %(creatordate)' refs/tags | grep 'refs/tags/v' | tail -1 | sed 's#.*/##' | sed 's/ .*//')" >> $GITHUB_ENV - + - name: Create openDTU-core-release-Badge uses: schneegans/dynamic-badges-action@v1.6.0 with: diff --git a/docs/DeviceProfiles/wt32-eth01.json b/docs/DeviceProfiles/wt32-eth01.json index 8af112832..b7f5cbdd4 100644 --- a/docs/DeviceProfiles/wt32-eth01.json +++ b/docs/DeviceProfiles/wt32-eth01.json @@ -22,6 +22,34 @@ "clk_mode": 0 } }, + { + "name": "WT32-ETH01 with SH1106", + "links": [ + {"name": "Datasheet", "url": "http://www.wireless-tag.com/portfolio/wt32-eth01/"} + ], + "nrf24": { + "miso": 4, + "mosi": 2, + "clk": 32, + "irq": 33, + "en": 14, + "cs": 15 + }, + "eth": { + "enabled": true, + "phy_addr": 1, + "power": 16, + "mdc": 23, + "mdio": 18, + "type": 0, + "clk_mode": 0 + }, + "display": { + "type": 3, + "data": 5, + "clk": 17 + } + }, { "name": "WT32-ETH01 with SSD1306", "links": [ @@ -78,4 +106,4 @@ "clk": 17 } } -] \ No newline at end of file +] diff --git a/include/BatteryCanReceiver.h b/include/BatteryCanReceiver.h new file mode 100644 index 000000000..d9565ee39 --- /dev/null +++ b/include/BatteryCanReceiver.h @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "Battery.h" +#include +#include + +class BatteryCanReceiver : public BatteryProvider { +public: + bool init(bool verboseLogging, char const* providerName); + void deinit() final; + void loop() final; + + virtual void onMessage(twai_message_t rx_message) = 0; + +protected: + uint8_t readUnsignedInt8(uint8_t *data); + uint16_t readUnsignedInt16(uint8_t *data); + int16_t readSignedInt16(uint8_t *data); + uint32_t readUnsignedInt32(uint8_t *data); + float scaleValue(int16_t value, float factor); + bool getBit(uint8_t value, uint8_t bit); + + bool _verboseLogging = true; + +private: + char const* _providerName = "Battery CAN"; +}; diff --git a/include/BatteryStats.h b/include/BatteryStats.h index 0f86c7c5d..94da35d78 100644 --- a/include/BatteryStats.h +++ b/include/BatteryStats.h @@ -14,33 +14,37 @@ class BatteryStats { public: String const& getManufacturer() const { return _manufacturer; } - // the last time *any* datum was updated + // the last time *any* data was updated uint32_t getAgeSeconds() const { return (millis() - _lastUpdate) / 1000; } bool updateAvailable(uint32_t since) const; uint8_t getSoC() const { return _soc; } uint32_t getSoCAgeSeconds() const { return (millis() - _lastUpdateSoC) / 1000; } + uint8_t getSoCPrecision() const { return _socPrecision; } float getVoltage() const { return _voltage; } uint32_t getVoltageAgeSeconds() const { return (millis() - _lastUpdateVoltage) / 1000; } + float getChargeCurrent() const { return _current; }; + uint8_t getChargeCurrentPrecision() const { return _currentPrecision; } + // convert stats to JSON for web application live view virtual void getLiveViewData(JsonVariant& root) const; void mqttLoop(); - // the interval at which all battery datums will be re-published, even + // the interval at which all battery data will be re-published, even // if they did not change. used to calculate Home Assistent expiration. virtual uint32_t getMqttFullPublishIntervalMs() const; bool isSoCValid() const { return _lastUpdateSoC > 0; } bool isVoltageValid() const { return _lastUpdateVoltage > 0; } + bool isCurrentValid() const { return _lastUpdateCurrent > 0; } // returns true if the battery reached a critically low voltage/SoC, // such that it is in need of charging to prevent degredation. virtual bool getImmediateChargingRequest() const { return false; }; - virtual float getChargeCurrent() const { return 0; }; virtual float getChargeCurrentLimitation() const { return FLT_MAX; }; protected: @@ -57,9 +61,16 @@ class BatteryStats { _lastUpdateVoltage = _lastUpdate = timestamp; } + void setCurrent(float current, uint8_t precision, uint32_t timestamp) { + _current = current; + _currentPrecision = precision; + _lastUpdateCurrent = _lastUpdate = timestamp; + } + String _manufacturer = "unknown"; String _hwversion = ""; String _fwversion = ""; + String _serial = ""; uint32_t _lastUpdate = 0; private: @@ -69,6 +80,12 @@ class BatteryStats { uint32_t _lastUpdateSoC = 0; float _voltage = 0; // total battery pack voltage uint32_t _lastUpdateVoltage = 0; + + // total current into (positive) or from (negative) + // the battery, i.e., the charging current + float _current = 0; + uint8_t _currentPrecision = 0; // decimal places + uint32_t _lastUpdateCurrent = 0; }; class PylontechBatteryStats : public BatteryStats { @@ -78,7 +95,6 @@ class PylontechBatteryStats : public BatteryStats { void getLiveViewData(JsonVariant& root) const final; void mqttPublish() const final; bool getImmediateChargingRequest() const { return _chargeImmediately; } ; - float getChargeCurrent() const { return _current; } ; float getChargeCurrentLimitation() const { return _chargeCurrentLimitation; } ; private: @@ -89,9 +105,6 @@ class PylontechBatteryStats : public BatteryStats { float _chargeCurrentLimitation; float _dischargeCurrentLimitation; uint16_t _stateOfHealth; - // total current into (positive) or from (negative) - // the battery, i.e., the charging current - float _current; float _temperature; bool _alarmOverCurrentDischarge; @@ -115,6 +128,80 @@ class PylontechBatteryStats : public BatteryStats { bool _chargeImmediately; }; +class PytesBatteryStats : public BatteryStats { + friend class PytesCanReceiver; + + public: + void getLiveViewData(JsonVariant& root) const final; + void mqttPublish() const final; + float getChargeCurrentLimitation() const { return _chargeCurrentLimit; } ; + + private: + void setManufacturer(String&& m) { _manufacturer = std::move(m); } + void setLastUpdate(uint32_t ts) { _lastUpdate = ts; } + void updateSerial() { + if (!_serialPart1.isEmpty() && !_serialPart2.isEmpty()) { + _serial = _serialPart1 + _serialPart2; + } + } + + String _serialPart1 = ""; + String _serialPart2 = ""; + + float _chargeVoltageLimit; + float _chargeCurrentLimit; + float _dischargeVoltageLimit; + float _dischargeCurrentLimit; + + uint16_t _stateOfHealth; + + float _temperature; + + uint16_t _cellMinMilliVolt; + uint16_t _cellMaxMilliVolt; + float _cellMinTemperature; + float _cellMaxTemperature; + + String _cellMinVoltageName; + String _cellMaxVoltageName; + String _cellMinTemperatureName; + String _cellMaxTemperatureName; + + uint8_t _moduleCountOnline; + uint8_t _moduleCountOffline; + + uint8_t _moduleCountBlockingCharge; + uint8_t _moduleCountBlockingDischarge; + + uint16_t _totalCapacity; + uint16_t _availableCapacity; + + float _chargedEnergy = -1; + float _dischargedEnergy = -1; + + bool _alarmUnderVoltage; + bool _alarmOverVoltage; + bool _alarmOverCurrentCharge; + bool _alarmOverCurrentDischarge; + bool _alarmUnderTemperature; + bool _alarmOverTemperature; + bool _alarmUnderTemperatureCharge; + bool _alarmOverTemperatureCharge; + bool _alarmInternalFailure; + bool _alarmCellImbalance; + + bool _warningLowVoltage; + bool _warningHighVoltage; + bool _warningHighChargeCurrent; + bool _warningHighDischargeCurrent; + bool _warningLowTemperature; + bool _warningHighTemperature; + bool _warningLowTemperatureCharge; + bool _warningHighTemperatureCharge; + bool _warningInternalFailure; + bool _warningCellImbalance; +}; + class JkBmsBatteryStats : public BatteryStats { public: void getLiveViewData(JsonVariant& root) const final { @@ -152,7 +239,6 @@ class VictronSmartShuntStats : public BatteryStats { void updateFrom(VeDirectShuntController::data_t const& shuntData); private: - float _current; float _temperature; bool _tempPresent; uint8_t _chargeCycles; @@ -180,7 +266,7 @@ class MqttBatteryStats : public BatteryStats { // we do NOT publish the same data under a different topic. void mqttPublish() const final { } - // if the voltage is subscribed to at all, it alone does not warrant a - // card in the live view, since the SoC is already displayed at the top + // we don't need a card in the liveview, since the SoC and + // voltage (if available) is already displayed at the top. void getLiveViewData(JsonVariant& root) const final { } }; diff --git a/include/Configuration.h b/include/Configuration.h index a78b80626..9f433faf3 100644 --- a/include/Configuration.h +++ b/include/Configuration.h @@ -3,6 +3,7 @@ #include "PinMapping.h" #include +#include #define CONFIG_FILENAME "/config.json" #define CONFIG_VERSION 0x00011c00 // 0.1.28 // make sure to clean all after change @@ -16,6 +17,7 @@ #define NTP_MAX_TIMEZONEDESCR_STRLEN 50 #define MQTT_MAX_HOSTNAME_STRLEN 128 +#define MQTT_MAX_CLIENTID_STRLEN 64 #define MQTT_MAX_USERNAME_STRLEN 64 #define MQTT_MAX_PASSWORD_STRLEN 64 #define MQTT_MAX_TOPIC_STRLEN 256 @@ -30,14 +32,16 @@ #define DEV_MAX_MAPPING_NAME_STRLEN 63 -#define POWERMETER_MAX_PHASES 3 -#define POWERMETER_MAX_HTTP_URL_STRLEN 1024 -#define POWERMETER_MAX_USERNAME_STRLEN 64 -#define POWERMETER_MAX_PASSWORD_STRLEN 64 -#define POWERMETER_MAX_HTTP_HEADER_KEY_STRLEN 64 -#define POWERMETER_MAX_HTTP_HEADER_VALUE_STRLEN 256 -#define POWERMETER_MAX_HTTP_JSON_PATH_STRLEN 256 -#define POWERMETER_HTTP_TIMEOUT 1000 +#define HTTP_REQUEST_MAX_URL_STRLEN 1024 +#define HTTP_REQUEST_MAX_USERNAME_STRLEN 64 +#define HTTP_REQUEST_MAX_PASSWORD_STRLEN 64 +#define HTTP_REQUEST_MAX_HEADER_KEY_STRLEN 64 +#define HTTP_REQUEST_MAX_HEADER_VALUE_STRLEN 256 + +#define POWERMETER_MQTT_MAX_VALUES 3 +#define POWERMETER_HTTP_JSON_MAX_VALUES 3 +#define POWERMETER_HTTP_JSON_MAX_PATH_STRLEN 256 +#define BATTERY_JSON_MAX_PATH_STRLEN 128 struct CHANNEL_CONFIG_T { uint16_t MaxChannelPower; @@ -56,26 +60,73 @@ struct INVERTER_CONFIG_T { uint8_t ReachableThreshold; bool ZeroRuntimeDataIfUnrechable; bool ZeroYieldDayOnMidnight; + bool ClearEventlogOnMidnight; bool YieldDayCorrection; CHANNEL_CONFIG_T channel[INV_MAX_CHAN_COUNT]; }; -struct POWERMETER_HTTP_PHASE_CONFIG_T { +struct HTTP_REQUEST_CONFIG_T { + char Url[HTTP_REQUEST_MAX_URL_STRLEN + 1]; + 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; - char Username[POWERMETER_MAX_USERNAME_STRLEN +1]; - char Password[POWERMETER_MAX_USERNAME_STRLEN +1]; - char HeaderKey[POWERMETER_MAX_HTTP_HEADER_KEY_STRLEN + 1]; - char HeaderValue[POWERMETER_MAX_HTTP_HEADER_VALUE_STRLEN + 1]; + + char Username[HTTP_REQUEST_MAX_USERNAME_STRLEN + 1]; + char Password[HTTP_REQUEST_MAX_PASSWORD_STRLEN + 1]; + char HeaderKey[HTTP_REQUEST_MAX_HEADER_KEY_STRLEN + 1]; + char HeaderValue[HTTP_REQUEST_MAX_HEADER_VALUE_STRLEN + 1]; uint16_t Timeout; - char JsonPath[POWERMETER_MAX_HTTP_JSON_PATH_STRLEN + 1]; +}; +using HttpRequestConfig = struct HTTP_REQUEST_CONFIG_T; + +struct POWERMETER_MQTT_VALUE_T { + char Topic[MQTT_MAX_TOPIC_STRLEN + 1]; + char JsonPath[POWERMETER_HTTP_JSON_MAX_PATH_STRLEN + 1]; + + enum Unit { Watts = 0, MilliWatts = 1, KiloWatts = 2 }; + Unit PowerUnit; + + bool SignInverted; +}; +using PowerMeterMqttValue = struct POWERMETER_MQTT_VALUE_T; + +struct POWERMETER_MQTT_CONFIG_T { + PowerMeterMqttValue Values[POWERMETER_MQTT_MAX_VALUES]; +}; +using PowerMeterMqttConfig = struct POWERMETER_MQTT_CONFIG_T; + +struct POWERMETER_SERIAL_SDM_CONFIG_T { + uint32_t Address; + uint32_t PollingInterval; +}; +using PowerMeterSerialSdmConfig = struct POWERMETER_SERIAL_SDM_CONFIG_T; + +struct POWERMETER_HTTP_JSON_VALUE_T { + HttpRequestConfig HttpRequest; + bool Enabled; + char JsonPath[POWERMETER_HTTP_JSON_MAX_PATH_STRLEN + 1]; + + enum Unit { Watts = 0, MilliWatts = 1, KiloWatts = 2 }; Unit PowerUnit; + bool SignInverted; }; -using PowerMeterHttpConfig = struct POWERMETER_HTTP_PHASE_CONFIG_T; +using PowerMeterHttpJsonValue = struct POWERMETER_HTTP_JSON_VALUE_T; + +struct POWERMETER_HTTP_JSON_CONFIG_T { + uint32_t PollingInterval; + bool IndividualRequests; + PowerMeterHttpJsonValue Values[POWERMETER_HTTP_JSON_MAX_VALUES]; +}; +using PowerMeterHttpJsonConfig = struct POWERMETER_HTTP_JSON_CONFIG_T; + +struct POWERMETER_HTTP_SML_CONFIG_T { + uint32_t PollingInterval; + HttpRequestConfig HttpRequest; +}; +using PowerMeterHttpSmlConfig = struct POWERMETER_HTTP_SML_CONFIG_T; + +enum BatteryVoltageUnit { Volts = 0, DeciVolts = 1, CentiVolts = 2, MilliVolts = 3 }; struct CONFIG_T { struct { @@ -114,6 +165,7 @@ struct CONFIG_T { char Hostname[MQTT_MAX_HOSTNAME_STRLEN + 1]; bool VerboseLogging; uint32_t Port; + char ClientId[MQTT_MAX_CLIENTID_STRLEN + 1]; char Username[MQTT_MAX_USERNAME_STRLEN + 1]; char Password[MQTT_MAX_PASSWORD_STRLEN + 1]; char Topic[MQTT_MAX_TOPIC_STRLEN + 1]; @@ -186,19 +238,14 @@ struct CONFIG_T { bool UpdatesOnly; } Vedirect; - struct { + struct PowerMeterConfig { bool Enabled; bool VerboseLogging; - uint32_t Interval; uint32_t Source; - char MqttTopicPowerMeter1[MQTT_MAX_TOPIC_STRLEN + 1]; - char MqttTopicPowerMeter2[MQTT_MAX_TOPIC_STRLEN + 1]; - char MqttTopicPowerMeter3[MQTT_MAX_TOPIC_STRLEN + 1]; - uint32_t SdmBaudrate; - uint32_t SdmAddress; - uint32_t HttpInterval; - bool HttpIndividualRequests; - PowerMeterHttpConfig Http_Phase[POWERMETER_MAX_PHASES]; + PowerMeterMqttConfig Mqtt; + PowerMeterSerialSdmConfig SerialSdm; + PowerMeterHttpJsonConfig HttpJson; + PowerMeterHttpSmlConfig HttpSml; } PowerMeter; struct { @@ -210,6 +257,7 @@ struct CONFIG_T { uint32_t Interval; bool IsInverterBehindPowerMeter; bool IsInverterSolarPowered; + bool UseOverscalingToCompensateShading; uint64_t InverterId; uint8_t InverterChannelId; int32_t TargetPowerConsumption; @@ -236,7 +284,10 @@ struct CONFIG_T { uint8_t JkBmsInterface; uint8_t JkBmsPollingInterval; char MqttSocTopic[MQTT_MAX_TOPIC_STRLEN + 1]; + char MqttSocJsonPath[BATTERY_JSON_MAX_PATH_STRLEN + 1]; char MqttVoltageTopic[MQTT_MAX_TOPIC_STRLEN + 1]; + char MqttVoltageJsonPath[BATTERY_JSON_MAX_PATH_STRLEN + 1]; + BatteryVoltageUnit MqttVoltageUnit; } Battery; struct { @@ -270,6 +321,18 @@ class ConfigurationClass { INVERTER_CONFIG_T* getFreeInverterSlot(); INVERTER_CONFIG_T* getInverterConfig(const uint64_t serial); void deleteInverterById(const uint8_t id); + + static void serializeHttpRequestConfig(HttpRequestConfig const& source, JsonObject& target); + static void serializePowerMeterMqttConfig(PowerMeterMqttConfig const& source, JsonObject& target); + static void serializePowerMeterSerialSdmConfig(PowerMeterSerialSdmConfig const& source, JsonObject& target); + static void serializePowerMeterHttpJsonConfig(PowerMeterHttpJsonConfig const& source, JsonObject& target); + static void serializePowerMeterHttpSmlConfig(PowerMeterHttpSmlConfig const& source, JsonObject& target); + + static void deserializeHttpRequestConfig(JsonObject const& source, HttpRequestConfig& target); + static void deserializePowerMeterMqttConfig(JsonObject const& source, PowerMeterMqttConfig& target); + static void deserializePowerMeterSerialSdmConfig(JsonObject const& source, PowerMeterSerialSdmConfig& target); + static void deserializePowerMeterHttpJsonConfig(JsonObject const& source, PowerMeterHttpJsonConfig& target); + static void deserializePowerMeterHttpSmlConfig(JsonObject const& source, PowerMeterHttpSmlConfig& target); }; extern ConfigurationClass Configuration; diff --git a/include/HttpGetter.h b/include/HttpGetter.h new file mode 100644 index 000000000..11ece1090 --- /dev/null +++ b/include/HttpGetter.h @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "Configuration.h" +#include +#include +#include +#include +#include +#include + +using up_http_client_t = std::unique_ptr; +using sp_wifi_client_t = std::shared_ptr; + +class HttpRequestResult { +public: + HttpRequestResult(bool success, + up_http_client_t upHttpClient = nullptr, + sp_wifi_client_t spWiFiClient = nullptr) + : _success(success) + , _upHttpClient(std::move(upHttpClient)) + , _spWiFiClient(std::move(spWiFiClient)) { } + + ~HttpRequestResult() { + // the wifi client *must* die *after* the http client, as the http + // client uses the wifi client in its destructor. + if (_upHttpClient) { _upHttpClient->end(); } + _upHttpClient = nullptr; + _spWiFiClient = nullptr; + } + + HttpRequestResult(HttpRequestResult const&) = delete; + HttpRequestResult(HttpRequestResult&&) = delete; + HttpRequestResult& operator=(HttpRequestResult const&) = delete; + HttpRequestResult& operator=(HttpRequestResult&&) = delete; + + operator bool() const { return _success; } + + Stream* getStream() { + if(!_upHttpClient) { return nullptr; } + return _upHttpClient->getStreamPtr(); + } + +private: + bool _success; + up_http_client_t _upHttpClient; + sp_wifi_client_t _spWiFiClient; +}; + +class HttpGetter { +public: + explicit HttpGetter(HttpRequestConfig const& cfg) + : _config(cfg) { } + + bool init(); + void addHeader(char const* key, char const* value); + HttpRequestResult performGetRequest(); + + char const* getErrorText() const { return _errBuffer; } + +private: + String getAuthDigest(String const& authReq, unsigned int counter); + HttpRequestConfig const& _config; + + template + void logError(char const* format, Args... args); + char _errBuffer[256]; + + bool _useHttps; + String _host; + String _uri; + uint16_t _port; + + sp_wifi_client_t _spWiFiClient; // reused for multiple HTTP requests + + std::vector> _additionalHeaders; +}; diff --git a/include/HttpPowerMeter.h b/include/HttpPowerMeter.h deleted file mode 100644 index 8f703bba2..000000000 --- a/include/HttpPowerMeter.h +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -#pragma once - -#include -#include -#include -#include "Configuration.h" - -using Auth_t = PowerMeterHttpConfig::Auth; -using Unit_t = PowerMeterHttpConfig::Unit; - -class HttpPowerMeterClass { -public: - void init(); - bool updateValues(); - float getPower(int8_t phase); - char httpPowerMeterError[256]; - bool queryPhase(int phase, PowerMeterHttpConfig const& config); - -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, 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, String 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 3a699cd7d..e84f2f092 100644 --- a/include/Huawei_can.h +++ b/include/Huawei_can.h @@ -1,158 +1,158 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -#pragma once - -#include -#include "SPI.h" -#include -#include -#include - -#ifndef HUAWEI_PIN_MISO -#define HUAWEI_PIN_MISO 12 -#endif - -#ifndef HUAWEI_PIN_MOSI -#define HUAWEI_PIN_MOSI 13 -#endif - -#ifndef HUAWEI_PIN_SCLK -#define HUAWEI_PIN_SCLK 26 -#endif - -#ifndef HUAWEI_PIN_IRQ -#define HUAWEI_PIN_IRQ 25 -#endif - -#ifndef HUAWEI_PIN_CS -#define HUAWEI_PIN_CS 15 -#endif - -#ifndef HUAWEI_PIN_POWER -#define HUAWEI_PIN_POWER 33 -#endif - -#define HUAWEI_MINIMAL_OFFLINE_VOLTAGE 48 -#define HUAWEI_MINIMAL_ONLINE_VOLTAGE 42 - -#define MAX_CURRENT_MULTIPLIER 20 - -// Index values for rec_values array -#define HUAWEI_INPUT_POWER_IDX 0 -#define HUAWEI_INPUT_FREQ_IDX 1 -#define HUAWEI_INPUT_CURRENT_IDX 2 -#define HUAWEI_OUTPUT_POWER_IDX 3 -#define HUAWEI_EFFICIENCY_IDX 4 -#define HUAWEI_OUTPUT_VOLTAGE_IDX 5 -#define HUAWEI_OUTPUT_CURRENT_MAX_IDX 6 -#define HUAWEI_INPUT_VOLTAGE_IDX 7 -#define HUAWEI_OUTPUT_TEMPERATURE_IDX 8 -#define HUAWEI_INPUT_TEMPERATURE_IDX 9 -#define HUAWEI_OUTPUT_CURRENT_IDX 10 -#define HUAWEI_OUTPUT_CURRENT1_IDX 11 - -// Defines and index values for tx_values array -#define HUAWEI_OFFLINE_VOLTAGE 0x01 -#define HUAWEI_ONLINE_VOLTAGE 0x00 -#define HUAWEI_OFFLINE_CURRENT 0x04 -#define HUAWEI_ONLINE_CURRENT 0x03 - -// Modes of operation -#define HUAWEI_MODE_OFF 0 -#define HUAWEI_MODE_ON 1 -#define HUAWEI_MODE_AUTO_EXT 2 -#define HUAWEI_MODE_AUTO_INT 3 - -// Error codes -#define HUAWEI_ERROR_CODE_RX 0x01 -#define HUAWEI_ERROR_CODE_TX 0x02 - -// Wait time/current before shuting down the PSU / charger -// This is set to allow the fan to run for some time -#define HUAWEI_AUTO_MODE_SHUTDOWN_DELAY 60000 -#define HUAWEI_AUTO_MODE_SHUTDOWN_CURRENT 0.75 - -// Updateinterval used to request new values from the PSU -#define HUAWEI_DATA_REQUEST_INTERVAL_MS 2500 - -typedef struct RectifierParameters { - float input_voltage; - float input_frequency; - float input_current; - float input_power; - float input_temp; - float efficiency; - float output_voltage; - float output_current; - float max_output_current; - float output_power; - float output_temp; - float amp_hour; -} RectifierParameters_t; - -class HuaweiCanCommClass { -public: - bool init(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, - uint8_t huawei_irq, uint8_t huawei_cs, uint32_t frequency); - void loop(); - bool gotNewRxDataFrame(bool clear); - uint8_t getErrorCode(bool clear); - uint32_t getParameterValue(uint8_t parameter); - void setParameterValue(uint16_t in, uint8_t parameterType); - -private: - void sendRequest(); - - SPIClass *SPI; - MCP_CAN *_CAN; - uint8_t _huaweiIrq; // IRQ pin - 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; -}; - -class HuaweiCanClass { -public: - void init(Scheduler& scheduler, uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, uint8_t huawei_irq, uint8_t huawei_cs, uint8_t huawei_power); - void updateSettings(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, uint8_t huawei_irq, uint8_t huawei_cs, uint8_t huawei_power); - void setValue(float in, uint8_t parameterType); - void setMode(uint8_t mode); - - RectifierParameters_t * get(); - uint32_t getLastUpdate() const { return _lastUpdateReceivedMillis; }; - bool getAutoPowerStatus() const { return _autoPowerEnabled; }; - uint8_t getMode() const { return _mode; }; - -private: - void loop(); - void processReceivedParameters(); - void _setValue(float in, uint8_t parameterType); - - Task _loopTask; - - TaskHandle_t _HuaweiCanCommunicationTaskHdl = NULL; - bool _initialized = false; - uint8_t _huaweiPower; // Power pin - uint8_t _mode = HUAWEI_MODE_AUTO_EXT; - - RectifierParameters_t _rp; - - uint32_t _lastUpdateReceivedMillis; // Timestamp for last data seen from the PSU - uint32_t _outputCurrentOnSinceMillis; // Timestamp since when the PSU was idle at zero amps - uint32_t _nextAutoModePeriodicIntMillis; // When to set the next output voltage in automatic mode - uint32_t _lastPowerMeterUpdateReceivedMillis; // Timestamp of last seen power meter value - uint32_t _autoModeBlockedTillMillis = 0; // Timestamp to block running auto mode for some time - - uint8_t _autoPowerEnabledCounter = 0; - bool _autoPowerEnabled = false; - bool _batteryEmergencyCharging = false; -}; - -extern HuaweiCanClass HuaweiCan; -extern HuaweiCanCommClass HuaweiCanComm; +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include "SPI.h" +#include +#include +#include + +#ifndef HUAWEI_PIN_MISO +#define HUAWEI_PIN_MISO 12 +#endif + +#ifndef HUAWEI_PIN_MOSI +#define HUAWEI_PIN_MOSI 13 +#endif + +#ifndef HUAWEI_PIN_SCLK +#define HUAWEI_PIN_SCLK 26 +#endif + +#ifndef HUAWEI_PIN_IRQ +#define HUAWEI_PIN_IRQ 25 +#endif + +#ifndef HUAWEI_PIN_CS +#define HUAWEI_PIN_CS 15 +#endif + +#ifndef HUAWEI_PIN_POWER +#define HUAWEI_PIN_POWER 33 +#endif + +#define HUAWEI_MINIMAL_OFFLINE_VOLTAGE 48 +#define HUAWEI_MINIMAL_ONLINE_VOLTAGE 42 + +#define MAX_CURRENT_MULTIPLIER 20 + +// Index values for rec_values array +#define HUAWEI_INPUT_POWER_IDX 0 +#define HUAWEI_INPUT_FREQ_IDX 1 +#define HUAWEI_INPUT_CURRENT_IDX 2 +#define HUAWEI_OUTPUT_POWER_IDX 3 +#define HUAWEI_EFFICIENCY_IDX 4 +#define HUAWEI_OUTPUT_VOLTAGE_IDX 5 +#define HUAWEI_OUTPUT_CURRENT_MAX_IDX 6 +#define HUAWEI_INPUT_VOLTAGE_IDX 7 +#define HUAWEI_OUTPUT_TEMPERATURE_IDX 8 +#define HUAWEI_INPUT_TEMPERATURE_IDX 9 +#define HUAWEI_OUTPUT_CURRENT_IDX 10 +#define HUAWEI_OUTPUT_CURRENT1_IDX 11 + +// Defines and index values for tx_values array +#define HUAWEI_OFFLINE_VOLTAGE 0x01 +#define HUAWEI_ONLINE_VOLTAGE 0x00 +#define HUAWEI_OFFLINE_CURRENT 0x04 +#define HUAWEI_ONLINE_CURRENT 0x03 + +// Modes of operation +#define HUAWEI_MODE_OFF 0 +#define HUAWEI_MODE_ON 1 +#define HUAWEI_MODE_AUTO_EXT 2 +#define HUAWEI_MODE_AUTO_INT 3 + +// Error codes +#define HUAWEI_ERROR_CODE_RX 0x01 +#define HUAWEI_ERROR_CODE_TX 0x02 + +// Wait time/current before shuting down the PSU / charger +// This is set to allow the fan to run for some time +#define HUAWEI_AUTO_MODE_SHUTDOWN_DELAY 60000 +#define HUAWEI_AUTO_MODE_SHUTDOWN_CURRENT 0.75 + +// Updateinterval used to request new values from the PSU +#define HUAWEI_DATA_REQUEST_INTERVAL_MS 2500 + +typedef struct RectifierParameters { + float input_voltage; + float input_frequency; + float input_current; + float input_power; + float input_temp; + float efficiency; + float output_voltage; + float output_current; + float max_output_current; + float output_power; + float output_temp; + float amp_hour; +} RectifierParameters_t; + +class HuaweiCanCommClass { +public: + bool init(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, + uint8_t huawei_irq, uint8_t huawei_cs, uint32_t frequency); + void loop(); + bool gotNewRxDataFrame(bool clear); + uint8_t getErrorCode(bool clear); + uint32_t getParameterValue(uint8_t parameter); + void setParameterValue(uint16_t in, uint8_t parameterType); + +private: + void sendRequest(); + + SPIClass *SPI; + MCP_CAN *_CAN; + uint8_t _huaweiIrq; // IRQ pin + 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; +}; + +class HuaweiCanClass { +public: + void init(Scheduler& scheduler, uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, uint8_t huawei_irq, uint8_t huawei_cs, uint8_t huawei_power); + void updateSettings(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, uint8_t huawei_irq, uint8_t huawei_cs, uint8_t huawei_power); + void setValue(float in, uint8_t parameterType); + void setMode(uint8_t mode); + + RectifierParameters_t * get(); + uint32_t getLastUpdate() const { return _lastUpdateReceivedMillis; }; + bool getAutoPowerStatus() const { return _autoPowerEnabled; }; + uint8_t getMode() const { return _mode; }; + +private: + void loop(); + void processReceivedParameters(); + void _setValue(float in, uint8_t parameterType); + + Task _loopTask; + + TaskHandle_t _HuaweiCanCommunicationTaskHdl = NULL; + bool _initialized = false; + uint8_t _huaweiPower; // Power pin + uint8_t _mode = HUAWEI_MODE_AUTO_EXT; + + RectifierParameters_t _rp; + + uint32_t _lastUpdateReceivedMillis; // Timestamp for last data seen from the PSU + uint32_t _outputCurrentOnSinceMillis; // Timestamp since when the PSU was idle at zero amps + uint32_t _nextAutoModePeriodicIntMillis; // When to set the next output voltage in automatic mode + uint32_t _lastPowerMeterUpdateReceivedMillis; // Timestamp of last seen power meter value + uint32_t _autoModeBlockedTillMillis = 0; // Timestamp to block running auto mode for some time + + uint8_t _autoPowerEnabledCounter = 0; + bool _autoPowerEnabled = false; + bool _batteryEmergencyCharging = false; +}; + +extern HuaweiCanClass HuaweiCan; +extern HuaweiCanCommClass HuaweiCanComm; diff --git a/include/JkBmsController.h b/include/JkBmsController.h index 7156d760b..1c31c6e3a 100644 --- a/include/JkBmsController.h +++ b/include/JkBmsController.h @@ -6,6 +6,7 @@ #include "Battery.h" #include "JkBmsSerialMessage.h" +#include "JkBmsDummy.h" //#define JKBMS_DUMMY_SERIAL diff --git a/include/JkBmsDummy.h b/include/JkBmsDummy.h new file mode 100644 index 000000000..1524023ae --- /dev/null +++ b/include/JkBmsDummy.h @@ -0,0 +1,196 @@ +#pragma once + +#include +#include +#include "MessageOutput.h" + +namespace JkBms { + +class DummySerial { + public: + DummySerial() = default; + void begin(uint32_t, uint32_t, int8_t, int8_t) { + MessageOutput.println("JK BMS Dummy Serial: begin()"); + } + void end() { MessageOutput.println("JK BMS Dummy Serial: end()"); } + void flush() { } + bool availableForWrite() const { return true; } + size_t write(const uint8_t *buffer, size_t size) { + MessageOutput.printf("JK BMS Dummy Serial: write(%d Bytes)\r\n", size); + _byte_idx = 0; + _msg_idx = (_msg_idx + 1) % _data.size(); + return size; + } + bool available() const { + return _byte_idx < _data[_msg_idx].size(); + } + int read() { + if (_byte_idx >= _data[_msg_idx].size()) { return 0; } + return _data[_msg_idx][_byte_idx++]; + } + + private: + std::vector> const _data = + { + { + 0x4e, 0x57, 0x01, 0x21, 0x00, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x01, 0x79, 0x30, 0x01, 0x0c, 0xfb, + 0x02, 0x0c, 0xfb, 0x03, 0x0c, 0xfb, 0x04, 0x0c, + 0xfb, 0x05, 0x0c, 0xfb, 0x06, 0x0c, 0xfb, 0x07, + 0x0c, 0xfb, 0x08, 0x0c, 0xf7, 0x09, 0x0d, 0x01, + 0x0a, 0x0c, 0xf9, 0x0b, 0x0c, 0xfb, 0x0c, 0x0c, + 0xfb, 0x0d, 0x0c, 0xfb, 0x0e, 0x0c, 0xf8, 0x0f, + 0x0c, 0xf9, 0x10, 0x0c, 0xfb, 0x80, 0x00, 0x1a, + 0x81, 0x00, 0x12, 0x82, 0x00, 0x12, 0x83, 0x14, + 0xc3, 0x84, 0x83, 0xf4, 0x85, 0x2e, 0x86, 0x02, + 0x87, 0x00, 0x15, 0x89, 0x00, 0x00, 0x13, 0x52, + 0x8a, 0x00, 0x10, 0x8b, 0x00, 0x00, 0x8c, 0x00, + 0x03, 0x8e, 0x16, 0x80, 0x8f, 0x12, 0xc0, 0x90, + 0x0e, 0x10, 0x91, 0x0c, 0xda, 0x92, 0x00, 0x05, + 0x93, 0x0b, 0xb8, 0x94, 0x0c, 0x80, 0x95, 0x00, + 0x05, 0x96, 0x01, 0x2c, 0x97, 0x00, 0x28, 0x98, + 0x01, 0x2c, 0x99, 0x00, 0x28, 0x9a, 0x00, 0x1e, + 0x9b, 0x0b, 0xb8, 0x9c, 0x00, 0x0a, 0x9d, 0x01, + 0x9e, 0x00, 0x64, 0x9f, 0x00, 0x50, 0xa0, 0x00, + 0x64, 0xa1, 0x00, 0x64, 0xa2, 0x00, 0x14, 0xa3, + 0x00, 0x46, 0xa4, 0x00, 0x46, 0xa5, 0x00, 0x00, + 0xa6, 0x00, 0x02, 0xa7, 0xff, 0xec, 0xa8, 0xff, + 0xf6, 0xa9, 0x10, 0xaa, 0x00, 0x00, 0x00, 0xe6, + 0xab, 0x01, 0xac, 0x01, 0xad, 0x04, 0x4d, 0xae, + 0x01, 0xaf, 0x00, 0xb0, 0x00, 0x0a, 0xb1, 0x14, + 0xb2, 0x32, 0x32, 0x31, 0x31, 0x38, 0x37, 0x00, + 0x00, 0x00, 0x00, 0xb3, 0x00, 0xb4, 0x62, 0x65, + 0x6b, 0x69, 0x00, 0x00, 0x00, 0x00, 0xb5, 0x32, + 0x33, 0x30, 0x36, 0xb6, 0x00, 0x01, 0x4a, 0xc3, + 0xb7, 0x31, 0x31, 0x2e, 0x58, 0x57, 0x5f, 0x53, + 0x31, 0x31, 0x2e, 0x32, 0x36, 0x32, 0x48, 0x5f, + 0xb8, 0x00, 0xb9, 0x00, 0x00, 0x00, 0xe6, 0xba, + 0x62, 0x65, 0x6b, 0x69, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x4a, 0x4b, 0x5f, 0x42, + 0x31, 0x41, 0x32, 0x34, 0x53, 0x31, 0x35, 0x50, + 0xc0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x68, 0x00, + 0x00, 0x53, 0xbb + }, + { + 0x4e, 0x57, 0x01, 0x21, 0x00, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x01, 0x79, 0x30, 0x01, 0x0c, 0xc0, + 0x02, 0x0c, 0xc1, 0x03, 0x0c, 0xc0, 0x04, 0x0c, + 0xc4, 0x05, 0x0c, 0xc4, 0x06, 0x0c, 0xc2, 0x07, + 0x0c, 0xc2, 0x08, 0x0c, 0xc1, 0x09, 0x0c, 0xba, + 0x0a, 0x0c, 0xc1, 0x0b, 0x0c, 0xc2, 0x0c, 0x0c, + 0xc2, 0x0d, 0x0c, 0xc2, 0x0e, 0x0c, 0xc4, 0x0f, + 0x0c, 0xc2, 0x10, 0x0c, 0xc1, 0x80, 0x00, 0x1b, + 0x81, 0x00, 0x1b, 0x82, 0x00, 0x1a, 0x83, 0x14, + 0x68, 0x84, 0x03, 0x70, 0x85, 0x3c, 0x86, 0x02, + 0x87, 0x00, 0x19, 0x89, 0x00, 0x00, 0x16, 0x86, + 0x8a, 0x00, 0x10, 0x8b, 0x00, 0x00, 0x8c, 0x00, + 0x07, 0x8e, 0x16, 0x80, 0x8f, 0x12, 0xc0, 0x90, + 0x0e, 0x10, 0x91, 0x0c, 0xda, 0x92, 0x00, 0x05, + 0x93, 0x0b, 0xb8, 0x94, 0x0c, 0x80, 0x95, 0x00, + 0x05, 0x96, 0x01, 0x2c, 0x97, 0x00, 0x28, 0x98, + 0x01, 0x2c, 0x99, 0x00, 0x28, 0x9a, 0x00, 0x1e, + 0x9b, 0x0b, 0xb8, 0x9c, 0x00, 0x0a, 0x9d, 0x01, + 0x9e, 0x00, 0x64, 0x9f, 0x00, 0x50, 0xa0, 0x00, + 0x64, 0xa1, 0x00, 0x64, 0xa2, 0x00, 0x14, 0xa3, + 0x00, 0x46, 0xa4, 0x00, 0x46, 0xa5, 0x00, 0x00, + 0xa6, 0x00, 0x02, 0xa7, 0xff, 0xec, 0xa8, 0xff, + 0xf6, 0xa9, 0x10, 0xaa, 0x00, 0x00, 0x00, 0xe6, + 0xab, 0x01, 0xac, 0x01, 0xad, 0x04, 0x4d, 0xae, + 0x01, 0xaf, 0x00, 0xb0, 0x00, 0x0a, 0xb1, 0x14, + 0xb2, 0x32, 0x32, 0x31, 0x31, 0x38, 0x37, 0x00, + 0x00, 0x00, 0x00, 0xb3, 0x00, 0xb4, 0x62, 0x65, + 0x6b, 0x69, 0x00, 0x00, 0x00, 0x00, 0xb5, 0x32, + 0x33, 0x30, 0x36, 0xb6, 0x00, 0x01, 0x7f, 0x2a, + 0xb7, 0x31, 0x31, 0x2e, 0x58, 0x57, 0x5f, 0x53, + 0x31, 0x31, 0x2e, 0x32, 0x36, 0x32, 0x48, 0x5f, + 0xb8, 0x00, 0xb9, 0x00, 0x00, 0x00, 0xe6, 0xba, + 0x62, 0x65, 0x6b, 0x69, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x4a, 0x4b, 0x5f, 0x42, + 0x31, 0x41, 0x32, 0x34, 0x53, 0x31, 0x35, 0x50, + 0xc0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x68, 0x00, + 0x00, 0x4f, 0xc1 + }, + { + 0x4e, 0x57, 0x01, 0x21, 0x00, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x01, 0x79, 0x30, 0x01, 0x0c, 0x13, + 0x02, 0x0c, 0x12, 0x03, 0x0c, 0x0f, 0x04, 0x0c, + 0x15, 0x05, 0x0c, 0x0d, 0x06, 0x0c, 0x13, 0x07, + 0x0c, 0x16, 0x08, 0x0c, 0x13, 0x09, 0x0b, 0xdb, + 0x0a, 0x0b, 0xf6, 0x0b, 0x0c, 0x17, 0x0c, 0x0b, + 0xf5, 0x0d, 0x0c, 0x16, 0x0e, 0x0c, 0x1a, 0x0f, + 0x0c, 0x1b, 0x10, 0x0c, 0x1c, 0x80, 0x00, 0x18, + 0x81, 0x00, 0x18, 0x82, 0x00, 0x18, 0x83, 0x13, + 0x49, 0x84, 0x00, 0x00, 0x85, 0x00, 0x86, 0x02, + 0x87, 0x00, 0x23, 0x89, 0x00, 0x00, 0x20, 0x14, + 0x8a, 0x00, 0x10, 0x8b, 0x00, 0x08, 0x8c, 0x00, + 0x05, 0x8e, 0x16, 0x80, 0x8f, 0x12, 0xc0, 0x90, + 0x0e, 0x10, 0x91, 0x0c, 0xda, 0x92, 0x00, 0x05, + 0x93, 0x0b, 0xb8, 0x94, 0x0c, 0x80, 0x95, 0x00, + 0x05, 0x96, 0x01, 0x2c, 0x97, 0x00, 0x28, 0x98, + 0x01, 0x2c, 0x99, 0x00, 0x28, 0x9a, 0x00, 0x1e, + 0x9b, 0x0b, 0xb8, 0x9c, 0x00, 0x0a, 0x9d, 0x01, + 0x9e, 0x00, 0x64, 0x9f, 0x00, 0x50, 0xa0, 0x00, + 0x64, 0xa1, 0x00, 0x64, 0xa2, 0x00, 0x14, 0xa3, + 0x00, 0x46, 0xa4, 0x00, 0x46, 0xa5, 0x00, 0x00, + 0xa6, 0x00, 0x02, 0xa7, 0xff, 0xec, 0xa8, 0xff, + 0xf6, 0xa9, 0x10, 0xaa, 0x00, 0x00, 0x00, 0xe6, + 0xab, 0x01, 0xac, 0x01, 0xad, 0x04, 0x4d, 0xae, + 0x01, 0xaf, 0x00, 0xb0, 0x00, 0x0a, 0xb1, 0x14, + 0xb2, 0x32, 0x32, 0x31, 0x31, 0x38, 0x37, 0x00, + 0x00, 0x00, 0x00, 0xb3, 0x00, 0xb4, 0x62, 0x65, + 0x6b, 0x69, 0x00, 0x00, 0x00, 0x00, 0xb5, 0x32, + 0x33, 0x30, 0x36, 0xb6, 0x00, 0x02, 0x17, 0x10, + 0xb7, 0x31, 0x31, 0x2e, 0x58, 0x57, 0x5f, 0x53, + 0x31, 0x31, 0x2e, 0x32, 0x36, 0x32, 0x48, 0x5f, + 0xb8, 0x00, 0xb9, 0x00, 0x00, 0x00, 0xe6, 0xba, + 0x62, 0x65, 0x6b, 0x69, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x4a, 0x4b, 0x5f, 0x42, + 0x31, 0x41, 0x32, 0x34, 0x53, 0x31, 0x35, 0x50, + 0xc0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x68, 0x00, + 0x00, 0x45, 0xce + }, + { + 0x4e, 0x57, 0x01, 0x21, 0x00, 0x00, 0x00, 0x00, + 0x06, 0x00, 0x01, 0x79, 0x30, 0x01, 0x0c, 0x07, + 0x02, 0x0c, 0x0a, 0x03, 0x0c, 0x0b, 0x04, 0x0c, + 0x08, 0x05, 0x0c, 0x05, 0x06, 0x0c, 0x0b, 0x07, + 0x0c, 0x07, 0x08, 0x0c, 0x0a, 0x09, 0x0c, 0x08, + 0x0a, 0x0c, 0x06, 0x0b, 0x0c, 0x0a, 0x0c, 0x0c, + 0x05, 0x0d, 0x0c, 0x0a, 0x0e, 0x0c, 0x0a, 0x0f, + 0x0c, 0x0a, 0x10, 0x0c, 0x0a, 0x80, 0x00, 0x06, + 0x81, 0x00, 0x03, 0x82, 0x00, 0x03, 0x83, 0x13, + 0x40, 0x84, 0x00, 0x00, 0x85, 0x29, 0x86, 0x02, + 0x87, 0x00, 0x01, 0x89, 0x00, 0x00, 0x01, 0x0a, + 0x8a, 0x00, 0x10, 0x8b, 0x02, 0x00, 0x8c, 0x00, + 0x02, 0x8e, 0x16, 0x80, 0x8f, 0x10, 0x40, 0x90, + 0x0e, 0x10, 0x91, 0x0d, 0xde, 0x92, 0x00, 0x05, + 0x93, 0x0a, 0x28, 0x94, 0x0a, 0x5a, 0x95, 0x00, + 0x05, 0x96, 0x01, 0x2c, 0x97, 0x00, 0x28, 0x98, + 0x01, 0x2c, 0x99, 0x00, 0x28, 0x9a, 0x00, 0x1e, + 0x9b, 0x0b, 0xb8, 0x9c, 0x00, 0x0a, 0x9d, 0x01, + 0x9e, 0x00, 0x5a, 0x9f, 0x00, 0x50, 0xa0, 0x00, + 0x64, 0xa1, 0x00, 0x64, 0xa2, 0x00, 0x14, 0xa3, + 0x00, 0x37, 0xa4, 0x00, 0x37, 0xa5, 0x00, 0x03, + 0xa6, 0x00, 0x05, 0xa7, 0xff, 0xec, 0xa8, 0xff, + 0xf6, 0xa9, 0x10, 0xaa, 0x00, 0x00, 0x00, 0xe6, + 0xab, 0x01, 0xac, 0x01, 0xad, 0x04, 0x4d, 0xae, + 0x01, 0xaf, 0x00, 0xb0, 0x00, 0x0a, 0xb1, 0x14, + 0xb2, 0x32, 0x32, 0x31, 0x31, 0x38, 0x37, 0x00, + 0x00, 0x00, 0x00, 0xb3, 0x00, 0xb4, 0x62, 0x65, + 0x6b, 0x69, 0x00, 0x00, 0x00, 0x00, 0xb5, 0x32, + 0x33, 0x30, 0x36, 0xb6, 0x00, 0x03, 0xb7, 0x2d, + 0xb7, 0x31, 0x31, 0x2e, 0x58, 0x57, 0x5f, 0x53, + 0x31, 0x31, 0x2e, 0x32, 0x36, 0x32, 0x48, 0x5f, + 0xb8, 0x00, 0xb9, 0x00, 0x00, 0x00, 0xe6, 0xba, + 0x62, 0x65, 0x6b, 0x69, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x4a, 0x4b, 0x5f, 0x42, + 0x31, 0x41, 0x32, 0x34, 0x53, 0x31, 0x35, 0x50, + 0xc0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x68, 0x00, + 0x00, 0x41, 0x7b + } + }; + size_t _msg_idx = 0; + size_t _byte_idx = 0; +}; + +} /* namespace JkBms */ diff --git a/include/MessageOutput.h b/include/MessageOutput.h index 6f2daa529..c68c116be 100644 --- a/include/MessageOutput.h +++ b/include/MessageOutput.h @@ -1,41 +1,41 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include - -class MessageOutputClass : public Print { -public: - MessageOutputClass(); - void init(Scheduler& scheduler); - size_t write(uint8_t c) override; - size_t write(const uint8_t* buffer, size_t size) override; - void register_ws_output(AsyncWebSocket* output); - -private: - void loop(); - - Task _loopTask; - - using message_t = std::vector; - - // we keep a buffer for every task and only write complete lines to the - // serial output and then move them to be pushed through the websocket. - // this way we prevent mangling of messages from different contexts. - std::unordered_map _task_messages; - std::queue _lines; - - AsyncWebSocket* _ws = nullptr; - - std::mutex _msgLock; - - void serialWrite(message_t const& m); -}; - -extern MessageOutputClass MessageOutput; +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +class MessageOutputClass : public Print { +public: + MessageOutputClass(); + void init(Scheduler& scheduler); + size_t write(uint8_t c) override; + size_t write(const uint8_t* buffer, size_t size) override; + void register_ws_output(AsyncWebSocket* output); + +private: + void loop(); + + Task _loopTask; + + using message_t = std::vector; + + // we keep a buffer for every task and only write complete lines to the + // serial output and then move them to be pushed through the websocket. + // this way we prevent mangling of messages from different contexts. + std::unordered_map _task_messages; + std::queue _lines; + + AsyncWebSocket* _ws = nullptr; + + std::mutex _msgLock; + + void serialWrite(message_t const& m); +}; + +extern MessageOutputClass MessageOutput; diff --git a/include/MqttBattery.h b/include/MqttBattery.h index 7948019e3..a230a9d43 100644 --- a/include/MqttBattery.h +++ b/include/MqttBattery.h @@ -19,9 +19,10 @@ class MqttBattery : public BatteryProvider { String _voltageTopic; std::shared_ptr _stats = std::make_shared(); - std::optional getFloat(std::string const& src, char const* topic); void onMqttMessageSoC(espMqttClientTypes::MessageProperties const& properties, - char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total); + char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total, + char const* jsonPath); void onMqttMessageVoltage(espMqttClientTypes::MessageProperties const& properties, - char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total); + char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total, + char const* jsonPath); }; diff --git a/include/MqttHandleHass.h b/include/MqttHandleHass.h index a76cb0c7b..1a914f934 100644 --- a/include/MqttHandleHass.h +++ b/include/MqttHandleHass.h @@ -56,6 +56,9 @@ class MqttHandleHassClass { void publishConfig(); void forceUpdate(); + static String getDtuUniqueId(); + static String getDtuUrl(); + private: void loop(); void publish(const String& subtopic, const String& payload); @@ -63,7 +66,7 @@ class MqttHandleHassClass { void publishDtuBinarySensor(const char* name, const char* device_class, const char* category, const char* payload_on, const char* payload_off, const char* subTopic = ""); void publishInverterField(std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel, const byteAssign_fieldDeviceClass_t fieldType, const bool clear = false); void publishInverterButton(std::shared_ptr inv, const char* caption, const char* icon, const char* category, const char* deviceClass, const char* subTopic, const char* payload); - 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 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, float step = 1.0); void publishInverterBinarySensor(std::shared_ptr inv, const char* caption, const char* subTopic, const char* payload_on, const char* payload_off); static void createInverterInfo(JsonDocument& doc, std::shared_ptr inv); @@ -71,9 +74,6 @@ class MqttHandleHassClass { 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(); - Task _loopTask; bool _wasConnected = false; diff --git a/include/MqttHandleHuawei.h b/include/MqttHandleHuawei.h index f518ed9d4..f7f6f4c20 100644 --- a/include/MqttHandleHuawei.h +++ b/include/MqttHandleHuawei.h @@ -1,44 +1,44 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -#pragma once - -#include "Configuration.h" -#include -#include -#include -#include -#include -#include - -class MqttHandleHuaweiClass { -public: - void init(Scheduler& scheduler); - -private: - void loop(); - - enum class Topic : unsigned { - LimitOnlineVoltage, - LimitOnlineCurrent, - LimitOfflineVoltage, - LimitOfflineCurrent, - Mode - }; - - void onMqttMessage(Topic t, - const espMqttClientTypes::MessageProperties& properties, - const char* topic, const uint8_t* payload, size_t len, - size_t index, size_t total); - - Task _loopTask; - - uint32_t _lastPublishStats; - uint32_t _lastPublish; - - // MQTT callbacks to process updates on subscribed topics are executed in - // the MQTT thread's context. we use this queue to switch processing the - // user requests into the main loop's context (TaskScheduler context). - mutable std::mutex _mqttMutex; - std::deque> _mqttCallbacks; -}; - +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "Configuration.h" +#include +#include +#include +#include +#include +#include + +class MqttHandleHuaweiClass { +public: + void init(Scheduler& scheduler); + +private: + void loop(); + + enum class Topic : unsigned { + LimitOnlineVoltage, + LimitOnlineCurrent, + LimitOfflineVoltage, + LimitOfflineCurrent, + Mode + }; + + void onMqttMessage(Topic t, + const espMqttClientTypes::MessageProperties& properties, + const char* topic, const uint8_t* payload, size_t len, + size_t index, size_t total); + + Task _loopTask; + + uint32_t _lastPublishStats; + uint32_t _lastPublish; + + // MQTT callbacks to process updates on subscribed topics are executed in + // the MQTT thread's context. we use this queue to switch processing the + // user requests into the main loop's context (TaskScheduler context). + mutable std::mutex _mqttMutex; + std::deque> _mqttCallbacks; +}; + extern MqttHandleHuaweiClass MqttHandleHuawei; \ No newline at end of file diff --git a/include/MqttHandleInverter.h b/include/MqttHandleInverter.h index 446f30afa..7c86a8098 100644 --- a/include/MqttHandleInverter.h +++ b/include/MqttHandleInverter.h @@ -13,6 +13,9 @@ class MqttHandleInverterClass { static String getTopic(std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId); + void subscribeTopics(); + void unsubscribeTopics(); + private: void loop(); void publishField(std::shared_ptr inv, const ChannelType_t type, const ChannelNum_t channel, const FieldId_t fieldId); diff --git a/include/MqttHandlePowerLimiter.h b/include/MqttHandlePowerLimiter.h index fa7ef12cc..12f2a714c 100644 --- a/include/MqttHandlePowerLimiter.h +++ b/include/MqttHandlePowerLimiter.h @@ -1,43 +1,45 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -#pragma once - -#include "Configuration.h" -#include -#include -#include -#include -#include - -class MqttHandlePowerLimiterClass { -public: - void init(Scheduler& scheduler); - -private: - void loop(); - - enum class MqttPowerLimiterCommand : unsigned { - Mode, - BatterySoCStartThreshold, - BatterySoCStopThreshold, - FullSolarPassthroughSoC, - VoltageStartThreshold, - VoltageStopThreshold, - FullSolarPassThroughStartVoltage, - FullSolarPassThroughStopVoltage - }; - - void onMqttCmd(MqttPowerLimiterCommand command, const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total); - - Task _loopTask; - - uint32_t _lastPublishStats; - uint32_t _lastPublish; - - // MQTT callbacks to process updates on subscribed topics are executed in - // the MQTT thread's context. we use this queue to switch processing the - // user requests into the main loop's context (TaskScheduler context). - mutable std::mutex _mqttMutex; - std::deque> _mqttCallbacks; -}; - -extern MqttHandlePowerLimiterClass MqttHandlePowerLimiter; +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "Configuration.h" +#include +#include +#include +#include +#include + +class MqttHandlePowerLimiterClass { +public: + void init(Scheduler& scheduler); + +private: + void loop(); + + enum class MqttPowerLimiterCommand : unsigned { + Mode, + BatterySoCStartThreshold, + BatterySoCStopThreshold, + FullSolarPassthroughSoC, + VoltageStartThreshold, + VoltageStopThreshold, + FullSolarPassThroughStartVoltage, + FullSolarPassThroughStopVoltage, + UpperPowerLimit, + TargetPowerConsumption + }; + + void onMqttCmd(MqttPowerLimiterCommand command, const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total); + + Task _loopTask; + + uint32_t _lastPublishStats; + uint32_t _lastPublish; + + // MQTT callbacks to process updates on subscribed topics are executed in + // the MQTT thread's context. we use this queue to switch processing the + // user requests into the main loop's context (TaskScheduler context). + mutable std::mutex _mqttMutex; + std::deque> _mqttCallbacks; +}; + +extern MqttHandlePowerLimiterClass MqttHandlePowerLimiter; diff --git a/include/MqttHandlePowerLimiterHass.h b/include/MqttHandlePowerLimiterHass.h index f465994c6..45bd80299 100644 --- a/include/MqttHandlePowerLimiterHass.h +++ b/include/MqttHandlePowerLimiterHass.h @@ -13,9 +13,10 @@ class MqttHandlePowerLimiterHassClass { private: void loop(); void publish(const String& subtopic, const String& payload); - void publishNumber(const char* caption, const char* icon, const char* category, const char* commandTopic, const char* stateTopic, const char* unitOfMeasure, const int16_t min, const int16_t max); + void publishNumber(const char* caption, const char* icon, const char* category, const char* commandTopic, const char* stateTopic, const char* unitOfMeasure, const int16_t min, const int16_t max, const float step); void publishSelect(const char* caption, const char* icon, const char* category, const char* commandTopic, const char* stateTopic); - void createDeviceInfo(JsonObject& object); + void publishBinarySensor(const char* caption, const char* icon, const char* stateTopic, const char* payload_on, const char* payload_off); + void createDeviceInfo(JsonDocument& root); Task _loopTask; diff --git a/include/MqttSettings.h b/include/MqttSettings.h index 875385684..add6a690b 100644 --- a/include/MqttSettings.h +++ b/include/MqttSettings.h @@ -21,6 +21,7 @@ class MqttSettingsClass { void unsubscribe(const String& topic); String getPrefix() const; + String getClientId(); private: void NetworkEvent(network_event event); @@ -41,4 +42,4 @@ class MqttSettingsClass { bool _verboseLogging = true; }; -extern MqttSettingsClass MqttSettings; \ No newline at end of file +extern MqttSettingsClass MqttSettings; diff --git a/include/PowerLimiter.h b/include/PowerLimiter.h index c5727fe5c..db69a303b 100644 --- a/include/PowerLimiter.h +++ b/include/PowerLimiter.h @@ -44,6 +44,7 @@ class PowerLimiterClass { uint8_t getInverterUpdateTimeouts() const { return _inverterUpdateTimeouts; } uint8_t getPowerLimiterState(); int32_t getLastRequestedPowerLimit() { return _lastRequestedPowerLimit; } + bool getFullSolarPassThroughEnabled() const { return _fullSolarPassThroughEnabled; } enum class Mode : unsigned { Normal = 0, @@ -74,6 +75,7 @@ class PowerLimiterClass { Mode _mode = Mode::Normal; std::shared_ptr _inverter = nullptr; bool _batteryDischargeEnabled = false; + bool _nighttimeDischarging = false; uint32_t _nextInverterRestart = 0; // Values: 0->not calculated / 1->no restart configured / >1->time of next inverter restart in millis() uint32_t _nextCalculateCheck = 5000; // time in millis for next NTP check to calulate restart bool _fullSolarPassThroughEnabled = false; diff --git a/include/PowerMeter.h b/include/PowerMeter.h index 5b3d8f31f..44c99d062 100644 --- a/include/PowerMeter.h +++ b/include/PowerMeter.h @@ -1,78 +1,27 @@ // SPDX-License-Identifier: GPL-2.0-or-later #pragma once -#include "Configuration.h" -#include -#include -#include -#include -#include -#include "SDM.h" -#include "sml.h" +#include "PowerMeterProvider.h" #include -#include - -typedef struct { - const unsigned char OBIS[6]; - void (*Fn)(double&); - float* Arg; -} OBISHandler; +#include +#include class PowerMeterClass { public: - enum class Source : unsigned { - MQTT = 0, - SDM1PH = 1, - SDM3PH = 2, - HTTP = 3, - SML = 4, - SMAHM2 = 5 - }; void init(Scheduler& scheduler); - float getPowerTotal(bool forceUpdate = true); - uint32_t getLastPowerMeterUpdate(); - bool isDataValid(); + + void updateSettings(); + + float getPowerTotal() const; + uint32_t getLastUpdate() const; + bool isDataValid() const; private: void loop(); - void mqtt(); - - void onMqttMessage(const espMqttClientTypes::MessageProperties& properties, - const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total); Task _loopTask; - - bool _verboseLogging = true; - uint32_t _lastPowerMeterCheck; - // Used in Power limiter for safety check - uint32_t _lastPowerMeterUpdate; - - float _powerMeter1Power = 0.0; - float _powerMeter2Power = 0.0; - float _powerMeter3Power = 0.0; - float _powerMeter1Voltage = 0.0; - float _powerMeter2Voltage = 0.0; - float _powerMeter3Voltage = 0.0; - float _powerMeterImport = 0.0; - float _powerMeterExport = 0.0; - - std::map _mqttSubscriptions; - mutable std::mutex _mutex; - - static char constexpr _sdmSerialPortOwner[] = "SDM power meter"; - std::unique_ptr _upSdmSerial = nullptr; - std::unique_ptr _upSdm = nullptr; - std::unique_ptr _upSmlSerial = nullptr; - - void readPowerMeter(); - - bool smlReadLoop(); - const std::list smlHandlerList{ - {{0x01, 0x00, 0x10, 0x07, 0x00, 0xff}, &smlOBISW, &_powerMeter1Power}, - {{0x01, 0x00, 0x01, 0x08, 0x00, 0xff}, &smlOBISWh, &_powerMeterImport}, - {{0x01, 0x00, 0x02, 0x08, 0x00, 0xff}, &smlOBISWh, &_powerMeterExport} - }; + std::unique_ptr _upProvider = nullptr; }; extern PowerMeterClass PowerMeter; diff --git a/include/PowerMeterHttpJson.h b/include/PowerMeterHttpJson.h new file mode 100644 index 000000000..a6c97a4c3 --- /dev/null +++ b/include/PowerMeterHttpJson.h @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include "HttpGetter.h" +#include "Configuration.h" +#include "PowerMeterProvider.h" + +using Auth_t = HttpRequestConfig::Auth; +using Unit_t = PowerMeterHttpJsonValue::Unit; + +class PowerMeterHttpJson : public PowerMeterProvider { +public: + explicit PowerMeterHttpJson(PowerMeterHttpJsonConfig const& cfg) + : _cfg(cfg) { } + + ~PowerMeterHttpJson(); + + bool init() final; + void loop() final; + float getPowerTotal() const final; + bool isDataValid() const final; + void doMqttPublish() const final; + + using power_values_t = std::array; + using poll_result_t = std::variant; + poll_result_t poll(); + +private: + static void pollingLoopHelper(void* context); + std::atomic _taskDone; + void pollingLoop(); + + PowerMeterHttpJsonConfig const _cfg; + + uint32_t _lastPoll = 0; + + mutable std::mutex _valueMutex; + power_values_t _powerValues; + + std::array, POWERMETER_HTTP_JSON_MAX_VALUES> _httpGetters; + + TaskHandle_t _taskHandle = nullptr; + bool _stopPolling; + mutable std::mutex _pollingMutex; + std::condition_variable _cv; +}; diff --git a/include/PowerMeterHttpSml.h b/include/PowerMeterHttpSml.h new file mode 100644 index 000000000..9c49b639c --- /dev/null +++ b/include/PowerMeterHttpSml.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include +#include +#include +#include +#include +#include "HttpGetter.h" +#include "Configuration.h" +#include "PowerMeterSml.h" + +class PowerMeterHttpSml : public PowerMeterSml { +public: + explicit PowerMeterHttpSml(PowerMeterHttpSmlConfig const& cfg) + : PowerMeterSml("PowerMeterHttpSml") + , _cfg(cfg) { } + + ~PowerMeterHttpSml(); + + bool init() final; + void loop() final; + bool isDataValid() const final; + + // returns an empty string on success, + // returns an error message otherwise. + String poll(); + +private: + static void pollingLoopHelper(void* context); + std::atomic _taskDone; + void pollingLoop(); + + PowerMeterHttpSmlConfig const _cfg; + + uint32_t _lastPoll = 0; + + std::unique_ptr _upHttpGetter; + + TaskHandle_t _taskHandle = nullptr; + bool _stopPolling; + mutable std::mutex _pollingMutex; + std::condition_variable _cv; +}; diff --git a/include/PowerMeterMqtt.h b/include/PowerMeterMqtt.h new file mode 100644 index 000000000..e63309f67 --- /dev/null +++ b/include/PowerMeterMqtt.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "Configuration.h" +#include "PowerMeterProvider.h" +#include +#include +#include +#include + +class PowerMeterMqtt : public PowerMeterProvider { +public: + explicit PowerMeterMqtt(PowerMeterMqttConfig const& cfg) + : _cfg(cfg) { } + + ~PowerMeterMqtt(); + + bool init() final; + void loop() final { } + float getPowerTotal() const final; + void doMqttPublish() const final; + +private: + using MsgProperties = espMqttClientTypes::MessageProperties; + void onMessage(MsgProperties const& properties, char const* topic, + uint8_t const* payload, size_t len, size_t index, + size_t total, float* targetVariable, PowerMeterMqttValue const* cfg); + + PowerMeterMqttConfig const _cfg; + + using power_values_t = std::array; + power_values_t _powerValues; + + std::vector _mqttSubscriptions; + + mutable std::mutex _mutex; +}; diff --git a/include/PowerMeterProvider.h b/include/PowerMeterProvider.h new file mode 100644 index 000000000..0ca7bcdf5 --- /dev/null +++ b/include/PowerMeterProvider.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include "Configuration.h" + +class PowerMeterProvider { +public: + virtual ~PowerMeterProvider() { } + + enum class Type : unsigned { + MQTT = 0, + SDM1PH = 1, + SDM3PH = 2, + HTTP_JSON = 3, + SERIAL_SML = 4, + SMAHM2 = 5, + HTTP_SML = 6 + }; + + // returns true if the provider is ready for use, false otherwise + virtual bool init() = 0; + + virtual void loop() = 0; + virtual float getPowerTotal() const = 0; + virtual bool isDataValid() const; + + uint32_t getLastUpdate() const { return _lastUpdate; } + void mqttLoop() const; + +protected: + PowerMeterProvider() { + auto const& config = Configuration.get(); + _verboseLogging = config.PowerMeter.VerboseLogging; + } + + void gotUpdate() { _lastUpdate = millis(); } + + void mqttPublish(String const& topic, float const& value) const; + + bool _verboseLogging; + +private: + virtual void doMqttPublish() const = 0; + + // gotUpdate() updates this variable potentially from a different thread + // than users that request to read this variable through getLastUpdate(). + std::atomic _lastUpdate = 0; + + mutable uint32_t _lastMqttPublish = 0; +}; diff --git a/include/PowerMeterSerialSdm.h b/include/PowerMeterSerialSdm.h new file mode 100644 index 000000000..d676fcbbd --- /dev/null +++ b/include/PowerMeterSerialSdm.h @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include +#include +#include +#include "Configuration.h" +#include "PowerMeterProvider.h" +#include "SDM.h" + +class PowerMeterSerialSdm : public PowerMeterProvider { +public: + enum class Phases { + One, + Three + }; + + PowerMeterSerialSdm(Phases phases, PowerMeterSerialSdmConfig const& cfg) + : _phases(phases) + , _cfg(cfg) { } + + ~PowerMeterSerialSdm(); + + bool init() final; + void loop() final; + float getPowerTotal() const final; + bool isDataValid() const final; + void doMqttPublish() const final; + +private: + static void pollingLoopHelper(void* context); + bool readValue(std::unique_lock& lock, uint16_t reg, float& targetVar); + std::atomic _taskDone; + void pollingLoop(); + + Phases _phases; + PowerMeterSerialSdmConfig const _cfg; + + uint32_t _lastPoll = 0; + + float _phase1Power = 0.0; + float _phase2Power = 0.0; + float _phase3Power = 0.0; + float _phase1Voltage = 0.0; + float _phase2Voltage = 0.0; + float _phase3Voltage = 0.0; + float _energyImport = 0.0; + float _energyExport = 0.0; + + mutable std::mutex _valueMutex; + + std::unique_ptr _upSdmSerial = nullptr; + std::unique_ptr _upSdm = nullptr; + + TaskHandle_t _taskHandle = nullptr; + bool _stopPolling; + mutable std::mutex _pollingMutex; + std::condition_variable _cv; +}; diff --git a/include/PowerMeterSerialSml.h b/include/PowerMeterSerialSml.h new file mode 100644 index 000000000..abe595332 --- /dev/null +++ b/include/PowerMeterSerialSml.h @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "PowerMeterSml.h" +#include + +class PowerMeterSerialSml : public PowerMeterSml { +public: + PowerMeterSerialSml() + : PowerMeterSml("PowerMeterSerialSml") { } + + ~PowerMeterSerialSml(); + + bool init() final; + void loop() final; + +private: + // we assume that an SML datagram is complete after no additional + // characters were received for this many milliseconds. + static uint8_t constexpr _datagramGapMillis = 50; + + static uint32_t constexpr _baud = 9600; + + // size in bytes of the software serial receive buffer. must have the + // capacity to hold a full SML datagram, as we are processing the datagrams + // only after all data of one datagram was received. + static int constexpr _bufCapacity = 1024; // memory usage: 1 byte each + + // amount of bits (RX pin state transitions) the software serial can buffer + // without decoding bits to bytes and storing those in the receive buffer. + // this value dictates how ofter we need to call a function of the software + // serial instance that performs bit decoding (we call available()). + static int constexpr _isrCapacity = 256; // memory usage: 8 bytes each (timestamp + pointer) + + static void pollingLoopHelper(void* context); + std::atomic _taskDone; + void pollingLoop(); + + TaskHandle_t _taskHandle = nullptr; + bool _stopPolling; + mutable std::mutex _pollingMutex; + + std::unique_ptr _upSmlSerial = nullptr; +}; diff --git a/include/PowerMeterSml.h b/include/PowerMeterSml.h new file mode 100644 index 000000000..79e37ab9a --- /dev/null +++ b/include/PowerMeterSml.h @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include +#include +#include +#include +#include +#include "Configuration.h" +#include "PowerMeterProvider.h" +#include "sml.h" + +class PowerMeterSml : public PowerMeterProvider { +public: + float getPowerTotal() const final; + void doMqttPublish() const final; + +protected: + explicit PowerMeterSml(char const* user) + : _user(user) { } + + void reset(); + void processSmlByte(uint8_t byte); + +private: + std::string _user; + mutable std::mutex _mutex; + + using values_t = struct { + std::optional activePowerTotal = std::nullopt; + std::optional activePowerL1 = std::nullopt; + std::optional activePowerL2 = std::nullopt; + std::optional activePowerL3 = std::nullopt; + std::optional voltageL1 = std::nullopt; + std::optional voltageL2 = std::nullopt; + std::optional voltageL3 = std::nullopt; + std::optional currentL1 = std::nullopt; + std::optional currentL2 = std::nullopt; + std::optional currentL3 = std::nullopt; + std::optional energyImport = std::nullopt; + std::optional energyExport = std::nullopt; + }; + + values_t _values; + values_t _cache; + + using OBISHandler = struct { + uint8_t const OBIS[6]; + void (*decoder)(float&); + std::optional* target; + char const* name; + }; + + const std::list smlHandlerList{ + {{0x01, 0x00, 0x10, 0x07, 0x00, 0xff}, &smlOBISW, &_cache.activePowerTotal, "active power total"}, + {{0x01, 0x00, 0x24, 0x07, 0x00, 0xff}, &smlOBISW, &_cache.activePowerL1, "active power L1"}, + {{0x01, 0x00, 0x38, 0x07, 0x00, 0xff}, &smlOBISW, &_cache.activePowerL2, "active power L2"}, + {{0x01, 0x00, 0x4c, 0x07, 0x00, 0xff}, &smlOBISW, &_cache.activePowerL3, "active power L3"}, + {{0x01, 0x00, 0x20, 0x07, 0x00, 0xff}, &smlOBISVolt, &_cache.voltageL1, "voltage L1"}, + {{0x01, 0x00, 0x34, 0x07, 0x00, 0xff}, &smlOBISVolt, &_cache.voltageL2, "voltage L2"}, + {{0x01, 0x00, 0x48, 0x07, 0x00, 0xff}, &smlOBISVolt, &_cache.voltageL3, "voltage L3"}, + {{0x01, 0x00, 0x1f, 0x07, 0x00, 0xff}, &smlOBISAmpere, &_cache.currentL1, "current L1"}, + {{0x01, 0x00, 0x33, 0x07, 0x00, 0xff}, &smlOBISAmpere, &_cache.currentL2, "current L2"}, + {{0x01, 0x00, 0x47, 0x07, 0x00, 0xff}, &smlOBISAmpere, &_cache.currentL3, "current L3"}, + {{0x01, 0x00, 0x01, 0x08, 0x00, 0xff}, &smlOBISWh, &_cache.energyImport, "energy import"}, + {{0x01, 0x00, 0x02, 0x08, 0x00, 0xff}, &smlOBISWh, &_cache.energyExport, "energy export"} + }; +}; diff --git a/include/SMA_HM.h b/include/PowerMeterUdpSmaHomeManager.h similarity index 53% rename from include/SMA_HM.h rename to include/PowerMeterUdpSmaHomeManager.h index e5600902b..5d4b3a8d3 100644 --- a/include/SMA_HM.h +++ b/include/PowerMeterUdpSmaHomeManager.h @@ -5,17 +5,16 @@ #pragma once #include -#include +#include "PowerMeterProvider.h" -class SMA_HMClass { +class PowerMeterUdpSmaHomeManager : public PowerMeterProvider { public: - void init(Scheduler& scheduler, bool verboseLogging); - void loop(); - void event1(); - float getPowerTotal() const { return _powerMeterPower; } - float getPowerL1() const { return _powerMeterL1; } - float getPowerL2() const { return _powerMeterL2; } - float getPowerL3() const { return _powerMeterL3; } + ~PowerMeterUdpSmaHomeManager(); + + bool init() final; + void loop() final; + float getPowerTotal() const final { return _powerMeterPower; } + void doMqttPublish() const final; private: void Soutput(int kanal, int index, int art, int tarif, @@ -23,14 +22,10 @@ class SMA_HMClass { uint8_t* decodeGroup(uint8_t* offset, uint16_t grouplen); - bool _verboseLogging = false; float _powerMeterPower = 0.0; float _powerMeterL1 = 0.0; float _powerMeterL2 = 0.0; float _powerMeterL3 = 0.0; uint32_t _previousMillis = 0; uint32_t _serial = 0; - Task _loopTask; }; - -extern SMA_HMClass SMA_HM; diff --git a/include/PylontechCanReceiver.h b/include/PylontechCanReceiver.h index 2b2b922d9..24bae8c06 100644 --- a/include/PylontechCanReceiver.h +++ b/include/PylontechCanReceiver.h @@ -3,27 +3,20 @@ #include "Configuration.h" #include "Battery.h" -#include +#include "BatteryCanReceiver.h" #include #include -#include -class PylontechCanReceiver : public BatteryProvider { +class PylontechCanReceiver : public BatteryCanReceiver { public: bool init(bool verboseLogging) final; - void deinit() final; - void loop() final; + void onMessage(twai_message_t rx_message) final; + std::shared_ptr getStats() const final { return _stats; } private: - uint16_t readUnsignedInt16(uint8_t *data); - int16_t readSignedInt16(uint8_t *data); - float scaleValue(int16_t value, float factor); - bool getBit(uint8_t value, uint8_t bit); - void dummyData(); - bool _verboseLogging = true; std::shared_ptr _stats = std::make_shared(); }; diff --git a/include/PytesCanReceiver.h b/include/PytesCanReceiver.h new file mode 100644 index 000000000..aaeb9d66d --- /dev/null +++ b/include/PytesCanReceiver.h @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "Configuration.h" +#include "Battery.h" +#include "BatteryCanReceiver.h" +#include + +class PytesCanReceiver : public BatteryCanReceiver { +public: + bool init(bool verboseLogging) final; + void onMessage(twai_message_t rx_message) final; + + std::shared_ptr getStats() const final { return _stats; } + +private: + std::shared_ptr _stats = + std::make_shared(); +}; diff --git a/include/Utils.h b/include/Utils.h index f81e73180..a6bc3b15e 100644 --- a/include/Utils.h +++ b/include/Utils.h @@ -3,6 +3,7 @@ #include #include +#include class Utils { public: @@ -12,4 +13,12 @@ class Utils { static void restartDtu(); static bool checkJsonAlloc(const JsonDocument& doc, const char* function, const uint16_t line); static void removeAllFiles(); + + /* OpenDTU-OnBatter-specific utils go here: */ + template + static std::pair getJsonValueByPath(JsonDocument const& root, String const& path); + + template + static std::optional getNumericValueFromMqttPayload(char const* client, + std::string const& src, char const* topic, char const* jsonPath); }; diff --git a/include/WebApi_Huawei.h b/include/WebApi_Huawei.h index 57d00ed83..5cda423b8 100644 --- a/include/WebApi_Huawei.h +++ b/include/WebApi_Huawei.h @@ -1,19 +1,19 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -#pragma once - -#include -#include -#include - -class WebApiHuaweiClass { -public: - void init(AsyncWebServer& server, Scheduler& scheduler); - void getJsonData(JsonVariant& root); -private: - void onStatus(AsyncWebServerRequest* request); - void onAdminGet(AsyncWebServerRequest* request); - void onAdminPost(AsyncWebServerRequest* request); - void onPost(AsyncWebServerRequest* request); - - AsyncWebServer* _server; +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include +#include +#include + +class WebApiHuaweiClass { +public: + void init(AsyncWebServer& server, Scheduler& scheduler); + void getJsonData(JsonVariant& root); +private: + void onStatus(AsyncWebServerRequest* request); + void onAdminGet(AsyncWebServerRequest* request); + void onAdminPost(AsyncWebServerRequest* request); + void onPost(AsyncWebServerRequest* request); + + AsyncWebServer* _server; }; \ No newline at end of file diff --git a/include/WebApi_errors.h b/include/WebApi_errors.h index 97d61b220..0da8d3d9f 100644 --- a/include/WebApi_errors.h +++ b/include/WebApi_errors.h @@ -60,6 +60,7 @@ enum WebApiError { MqttHassTopicLength, MqttHassTopicCharacter, MqttLwtQos, + MqttClientIdLength, NetworkBase = 8000, NetworkIpInvalid, diff --git a/include/WebApi_firmware.h b/include/WebApi_firmware.h index 990a5e064..d4822afa3 100644 --- a/include/WebApi_firmware.h +++ b/include/WebApi_firmware.h @@ -9,6 +9,9 @@ class WebApiFirmwareClass { void init(AsyncWebServer& server, Scheduler& scheduler); private: + bool otaSupported() const; + void onFirmwareUpdateFinish(AsyncWebServerRequest* request); void onFirmwareUpdateUpload(AsyncWebServerRequest* request, String filename, size_t index, uint8_t* data, size_t len, bool final); + void onFirmwareStatus(AsyncWebServerRequest* request); }; diff --git a/include/WebApi_powermeter.h b/include/WebApi_powermeter.h index 7e873b1c1..3cfe2a2dc 100644 --- a/include/WebApi_powermeter.h +++ b/include/WebApi_powermeter.h @@ -14,8 +14,8 @@ 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); + void onTestHttpJsonRequest(AsyncWebServerRequest* request); + void onTestHttpSmlRequest(AsyncWebServerRequest* request); AsyncWebServer* _server; }; diff --git a/include/WebApi_ws_Huawei.h b/include/WebApi_ws_Huawei.h index 38025bb61..43e528e83 100644 --- a/include/WebApi_ws_Huawei.h +++ b/include/WebApi_ws_Huawei.h @@ -1,29 +1,29 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -#pragma once - -#include "ArduinoJson.h" -#include -#include -#include - -class WebApiWsHuaweiLiveClass { -public: - WebApiWsHuaweiLiveClass(); - void init(AsyncWebServer& server, Scheduler& scheduler); - -private: - void generateCommonJsonResponse(JsonVariant& root); - void onLivedataStatus(AsyncWebServerRequest* request); - void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len); - - AsyncWebServer* _server; - AsyncWebSocket _ws; - - std::mutex _mutex; - - Task _wsCleanupTask; - void wsCleanupTaskCb(); - - Task _sendDataTask; - void sendDataTaskCb(); +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "ArduinoJson.h" +#include +#include +#include + +class WebApiWsHuaweiLiveClass { +public: + WebApiWsHuaweiLiveClass(); + void init(AsyncWebServer& server, Scheduler& scheduler); + +private: + void generateCommonJsonResponse(JsonVariant& root); + void onLivedataStatus(AsyncWebServerRequest* request); + void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len); + + AsyncWebServer* _server; + AsyncWebSocket _ws; + + std::mutex _mutex; + + Task _wsCleanupTask; + void wsCleanupTaskCb(); + + Task _sendDataTask; + void sendDataTaskCb(); }; \ No newline at end of file diff --git a/include/WebApi_ws_battery.h b/include/WebApi_ws_battery.h index 17c63d42f..d89e01aec 100644 --- a/include/WebApi_ws_battery.h +++ b/include/WebApi_ws_battery.h @@ -1,32 +1,32 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -#pragma once - -#include "ArduinoJson.h" -#include -#include -#include - -class WebApiWsBatteryLiveClass { -public: - WebApiWsBatteryLiveClass(); - void init(AsyncWebServer& server, Scheduler& scheduler); - -private: - void generateCommonJsonResponse(JsonVariant& root); - void onLivedataStatus(AsyncWebServerRequest* request); - void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len); - - AsyncWebServer* _server; - AsyncWebSocket _ws; - - uint32_t _lastUpdateCheck = 0; - static constexpr uint16_t _responseSize = 1024 + 512; - - std::mutex _mutex; - - Task _wsCleanupTask; - void wsCleanupTaskCb(); - - Task _sendDataTask; - void sendDataTaskCb(); +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include "ArduinoJson.h" +#include +#include +#include + +class WebApiWsBatteryLiveClass { +public: + WebApiWsBatteryLiveClass(); + void init(AsyncWebServer& server, Scheduler& scheduler); + +private: + void generateCommonJsonResponse(JsonVariant& root); + void onLivedataStatus(AsyncWebServerRequest* request); + void onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len); + + AsyncWebServer* _server; + AsyncWebSocket _ws; + + uint32_t _lastUpdateCheck = 0; + static constexpr uint16_t _responseSize = 1024 + 512; + + std::mutex _mutex; + + Task _wsCleanupTask; + void wsCleanupTaskCb(); + + Task _sendDataTask; + void sendDataTaskCb(); }; \ No newline at end of file diff --git a/include/defaults.h b/include/defaults.h index 237a94b06..e348ae63f 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -9,7 +9,7 @@ #define ACCESS_POINT_NAME "OpenDTU-" #define ACCESS_POINT_PASSWORD "openDTU42" -#define ACCESS_POINT_TIMEOUT 3; +#define ACCESS_POINT_TIMEOUT 3 #define AUTH_USERNAME "admin" #define SECURITY_ALLOW_READONLY true @@ -115,11 +115,12 @@ #define VEDIRECT_UPDATESONLY true #define POWERMETER_ENABLED false -#define POWERMETER_INTERVAL 10 -#define POWERMETER_SOURCE 2 -#define POWERMETER_SDMBAUDRATE 9600 +#define POWERMETER_POLLING_INTERVAL 10 +#define POWERMETER_SOURCE 0 #define POWERMETER_SDMADDRESS 1 +#define HTTP_REQUEST_TIMEOUT_MS 1000 + #define POWERLIMITER_ENABLED false #define POWERLIMITER_SOLAR_PASSTHROUGH_ENABLED true #define POWERLIMITER_SOLAR_PASSTHROUGH_LOSSES 3 @@ -127,6 +128,7 @@ #define POWERLIMITER_INTERVAL 10 #define POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER true #define POWERLIMITER_IS_INVERTER_SOLAR_POWERED false +#define POWERLIMITER_USE_OVERSCALING_TO_COMPENSATE_SHADING false #define POWERLIMITER_INVERTER_ID 0ULL #define POWERLIMITER_INVERTER_CHANNEL_ID 0 #define POWERLIMITER_TARGET_POWER_CONSUMPTION 0 diff --git a/lib/CpuTemperature/src/CpuTemperature.cpp b/lib/CpuTemperature/src/CpuTemperature.cpp new file mode 100644 index 000000000..60e3fc7b4 --- /dev/null +++ b/lib/CpuTemperature/src/CpuTemperature.cpp @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2024 Thomas Basler and others + */ + +#include "CpuTemperature.h" +#include + +#if defined(CONFIG_IDF_TARGET_ESP32) +// there is no official API available on the original ESP32 +extern "C" { +uint8_t temprature_sens_read(); +} +#elif defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) +#include "driver/temp_sensor.h" +#endif + +CpuTemperatureClass CpuTemperature; + +float CpuTemperatureClass::read() +{ + std::lock_guard lock(_mutex); + + float temperature = NAN; + bool success = false; + +#if defined(CONFIG_IDF_TARGET_ESP32) + uint8_t raw = temprature_sens_read(); + ESP_LOGV(TAG, "Raw temperature value: %d", raw); + temperature = (raw - 32) / 1.8f; + success = (raw != 128); +#elif defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32S3) + temp_sensor_config_t tsens = TSENS_CONFIG_DEFAULT(); + temp_sensor_set_config(tsens); + temp_sensor_start(); +#if defined(CONFIG_IDF_TARGET_ESP32S3) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(4, 4, 3)) +#error \ + "ESP32-S3 internal temperature sensor requires ESP IDF V4.4.3 or higher. See https://github.com/esphome/issues/issues/4271" +#endif + esp_err_t result = temp_sensor_read_celsius(&temperature); + temp_sensor_stop(); + success = (result == ESP_OK); +#endif + + if (success && std::isfinite(temperature)) { + return temperature; + } else { + ESP_LOGD(TAG, "Ignoring invalid temperature (success=%d, value=%.1f)", success, temperature); + return NAN; + } +} diff --git a/lib/CpuTemperature/src/CpuTemperature.h b/lib/CpuTemperature/src/CpuTemperature.h new file mode 100644 index 000000000..06199c825 --- /dev/null +++ b/lib/CpuTemperature/src/CpuTemperature.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#pragma once + +#include + +class CpuTemperatureClass { +public: + float read(); + +private: + std::mutex _mutex; +}; + +extern CpuTemperatureClass CpuTemperature; diff --git a/lib/Hoymiles/src/Hoymiles.cpp b/lib/Hoymiles/src/Hoymiles.cpp index 384276d58..67fe497c4 100644 --- a/lib/Hoymiles/src/Hoymiles.cpp +++ b/lib/Hoymiles/src/Hoymiles.cpp @@ -141,6 +141,9 @@ void HoymilesClass::loop() if (inv->getZeroYieldDayOnMidnight()) { inv->Statistics()->zeroDailyData(); } + if (inv->getClearEventlogOnMidnight()) { + inv->EventLog()->clearBuffer(); + } } lastWeekDay = currentWeekDay; diff --git a/lib/Hoymiles/src/HoymilesRadio.h b/lib/Hoymiles/src/HoymilesRadio.h index cb2a947cd..296b479bb 100644 --- a/lib/Hoymiles/src/HoymilesRadio.h +++ b/lib/Hoymiles/src/HoymilesRadio.h @@ -22,9 +22,9 @@ class HoymilesRadio { } template - std::shared_ptr prepareCommand() + std::shared_ptr prepareCommand(InverterAbstract* inv) { - return std::make_shared(); + return std::make_shared(inv); } protected: diff --git a/lib/Hoymiles/src/commands/ActivePowerControlCommand.cpp b/lib/Hoymiles/src/commands/ActivePowerControlCommand.cpp index 95af23cfc..dcd2370c1 100644 --- a/lib/Hoymiles/src/commands/ActivePowerControlCommand.cpp +++ b/lib/Hoymiles/src/commands/ActivePowerControlCommand.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022-2023 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ /* @@ -25,8 +25,8 @@ ID Target Addr Source Addr Cmd SCmd ? Limit Type CRC16 CRC8 #define CRC_SIZE 6 -ActivePowerControlCommand::ActivePowerControlCommand(const uint64_t target_address, const uint64_t router_address) - : DevControlCommand(target_address, router_address) +ActivePowerControlCommand::ActivePowerControlCommand(InverterAbstract* inv, const uint64_t router_address) + : DevControlCommand(inv, router_address) { _payload[10] = 0x0b; _payload[11] = 0x00; @@ -62,30 +62,30 @@ void ActivePowerControlCommand::setActivePowerLimit(const float limit, const Pow udpateCRC(CRC_SIZE); } -bool ActivePowerControlCommand::handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id) +bool ActivePowerControlCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id) { - if (!DevControlCommand::handleResponse(inverter, fragment, max_fragment_id)) { + if (!DevControlCommand::handleResponse(fragment, max_fragment_id)) { return false; } if ((getType() == PowerLimitControlType::RelativNonPersistent) || (getType() == PowerLimitControlType::RelativPersistent)) { - inverter.SystemConfigPara()->setLimitPercent(getLimit()); + _inv->SystemConfigPara()->setLimitPercent(getLimit()); } else { - const uint16_t max_power = inverter.DevInfo()->getMaxPower(); + const uint16_t max_power = _inv->DevInfo()->getMaxPower(); if (max_power > 0) { - inverter.SystemConfigPara()->setLimitPercent(static_cast(getLimit()) / max_power * 100); + _inv->SystemConfigPara()->setLimitPercent(static_cast(getLimit()) / max_power * 100); } else { // TODO(tbnobody): Not implemented yet because we only can publish the percentage value } } - inverter.SystemConfigPara()->setLastUpdateCommand(millis()); - inverter.SystemConfigPara()->setLastLimitCommandSuccess(CMD_OK); + _inv->SystemConfigPara()->setLastUpdateCommand(millis()); + _inv->SystemConfigPara()->setLastLimitCommandSuccess(CMD_OK); return true; } float ActivePowerControlCommand::getLimit() const { - const uint16_t l = (((uint16_t)_payload[12] << 8) | _payload[13]); + const float l = (((uint16_t)_payload[12] << 8) | _payload[13]); return l / 10; } @@ -94,7 +94,7 @@ PowerLimitControlType ActivePowerControlCommand::getType() return (PowerLimitControlType)(((uint16_t)_payload[14] << 8) | _payload[15]); } -void ActivePowerControlCommand::gotTimeout(InverterAbstract& inverter) +void ActivePowerControlCommand::gotTimeout() { - inverter.SystemConfigPara()->setLastLimitCommandSuccess(CMD_NOK); -} \ No newline at end of file + _inv->SystemConfigPara()->setLastLimitCommandSuccess(CMD_NOK); +} diff --git a/lib/Hoymiles/src/commands/ActivePowerControlCommand.h b/lib/Hoymiles/src/commands/ActivePowerControlCommand.h index b7831fb86..375b278bb 100644 --- a/lib/Hoymiles/src/commands/ActivePowerControlCommand.h +++ b/lib/Hoymiles/src/commands/ActivePowerControlCommand.h @@ -12,14 +12,14 @@ typedef enum { // ToDo: to be verified by field tests class ActivePowerControlCommand : public DevControlCommand { public: - explicit ActivePowerControlCommand(const uint64_t target_address = 0, const uint64_t router_address = 0); + explicit ActivePowerControlCommand(InverterAbstract* inv, const uint64_t router_address = 0); virtual String getCommandName() const; - virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id); - virtual void gotTimeout(InverterAbstract& inverter); + virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id); + virtual void gotTimeout(); void setActivePowerLimit(const float limit, const PowerLimitControlType type = RelativNonPersistent); float getLimit() const; PowerLimitControlType getType(); -}; \ No newline at end of file +}; diff --git a/lib/Hoymiles/src/commands/AlarmDataCommand.cpp b/lib/Hoymiles/src/commands/AlarmDataCommand.cpp index 143a6cd56..98a97d0b1 100644 --- a/lib/Hoymiles/src/commands/AlarmDataCommand.cpp +++ b/lib/Hoymiles/src/commands/AlarmDataCommand.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022-2023 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ /* @@ -23,8 +23,8 @@ ID Target Addr Source Addr Idx DT ? Time Gap AlarmId Pa #include "AlarmDataCommand.h" #include "inverters/InverterAbstract.h" -AlarmDataCommand::AlarmDataCommand(const uint64_t target_address, const uint64_t router_address, const time_t time) - : MultiDataCommand(target_address, router_address) +AlarmDataCommand::AlarmDataCommand(InverterAbstract* inv, const uint64_t router_address, const time_t time) + : MultiDataCommand(inv, router_address) { setTime(time); setDataType(0x11); @@ -36,28 +36,28 @@ String AlarmDataCommand::getCommandName() const return "AlarmData"; } -bool AlarmDataCommand::handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id) +bool AlarmDataCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id) { // Check CRC of whole payload - if (!MultiDataCommand::handleResponse(inverter, fragment, max_fragment_id)) { + if (!MultiDataCommand::handleResponse(fragment, max_fragment_id)) { return false; } // Move all fragments into target buffer uint8_t offs = 0; - inverter.EventLog()->beginAppendFragment(); - inverter.EventLog()->clearBuffer(); + _inv->EventLog()->beginAppendFragment(); + _inv->EventLog()->clearBuffer(); for (uint8_t i = 0; i < max_fragment_id; i++) { - inverter.EventLog()->appendFragment(offs, fragment[i].fragment, fragment[i].len); + _inv->EventLog()->appendFragment(offs, fragment[i].fragment, fragment[i].len); offs += (fragment[i].len); } - inverter.EventLog()->endAppendFragment(); - inverter.EventLog()->setLastAlarmRequestSuccess(CMD_OK); - inverter.EventLog()->setLastUpdate(millis()); + _inv->EventLog()->endAppendFragment(); + _inv->EventLog()->setLastAlarmRequestSuccess(CMD_OK); + _inv->EventLog()->setLastUpdate(millis()); return true; } -void AlarmDataCommand::gotTimeout(InverterAbstract& inverter) +void AlarmDataCommand::gotTimeout() { - inverter.EventLog()->setLastAlarmRequestSuccess(CMD_NOK); -} \ No newline at end of file + _inv->EventLog()->setLastAlarmRequestSuccess(CMD_NOK); +} diff --git a/lib/Hoymiles/src/commands/AlarmDataCommand.h b/lib/Hoymiles/src/commands/AlarmDataCommand.h index abdfc5f83..ef8404c30 100644 --- a/lib/Hoymiles/src/commands/AlarmDataCommand.h +++ b/lib/Hoymiles/src/commands/AlarmDataCommand.h @@ -5,10 +5,10 @@ class AlarmDataCommand : public MultiDataCommand { public: - explicit AlarmDataCommand(const uint64_t target_address = 0, const uint64_t router_address = 0, const time_t time = 0); + explicit AlarmDataCommand(InverterAbstract* inv, const uint64_t router_address = 0, const time_t time = 0); virtual String getCommandName() const; - virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id); - virtual void gotTimeout(InverterAbstract& inverter); -}; \ No newline at end of file + virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id); + virtual void gotTimeout(); +}; diff --git a/lib/Hoymiles/src/commands/ChannelChangeCommand.cpp b/lib/Hoymiles/src/commands/ChannelChangeCommand.cpp index 301785098..ad89f2d5f 100644 --- a/lib/Hoymiles/src/commands/ChannelChangeCommand.cpp +++ b/lib/Hoymiles/src/commands/ChannelChangeCommand.cpp @@ -19,8 +19,8 @@ ID Target Addr Source Addr ? ? ? CH ? CRC8 */ #include "ChannelChangeCommand.h" -ChannelChangeCommand::ChannelChangeCommand(const uint64_t target_address, const uint64_t router_address, const uint8_t channel) - : CommandAbstract(target_address, router_address) +ChannelChangeCommand::ChannelChangeCommand(InverterAbstract* inv, const uint64_t router_address, const uint8_t channel) + : CommandAbstract(inv, router_address) { _payload[0] = 0x56; _payload[13] = 0x14; @@ -67,7 +67,7 @@ void ChannelChangeCommand::setCountryMode(const CountryModeId_t mode) } } -bool ChannelChangeCommand::handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id) +bool ChannelChangeCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id) { return true; } diff --git a/lib/Hoymiles/src/commands/ChannelChangeCommand.h b/lib/Hoymiles/src/commands/ChannelChangeCommand.h index 44a4c9ebd..70b5f64c7 100644 --- a/lib/Hoymiles/src/commands/ChannelChangeCommand.h +++ b/lib/Hoymiles/src/commands/ChannelChangeCommand.h @@ -6,7 +6,7 @@ class ChannelChangeCommand : public CommandAbstract { public: - explicit ChannelChangeCommand(const uint64_t target_address = 0, const uint64_t router_address = 0, const uint8_t channel = 0); + explicit ChannelChangeCommand(InverterAbstract* inv, const uint64_t router_address = 0, const uint8_t channel = 0); virtual String getCommandName() const; @@ -15,7 +15,7 @@ class ChannelChangeCommand : public CommandAbstract { void setCountryMode(const CountryModeId_t mode); - virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id); + virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id); virtual uint8_t getMaxResendCount(); }; diff --git a/lib/Hoymiles/src/commands/CommandAbstract.cpp b/lib/Hoymiles/src/commands/CommandAbstract.cpp index dafe2b175..16a7857e1 100644 --- a/lib/Hoymiles/src/commands/CommandAbstract.cpp +++ b/lib/Hoymiles/src/commands/CommandAbstract.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022-2023 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ /* @@ -29,13 +29,16 @@ Source Address: 80 12 23 04 #include "CommandAbstract.h" #include "crc.h" #include +#include "../inverters/InverterAbstract.h" -CommandAbstract::CommandAbstract(const uint64_t target_address, const uint64_t router_address) +CommandAbstract::CommandAbstract(InverterAbstract* inv, const uint64_t router_address) { memset(_payload, 0, RF_LEN); _payload_size = 0; - setTargetAddress(target_address); + _inv = inv; + + setTargetAddress(_inv->serial()); setRouterAddress(router_address); setSendCount(0); setTimeout(0); @@ -122,7 +125,7 @@ void CommandAbstract::convertSerialToPacketId(uint8_t buffer[], const uint64_t s buffer[0] = s.b[3]; } -void CommandAbstract::gotTimeout(InverterAbstract& inverter) +void CommandAbstract::gotTimeout() { } diff --git a/lib/Hoymiles/src/commands/CommandAbstract.h b/lib/Hoymiles/src/commands/CommandAbstract.h index 677fc0d12..c93cb3416 100644 --- a/lib/Hoymiles/src/commands/CommandAbstract.h +++ b/lib/Hoymiles/src/commands/CommandAbstract.h @@ -13,7 +13,7 @@ class InverterAbstract; class CommandAbstract { public: - explicit CommandAbstract(const uint64_t target_address = 0, const uint64_t router_address = 0); + explicit CommandAbstract(InverterAbstract* inv, const uint64_t router_address = 0); virtual ~CommandAbstract() {}; const uint8_t* getDataPayload(); @@ -21,7 +21,6 @@ class CommandAbstract { uint8_t getDataSize() const; - void setTargetAddress(const uint64_t address); uint64_t getTargetAddress() const; void setRouterAddress(const uint64_t address); @@ -38,8 +37,8 @@ class CommandAbstract { virtual CommandAbstract* getRequestFrameCommand(const uint8_t frame_no); - virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id) = 0; - virtual void gotTimeout(InverterAbstract& inverter); + virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id) = 0; + virtual void gotTimeout(); // Sets the amount how often the specific command is resent if all fragments where missing virtual uint8_t getMaxResendCount() const; @@ -56,6 +55,9 @@ class CommandAbstract { uint64_t _targetAddress; uint64_t _routerAddress; + InverterAbstract* _inv; + private: + void setTargetAddress(const uint64_t address); static void convertSerialToPacketId(uint8_t buffer[], const uint64_t serial); -}; \ No newline at end of file +}; diff --git a/lib/Hoymiles/src/commands/DevControlCommand.cpp b/lib/Hoymiles/src/commands/DevControlCommand.cpp index a5e7d2b60..b73f74f0c 100644 --- a/lib/Hoymiles/src/commands/DevControlCommand.cpp +++ b/lib/Hoymiles/src/commands/DevControlCommand.cpp @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022-2023 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ /* @@ -23,8 +23,8 @@ ID Target Addr Source Addr Cmd Payload CRC16 CRC8 #include "DevControlCommand.h" #include "crc.h" -DevControlCommand::DevControlCommand(const uint64_t target_address, const uint64_t router_address) - : CommandAbstract(target_address, router_address) +DevControlCommand::DevControlCommand(InverterAbstract* inv, const uint64_t router_address) + : CommandAbstract(inv, router_address) { _payload[0] = 0x51; _payload[9] = 0x81; @@ -39,7 +39,7 @@ void DevControlCommand::udpateCRC(const uint8_t len) _payload[10 + len + 1] = (uint8_t)(crc); } -bool DevControlCommand::handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id) +bool DevControlCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id) { for (uint8_t i = 0; i < max_fragment_id; i++) { if (fragment[i].mainCmd != (_payload[0] | 0x80)) { @@ -48,4 +48,4 @@ bool DevControlCommand::handleResponse(InverterAbstract& inverter, const fragmen } return true; -} \ No newline at end of file +} diff --git a/lib/Hoymiles/src/commands/DevControlCommand.h b/lib/Hoymiles/src/commands/DevControlCommand.h index c24bc60b2..7e7637edc 100644 --- a/lib/Hoymiles/src/commands/DevControlCommand.h +++ b/lib/Hoymiles/src/commands/DevControlCommand.h @@ -5,10 +5,10 @@ class DevControlCommand : public CommandAbstract { public: - explicit DevControlCommand(const uint64_t target_address = 0, const uint64_t router_address = 0); + explicit DevControlCommand(InverterAbstract* inv, const uint64_t router_address = 0); - virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id); + virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id); protected: void udpateCRC(const uint8_t len); -}; \ No newline at end of file +}; diff --git a/lib/Hoymiles/src/commands/DevInfoAllCommand.cpp b/lib/Hoymiles/src/commands/DevInfoAllCommand.cpp index c7bd80272..8a258ac20 100644 --- a/lib/Hoymiles/src/commands/DevInfoAllCommand.cpp +++ b/lib/Hoymiles/src/commands/DevInfoAllCommand.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022-2023 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ /* @@ -21,8 +21,8 @@ ID Target Addr Source Addr Idx DT ? Time Gap Pa #include "DevInfoAllCommand.h" #include "inverters/InverterAbstract.h" -DevInfoAllCommand::DevInfoAllCommand(const uint64_t target_address, const uint64_t router_address, const time_t time) - : MultiDataCommand(target_address, router_address) +DevInfoAllCommand::DevInfoAllCommand(InverterAbstract* inv, const uint64_t router_address, const time_t time) + : MultiDataCommand(inv, router_address) { setTime(time); setDataType(0x01); @@ -34,22 +34,22 @@ String DevInfoAllCommand::getCommandName() const return "DevInfoAll"; } -bool DevInfoAllCommand::handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id) +bool DevInfoAllCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id) { // Check CRC of whole payload - if (!MultiDataCommand::handleResponse(inverter, fragment, max_fragment_id)) { + if (!MultiDataCommand::handleResponse(fragment, max_fragment_id)) { return false; } // Move all fragments into target buffer uint8_t offs = 0; - inverter.DevInfo()->beginAppendFragment(); - inverter.DevInfo()->clearBufferAll(); + _inv->DevInfo()->beginAppendFragment(); + _inv->DevInfo()->clearBufferAll(); for (uint8_t i = 0; i < max_fragment_id; i++) { - inverter.DevInfo()->appendFragmentAll(offs, fragment[i].fragment, fragment[i].len); + _inv->DevInfo()->appendFragmentAll(offs, fragment[i].fragment, fragment[i].len); offs += (fragment[i].len); } - inverter.DevInfo()->endAppendFragment(); - inverter.DevInfo()->setLastUpdateAll(millis()); + _inv->DevInfo()->endAppendFragment(); + _inv->DevInfo()->setLastUpdateAll(millis()); return true; -} \ No newline at end of file +} diff --git a/lib/Hoymiles/src/commands/DevInfoAllCommand.h b/lib/Hoymiles/src/commands/DevInfoAllCommand.h index 3facffa7c..8ddfd8341 100644 --- a/lib/Hoymiles/src/commands/DevInfoAllCommand.h +++ b/lib/Hoymiles/src/commands/DevInfoAllCommand.h @@ -5,9 +5,9 @@ class DevInfoAllCommand : public MultiDataCommand { public: - explicit DevInfoAllCommand(const uint64_t target_address = 0, const uint64_t router_address = 0, const time_t time = 0); + explicit DevInfoAllCommand(InverterAbstract* inv, const uint64_t router_address = 0, const time_t time = 0); virtual String getCommandName() const; - virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id); -}; \ No newline at end of file + virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id); +}; diff --git a/lib/Hoymiles/src/commands/DevInfoSimpleCommand.cpp b/lib/Hoymiles/src/commands/DevInfoSimpleCommand.cpp index 2afaae4bd..d134a0ac1 100644 --- a/lib/Hoymiles/src/commands/DevInfoSimpleCommand.cpp +++ b/lib/Hoymiles/src/commands/DevInfoSimpleCommand.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022-2023 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ /* @@ -21,8 +21,8 @@ ID Target Addr Source Addr Idx DT ? Time Gap Pa #include "DevInfoSimpleCommand.h" #include "inverters/InverterAbstract.h" -DevInfoSimpleCommand::DevInfoSimpleCommand(const uint64_t target_address, const uint64_t router_address, const time_t time) - : MultiDataCommand(target_address, router_address) +DevInfoSimpleCommand::DevInfoSimpleCommand(InverterAbstract* inv, const uint64_t router_address, const time_t time) + : MultiDataCommand(inv, router_address) { setTime(time); setDataType(0x00); @@ -34,22 +34,22 @@ String DevInfoSimpleCommand::getCommandName() const return "DevInfoSimple"; } -bool DevInfoSimpleCommand::handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id) +bool DevInfoSimpleCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id) { // Check CRC of whole payload - if (!MultiDataCommand::handleResponse(inverter, fragment, max_fragment_id)) { + if (!MultiDataCommand::handleResponse(fragment, max_fragment_id)) { return false; } // Move all fragments into target buffer uint8_t offs = 0; - inverter.DevInfo()->beginAppendFragment(); - inverter.DevInfo()->clearBufferSimple(); + _inv->DevInfo()->beginAppendFragment(); + _inv->DevInfo()->clearBufferSimple(); for (uint8_t i = 0; i < max_fragment_id; i++) { - inverter.DevInfo()->appendFragmentSimple(offs, fragment[i].fragment, fragment[i].len); + _inv->DevInfo()->appendFragmentSimple(offs, fragment[i].fragment, fragment[i].len); offs += (fragment[i].len); } - inverter.DevInfo()->endAppendFragment(); - inverter.DevInfo()->setLastUpdateSimple(millis()); + _inv->DevInfo()->endAppendFragment(); + _inv->DevInfo()->setLastUpdateSimple(millis()); return true; -} \ No newline at end of file +} diff --git a/lib/Hoymiles/src/commands/DevInfoSimpleCommand.h b/lib/Hoymiles/src/commands/DevInfoSimpleCommand.h index 66a7301a9..927f1eab9 100644 --- a/lib/Hoymiles/src/commands/DevInfoSimpleCommand.h +++ b/lib/Hoymiles/src/commands/DevInfoSimpleCommand.h @@ -5,9 +5,9 @@ class DevInfoSimpleCommand : public MultiDataCommand { public: - explicit DevInfoSimpleCommand(const uint64_t target_address = 0, const uint64_t router_address = 0, const time_t time = 0); + explicit DevInfoSimpleCommand(InverterAbstract* inv, const uint64_t router_address = 0, const time_t time = 0); virtual String getCommandName() const; - virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id); -}; \ No newline at end of file + virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id); +}; diff --git a/lib/Hoymiles/src/commands/GridOnProFilePara.cpp b/lib/Hoymiles/src/commands/GridOnProFilePara.cpp index c98c7e5a5..779303773 100644 --- a/lib/Hoymiles/src/commands/GridOnProFilePara.cpp +++ b/lib/Hoymiles/src/commands/GridOnProFilePara.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022-2023 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ /* @@ -22,8 +22,8 @@ ID Target Addr Source Addr Idx DT ? Time Gap Pa #include "Hoymiles.h" #include "inverters/InverterAbstract.h" -GridOnProFilePara::GridOnProFilePara(const uint64_t target_address, const uint64_t router_address, const time_t time) - : MultiDataCommand(target_address, router_address) +GridOnProFilePara::GridOnProFilePara(InverterAbstract* inv, const uint64_t router_address, const time_t time) + : MultiDataCommand(inv, router_address) { setTime(time); setDataType(0x02); @@ -35,22 +35,22 @@ String GridOnProFilePara::getCommandName() const return "GridOnProFilePara"; } -bool GridOnProFilePara::handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id) +bool GridOnProFilePara::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id) { // Check CRC of whole payload - if (!MultiDataCommand::handleResponse(inverter, fragment, max_fragment_id)) { + if (!MultiDataCommand::handleResponse(fragment, max_fragment_id)) { return false; } // Move all fragments into target buffer uint8_t offs = 0; - inverter.GridProfile()->beginAppendFragment(); - inverter.GridProfile()->clearBuffer(); + _inv->GridProfile()->beginAppendFragment(); + _inv->GridProfile()->clearBuffer(); for (uint8_t i = 0; i < max_fragment_id; i++) { - inverter.GridProfile()->appendFragment(offs, fragment[i].fragment, fragment[i].len); + _inv->GridProfile()->appendFragment(offs, fragment[i].fragment, fragment[i].len); offs += (fragment[i].len); } - inverter.GridProfile()->endAppendFragment(); - inverter.GridProfile()->setLastUpdate(millis()); + _inv->GridProfile()->endAppendFragment(); + _inv->GridProfile()->setLastUpdate(millis()); return true; -} \ No newline at end of file +} diff --git a/lib/Hoymiles/src/commands/GridOnProFilePara.h b/lib/Hoymiles/src/commands/GridOnProFilePara.h index 382ebcbb1..b2380c75e 100644 --- a/lib/Hoymiles/src/commands/GridOnProFilePara.h +++ b/lib/Hoymiles/src/commands/GridOnProFilePara.h @@ -5,9 +5,9 @@ class GridOnProFilePara : public MultiDataCommand { public: - explicit GridOnProFilePara(const uint64_t target_address = 0, const uint64_t router_address = 0, const time_t time = 0); + explicit GridOnProFilePara(InverterAbstract* inv, const uint64_t router_address = 0, const time_t time = 0); virtual String getCommandName() const; - virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id); -}; \ No newline at end of file + virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id); +}; diff --git a/lib/Hoymiles/src/commands/MultiDataCommand.cpp b/lib/Hoymiles/src/commands/MultiDataCommand.cpp index bbd320916..0e7bf51f1 100644 --- a/lib/Hoymiles/src/commands/MultiDataCommand.cpp +++ b/lib/Hoymiles/src/commands/MultiDataCommand.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022-2023 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ /* @@ -28,8 +28,9 @@ ID Target Addr Source Addr Idx DT ? Time Gap Pa #include "MultiDataCommand.h" #include "crc.h" -MultiDataCommand::MultiDataCommand(const uint64_t target_address, const uint64_t router_address, const uint8_t data_type, const time_t time) - : CommandAbstract(target_address, router_address) +MultiDataCommand::MultiDataCommand(InverterAbstract* inv, const uint64_t router_address, const uint8_t data_type, const time_t time) + : CommandAbstract(inv, router_address) + , _cmdRequestFrame(inv) { _payload[0] = 0x15; _payload[9] = 0x80; @@ -79,13 +80,12 @@ time_t MultiDataCommand::getTime() const CommandAbstract* MultiDataCommand::getRequestFrameCommand(const uint8_t frame_no) { - _cmdRequestFrame.setTargetAddress(getTargetAddress()); _cmdRequestFrame.setFrameNo(frame_no); return &_cmdRequestFrame; } -bool MultiDataCommand::handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id) +bool MultiDataCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id) { // All fragments are available --> Check CRC uint16_t crc = 0xffff, crcRcv = 0; diff --git a/lib/Hoymiles/src/commands/MultiDataCommand.h b/lib/Hoymiles/src/commands/MultiDataCommand.h index 821074745..5693287fa 100644 --- a/lib/Hoymiles/src/commands/MultiDataCommand.h +++ b/lib/Hoymiles/src/commands/MultiDataCommand.h @@ -7,14 +7,14 @@ class MultiDataCommand : public CommandAbstract { public: - explicit MultiDataCommand(const uint64_t target_address = 0, const uint64_t router_address = 0, const uint8_t data_type = 0, const time_t time = 0); + explicit MultiDataCommand(InverterAbstract* inv, const uint64_t router_address = 0, const uint8_t data_type = 0, const time_t time = 0); void setTime(const time_t time); time_t getTime() const; CommandAbstract* getRequestFrameCommand(const uint8_t frame_no); - virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id); + virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id); protected: void setDataType(const uint8_t data_type); @@ -23,4 +23,4 @@ class MultiDataCommand : public CommandAbstract { static uint8_t getTotalFragmentSize(const fragment_t fragment[], const uint8_t max_fragment_id); RequestFrameCommand _cmdRequestFrame; -}; \ No newline at end of file +}; diff --git a/lib/Hoymiles/src/commands/ParaSetCommand.cpp b/lib/Hoymiles/src/commands/ParaSetCommand.cpp index a33749450..8b71867bd 100644 --- a/lib/Hoymiles/src/commands/ParaSetCommand.cpp +++ b/lib/Hoymiles/src/commands/ParaSetCommand.cpp @@ -1,11 +1,11 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ #include "ParaSetCommand.h" -ParaSetCommand::ParaSetCommand(const uint64_t target_address, const uint64_t router_address) - : CommandAbstract(target_address, router_address) +ParaSetCommand::ParaSetCommand(InverterAbstract* inv, const uint64_t router_address) + : CommandAbstract(inv, router_address) { _payload[0] = 0x52; -} \ No newline at end of file +} diff --git a/lib/Hoymiles/src/commands/ParaSetCommand.h b/lib/Hoymiles/src/commands/ParaSetCommand.h index 424d0e373..224aba390 100644 --- a/lib/Hoymiles/src/commands/ParaSetCommand.h +++ b/lib/Hoymiles/src/commands/ParaSetCommand.h @@ -5,5 +5,5 @@ class ParaSetCommand : public CommandAbstract { public: - explicit ParaSetCommand(const uint64_t target_address = 0, const uint64_t router_address = 0); -}; \ No newline at end of file + explicit ParaSetCommand(InverterAbstract* inv, const uint64_t router_address = 0); +}; diff --git a/lib/Hoymiles/src/commands/PowerControlCommand.cpp b/lib/Hoymiles/src/commands/PowerControlCommand.cpp index fbf12db80..927c33303 100644 --- a/lib/Hoymiles/src/commands/PowerControlCommand.cpp +++ b/lib/Hoymiles/src/commands/PowerControlCommand.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022-2023 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ /* @@ -26,8 +26,8 @@ ID Target Addr Source Addr Cmd SCmd ? CRC16 CRC8 #define CRC_SIZE 2 -PowerControlCommand::PowerControlCommand(const uint64_t target_address, const uint64_t router_address) - : DevControlCommand(target_address, router_address) +PowerControlCommand::PowerControlCommand(InverterAbstract* inv, const uint64_t router_address) + : DevControlCommand(inv, router_address) { _payload[10] = 0x00; // TurnOn _payload[11] = 0x00; @@ -44,20 +44,20 @@ String PowerControlCommand::getCommandName() const return "PowerControl"; } -bool PowerControlCommand::handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id) +bool PowerControlCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id) { - if (!DevControlCommand::handleResponse(inverter, fragment, max_fragment_id)) { + if (!DevControlCommand::handleResponse(fragment, max_fragment_id)) { return false; } - inverter.PowerCommand()->setLastUpdateCommand(millis()); - inverter.PowerCommand()->setLastPowerCommandSuccess(CMD_OK); + _inv->PowerCommand()->setLastUpdateCommand(millis()); + _inv->PowerCommand()->setLastPowerCommandSuccess(CMD_OK); return true; } -void PowerControlCommand::gotTimeout(InverterAbstract& inverter) +void PowerControlCommand::gotTimeout() { - inverter.PowerCommand()->setLastPowerCommandSuccess(CMD_NOK); + _inv->PowerCommand()->setLastPowerCommandSuccess(CMD_NOK); } void PowerControlCommand::setPowerOn(const bool state) @@ -76,4 +76,4 @@ void PowerControlCommand::setRestart() _payload[10] = 0x02; // Restart udpateCRC(CRC_SIZE); // 2 byte crc -} \ No newline at end of file +} diff --git a/lib/Hoymiles/src/commands/PowerControlCommand.h b/lib/Hoymiles/src/commands/PowerControlCommand.h index 8b9f11ac4..d40c356db 100644 --- a/lib/Hoymiles/src/commands/PowerControlCommand.h +++ b/lib/Hoymiles/src/commands/PowerControlCommand.h @@ -5,13 +5,13 @@ class PowerControlCommand : public DevControlCommand { public: - explicit PowerControlCommand(const uint64_t target_address = 0, const uint64_t router_address = 0); + explicit PowerControlCommand(InverterAbstract* inv, const uint64_t router_address = 0); virtual String getCommandName() const; - virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id); - virtual void gotTimeout(InverterAbstract& inverter); + virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id); + virtual void gotTimeout(); void setPowerOn(const bool state); void setRestart(); -}; \ No newline at end of file +}; diff --git a/lib/Hoymiles/src/commands/RealTimeRunDataCommand.cpp b/lib/Hoymiles/src/commands/RealTimeRunDataCommand.cpp index 5f04c948b..b1396a4dd 100644 --- a/lib/Hoymiles/src/commands/RealTimeRunDataCommand.cpp +++ b/lib/Hoymiles/src/commands/RealTimeRunDataCommand.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022-2023 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ /* @@ -22,8 +22,8 @@ ID Target Addr Source Addr Idx DT ? Time Gap Pa #include "Hoymiles.h" #include "inverters/InverterAbstract.h" -RealTimeRunDataCommand::RealTimeRunDataCommand(const uint64_t target_address, const uint64_t router_address, const time_t time) - : MultiDataCommand(target_address, router_address) +RealTimeRunDataCommand::RealTimeRunDataCommand(InverterAbstract* inv, const uint64_t router_address, const time_t time) + : MultiDataCommand(inv, router_address) { setTime(time); setDataType(0x0b); @@ -35,10 +35,10 @@ String RealTimeRunDataCommand::getCommandName() const return "RealTimeRunData"; } -bool RealTimeRunDataCommand::handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id) +bool RealTimeRunDataCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id) { // Check CRC of whole payload - if (!MultiDataCommand::handleResponse(inverter, fragment, max_fragment_id)) { + if (!MultiDataCommand::handleResponse(fragment, max_fragment_id)) { return false; } @@ -46,7 +46,7 @@ bool RealTimeRunDataCommand::handleResponse(InverterAbstract& inverter, const fr // In case of low power in the inverter it occours that some incomplete fragments // with a valid CRC are received. const uint8_t fragmentsSize = getTotalFragmentSize(fragment, max_fragment_id); - const uint8_t expectedSize = inverter.Statistics()->getExpectedByteCount(); + const uint8_t expectedSize = _inv->Statistics()->getExpectedByteCount(); if (fragmentsSize < expectedSize) { Hoymiles.getMessageOutput()->printf("ERROR in %s: Received fragment size: %d, min expected size: %d\r\n", getCommandName().c_str(), fragmentsSize, expectedSize); @@ -56,19 +56,19 @@ bool RealTimeRunDataCommand::handleResponse(InverterAbstract& inverter, const fr // Move all fragments into target buffer uint8_t offs = 0; - inverter.Statistics()->beginAppendFragment(); - inverter.Statistics()->clearBuffer(); + _inv->Statistics()->beginAppendFragment(); + _inv->Statistics()->clearBuffer(); for (uint8_t i = 0; i < max_fragment_id; i++) { - inverter.Statistics()->appendFragment(offs, fragment[i].fragment, fragment[i].len); + _inv->Statistics()->appendFragment(offs, fragment[i].fragment, fragment[i].len); offs += (fragment[i].len); } - inverter.Statistics()->endAppendFragment(); - inverter.Statistics()->resetRxFailureCount(); - inverter.Statistics()->setLastUpdate(millis()); + _inv->Statistics()->endAppendFragment(); + _inv->Statistics()->resetRxFailureCount(); + _inv->Statistics()->setLastUpdate(millis()); return true; } -void RealTimeRunDataCommand::gotTimeout(InverterAbstract& inverter) +void RealTimeRunDataCommand::gotTimeout() { - inverter.Statistics()->incrementRxFailureCount(); -} \ No newline at end of file + _inv->Statistics()->incrementRxFailureCount(); +} diff --git a/lib/Hoymiles/src/commands/RealTimeRunDataCommand.h b/lib/Hoymiles/src/commands/RealTimeRunDataCommand.h index 7a0eeec14..9341247f6 100644 --- a/lib/Hoymiles/src/commands/RealTimeRunDataCommand.h +++ b/lib/Hoymiles/src/commands/RealTimeRunDataCommand.h @@ -5,10 +5,10 @@ class RealTimeRunDataCommand : public MultiDataCommand { public: - explicit RealTimeRunDataCommand(const uint64_t target_address = 0, const uint64_t router_address = 0, const time_t time = 0); + explicit RealTimeRunDataCommand(InverterAbstract* inv, const uint64_t router_address = 0, const time_t time = 0); virtual String getCommandName() const; - virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id); - virtual void gotTimeout(InverterAbstract& inverter); -}; \ No newline at end of file + virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id); + virtual void gotTimeout(); +}; diff --git a/lib/Hoymiles/src/commands/RequestFrameCommand.cpp b/lib/Hoymiles/src/commands/RequestFrameCommand.cpp index 68c4977f7..0abb52356 100644 --- a/lib/Hoymiles/src/commands/RequestFrameCommand.cpp +++ b/lib/Hoymiles/src/commands/RequestFrameCommand.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022-2023 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ /* @@ -22,8 +22,8 @@ ID Target Addr Source Addr Frm CRC8 */ #include "RequestFrameCommand.h" -RequestFrameCommand::RequestFrameCommand(const uint64_t target_address, const uint64_t router_address, uint8_t frame_no) - : SingleDataCommand(target_address, router_address) +RequestFrameCommand::RequestFrameCommand(InverterAbstract* inv, const uint64_t router_address, uint8_t frame_no) + : SingleDataCommand(inv, router_address) { if (frame_no > 127) { frame_no = 0; @@ -47,7 +47,7 @@ uint8_t RequestFrameCommand::getFrameNo() const return _payload[9] & (~0x80); } -bool RequestFrameCommand::handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id) +bool RequestFrameCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id) { return true; -} \ No newline at end of file +} diff --git a/lib/Hoymiles/src/commands/RequestFrameCommand.h b/lib/Hoymiles/src/commands/RequestFrameCommand.h index 92663b708..2924e69bb 100644 --- a/lib/Hoymiles/src/commands/RequestFrameCommand.h +++ b/lib/Hoymiles/src/commands/RequestFrameCommand.h @@ -5,12 +5,12 @@ class RequestFrameCommand : public SingleDataCommand { public: - explicit RequestFrameCommand(const uint64_t target_address = 0, const uint64_t router_address = 0, uint8_t frame_no = 0); + explicit RequestFrameCommand(InverterAbstract* inv, const uint64_t router_address = 0, uint8_t frame_no = 0); virtual String getCommandName() const; void setFrameNo(const uint8_t frame_no); uint8_t getFrameNo() const; - virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id); -}; \ No newline at end of file + virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id); +}; diff --git a/lib/Hoymiles/src/commands/SingleDataCommand.cpp b/lib/Hoymiles/src/commands/SingleDataCommand.cpp index 4f775146d..3b648814a 100644 --- a/lib/Hoymiles/src/commands/SingleDataCommand.cpp +++ b/lib/Hoymiles/src/commands/SingleDataCommand.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022-2023 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ /* @@ -19,8 +19,8 @@ ID Target Addr Source Addr CRC8 */ #include "SingleDataCommand.h" -SingleDataCommand::SingleDataCommand(const uint64_t target_address, const uint64_t router_address) - : CommandAbstract(target_address, router_address) +SingleDataCommand::SingleDataCommand(InverterAbstract* inv, const uint64_t router_address) + : CommandAbstract(inv, router_address) { _payload[0] = 0x15; setTimeout(100); diff --git a/lib/Hoymiles/src/commands/SingleDataCommand.h b/lib/Hoymiles/src/commands/SingleDataCommand.h index d05151691..39f3c480c 100644 --- a/lib/Hoymiles/src/commands/SingleDataCommand.h +++ b/lib/Hoymiles/src/commands/SingleDataCommand.h @@ -5,5 +5,5 @@ class SingleDataCommand : public CommandAbstract { public: - explicit SingleDataCommand(const uint64_t target_address = 0, const uint64_t router_address = 0); -}; \ No newline at end of file + explicit SingleDataCommand(InverterAbstract* inv, const uint64_t router_address = 0); +}; diff --git a/lib/Hoymiles/src/commands/SystemConfigParaCommand.cpp b/lib/Hoymiles/src/commands/SystemConfigParaCommand.cpp index 0c8e7ded7..0c142afc8 100644 --- a/lib/Hoymiles/src/commands/SystemConfigParaCommand.cpp +++ b/lib/Hoymiles/src/commands/SystemConfigParaCommand.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022-2023 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ /* @@ -22,8 +22,8 @@ ID Target Addr Source Addr Idx DT ? Time Gap Pa #include "Hoymiles.h" #include "inverters/InverterAbstract.h" -SystemConfigParaCommand::SystemConfigParaCommand(const uint64_t target_address, const uint64_t router_address, const time_t time) - : MultiDataCommand(target_address, router_address) +SystemConfigParaCommand::SystemConfigParaCommand(InverterAbstract* inv, const uint64_t router_address, const time_t time) + : MultiDataCommand(inv, router_address) { setTime(time); setDataType(0x05); @@ -35,10 +35,10 @@ String SystemConfigParaCommand::getCommandName() const return "SystemConfigPara"; } -bool SystemConfigParaCommand::handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id) +bool SystemConfigParaCommand::handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id) { // Check CRC of whole payload - if (!MultiDataCommand::handleResponse(inverter, fragment, max_fragment_id)) { + if (!MultiDataCommand::handleResponse(fragment, max_fragment_id)) { return false; } @@ -46,7 +46,7 @@ bool SystemConfigParaCommand::handleResponse(InverterAbstract& inverter, const f // In case of low power in the inverter it occours that some incomplete fragments // with a valid CRC are received. const uint8_t fragmentsSize = getTotalFragmentSize(fragment, max_fragment_id); - const uint8_t expectedSize = inverter.SystemConfigPara()->getExpectedByteCount(); + const uint8_t expectedSize = _inv->SystemConfigPara()->getExpectedByteCount(); if (fragmentsSize < expectedSize) { Hoymiles.getMessageOutput()->printf("ERROR in %s: Received fragment size: %d, min expected size: %d\r\n", getCommandName().c_str(), fragmentsSize, expectedSize); @@ -56,19 +56,19 @@ bool SystemConfigParaCommand::handleResponse(InverterAbstract& inverter, const f // Move all fragments into target buffer uint8_t offs = 0; - inverter.SystemConfigPara()->beginAppendFragment(); - inverter.SystemConfigPara()->clearBuffer(); + _inv->SystemConfigPara()->beginAppendFragment(); + _inv->SystemConfigPara()->clearBuffer(); for (uint8_t i = 0; i < max_fragment_id; i++) { - inverter.SystemConfigPara()->appendFragment(offs, fragment[i].fragment, fragment[i].len); + _inv->SystemConfigPara()->appendFragment(offs, fragment[i].fragment, fragment[i].len); offs += (fragment[i].len); } - inverter.SystemConfigPara()->endAppendFragment(); - inverter.SystemConfigPara()->setLastUpdateRequest(millis()); - inverter.SystemConfigPara()->setLastLimitRequestSuccess(CMD_OK); + _inv->SystemConfigPara()->endAppendFragment(); + _inv->SystemConfigPara()->setLastUpdateRequest(millis()); + _inv->SystemConfigPara()->setLastLimitRequestSuccess(CMD_OK); return true; } -void SystemConfigParaCommand::gotTimeout(InverterAbstract& inverter) +void SystemConfigParaCommand::gotTimeout() { - inverter.SystemConfigPara()->setLastLimitRequestSuccess(CMD_NOK); -} \ No newline at end of file + _inv->SystemConfigPara()->setLastLimitRequestSuccess(CMD_NOK); +} diff --git a/lib/Hoymiles/src/commands/SystemConfigParaCommand.h b/lib/Hoymiles/src/commands/SystemConfigParaCommand.h index e2480a973..147f18dae 100644 --- a/lib/Hoymiles/src/commands/SystemConfigParaCommand.h +++ b/lib/Hoymiles/src/commands/SystemConfigParaCommand.h @@ -5,10 +5,10 @@ class SystemConfigParaCommand : public MultiDataCommand { public: - explicit SystemConfigParaCommand(const uint64_t target_address = 0, const uint64_t router_address = 0, const time_t time = 0); + explicit SystemConfigParaCommand(InverterAbstract* inv, const uint64_t router_address = 0, const time_t time = 0); virtual String getCommandName() const; - virtual bool handleResponse(InverterAbstract& inverter, const fragment_t fragment[], const uint8_t max_fragment_id); - virtual void gotTimeout(InverterAbstract& inverter); -}; \ No newline at end of file + virtual bool handleResponse(const fragment_t fragment[], const uint8_t max_fragment_id); + virtual void gotTimeout(); +}; diff --git a/lib/Hoymiles/src/inverters/HMS_2CH.cpp b/lib/Hoymiles/src/inverters/HMS_2CH.cpp index 4a700a9a9..4ad0157f5 100644 --- a/lib/Hoymiles/src/inverters/HMS_2CH.cpp +++ b/lib/Hoymiles/src/inverters/HMS_2CH.cpp @@ -42,7 +42,7 @@ bool HMS_2CH::isValidSerial(const uint64_t serial) { // serial >= 0x114400000000 && serial <= 0x1144ffffffff uint16_t preSerial = (serial >> 32) & 0xffff; - return preSerial == 0x1144; + return preSerial == 0x1144 || preSerial == 0x1143; } String HMS_2CH::typeName() const diff --git a/lib/Hoymiles/src/inverters/HMS_Abstract.cpp b/lib/Hoymiles/src/inverters/HMS_Abstract.cpp index ffa7d7219..4fc64b036 100644 --- a/lib/Hoymiles/src/inverters/HMS_Abstract.cpp +++ b/lib/Hoymiles/src/inverters/HMS_Abstract.cpp @@ -18,10 +18,9 @@ bool HMS_Abstract::sendChangeChannelRequest() return false; } - auto cmdChannel = _radio->prepareCommand(); + auto cmdChannel = _radio->prepareCommand(this); cmdChannel->setCountryMode(Hoymiles.getRadioCmt()->getCountryMode()); cmdChannel->setChannel(Hoymiles.getRadioCmt()->getChannelFromFrequency(Hoymiles.getRadioCmt()->getInverterTargetFrequency())); - cmdChannel->setTargetAddress(serial()); _radio->enqueCommand(cmdChannel); return true; diff --git a/lib/Hoymiles/src/inverters/HMT_Abstract.cpp b/lib/Hoymiles/src/inverters/HMT_Abstract.cpp index 5c232b900..50c895cc6 100644 --- a/lib/Hoymiles/src/inverters/HMT_Abstract.cpp +++ b/lib/Hoymiles/src/inverters/HMT_Abstract.cpp @@ -20,10 +20,9 @@ bool HMT_Abstract::sendChangeChannelRequest() return false; } - auto cmdChannel = _radio->prepareCommand(); + auto cmdChannel = _radio->prepareCommand(this); cmdChannel->setCountryMode(Hoymiles.getRadioCmt()->getCountryMode()); cmdChannel->setChannel(Hoymiles.getRadioCmt()->getChannelFromFrequency(Hoymiles.getRadioCmt()->getInverterTargetFrequency())); - cmdChannel->setTargetAddress(serial()); _radio->enqueCommand(cmdChannel); return true; diff --git a/lib/Hoymiles/src/inverters/HM_Abstract.cpp b/lib/Hoymiles/src/inverters/HM_Abstract.cpp index 38515ab83..c99d4cfc6 100644 --- a/lib/Hoymiles/src/inverters/HM_Abstract.cpp +++ b/lib/Hoymiles/src/inverters/HM_Abstract.cpp @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ #include "HM_Abstract.h" #include "HoymilesRadio.h" @@ -30,9 +30,8 @@ bool HM_Abstract::sendStatsRequest() time_t now; time(&now); - auto cmd = _radio->prepareCommand(); + auto cmd = _radio->prepareCommand(this); cmd->setTime(now); - cmd->setTargetAddress(serial()); _radio->enqueCommand(cmd); return true; @@ -62,9 +61,8 @@ bool HM_Abstract::sendAlarmLogRequest(const bool force) time_t now; time(&now); - auto cmd = _radio->prepareCommand(); + auto cmd = _radio->prepareCommand(this); cmd->setTime(now); - cmd->setTargetAddress(serial()); EventLog()->setLastAlarmRequestSuccess(CMD_PENDING); _radio->enqueCommand(cmd); @@ -85,14 +83,12 @@ bool HM_Abstract::sendDevInfoRequest() time_t now; time(&now); - auto cmdAll = _radio->prepareCommand(); + auto cmdAll = _radio->prepareCommand(this); cmdAll->setTime(now); - cmdAll->setTargetAddress(serial()); _radio->enqueCommand(cmdAll); - auto cmdSimple = _radio->prepareCommand(); + auto cmdSimple = _radio->prepareCommand(this); cmdSimple->setTime(now); - cmdSimple->setTargetAddress(serial()); _radio->enqueCommand(cmdSimple); return true; @@ -112,9 +108,8 @@ bool HM_Abstract::sendSystemConfigParaRequest() time_t now; time(&now); - auto cmd = _radio->prepareCommand(); + auto cmd = _radio->prepareCommand(this); cmd->setTime(now); - cmd->setTargetAddress(serial()); SystemConfigPara()->setLastLimitRequestSuccess(CMD_PENDING); _radio->enqueCommand(cmd); @@ -138,9 +133,8 @@ bool HM_Abstract::sendActivePowerControlRequest(float limit, const PowerLimitCon _activePowerControlLimit = limit; _activePowerControlType = type; - auto cmd = _radio->prepareCommand(); + auto cmd = _radio->prepareCommand(this); cmd->setActivePowerLimit(limit, type); - cmd->setTargetAddress(serial()); SystemConfigPara()->setLastLimitCommandSuccess(CMD_PENDING); _radio->enqueCommand(cmd); @@ -168,9 +162,8 @@ bool HM_Abstract::sendPowerControlRequest(const bool turnOn) _powerState = 0; } - auto cmd = _radio->prepareCommand(); + auto cmd = _radio->prepareCommand(this); cmd->setPowerOn(turnOn); - cmd->setTargetAddress(serial()); PowerCommand()->setLastPowerCommandSuccess(CMD_PENDING); _radio->enqueCommand(cmd); @@ -185,9 +178,8 @@ bool HM_Abstract::sendRestartControlRequest() _powerState = 2; - auto cmd = _radio->prepareCommand(); + auto cmd = _radio->prepareCommand(this); cmd->setRestart(); - cmd->setTargetAddress(serial()); PowerCommand()->setLastPowerCommandSuccess(CMD_PENDING); _radio->enqueCommand(cmd); @@ -227,9 +219,8 @@ bool HM_Abstract::sendGridOnProFileParaRequest() time_t now; time(&now); - auto cmd = _radio->prepareCommand(); + auto cmd = _radio->prepareCommand(this); cmd->setTime(now); - cmd->setTargetAddress(serial()); _radio->enqueCommand(cmd); return true; diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.cpp b/lib/Hoymiles/src/inverters/InverterAbstract.cpp index d80d0e531..68d611836 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.cpp +++ b/lib/Hoymiles/src/inverters/InverterAbstract.cpp @@ -127,6 +127,16 @@ bool InverterAbstract::getZeroYieldDayOnMidnight() const return _zeroYieldDayOnMidnight; } +void InverterAbstract::setClearEventlogOnMidnight(const bool enabled) +{ + _clearEventlogOnMidnight = enabled; +} + +bool InverterAbstract::getClearEventlogOnMidnight() const +{ + return _clearEventlogOnMidnight; +} + bool InverterAbstract::sendChangeChannelRequest() { return false; @@ -226,7 +236,7 @@ uint8_t InverterAbstract::verifyAllFragments(CommandAbstract& cmd) if (cmd.getSendCount() <= cmd.getMaxResendCount()) { return FRAGMENT_ALL_MISSING_RESEND; } else { - cmd.gotTimeout(*this); + cmd.gotTimeout(); return FRAGMENT_ALL_MISSING_TIMEOUT; } } @@ -237,7 +247,7 @@ uint8_t InverterAbstract::verifyAllFragments(CommandAbstract& cmd) if (_rxFragmentRetransmitCnt++ < cmd.getMaxRetransmitCount()) { return _rxFragmentLastPacketId + 1; } else { - cmd.gotTimeout(*this); + cmd.gotTimeout(); return FRAGMENT_RETRANSMIT_TIMEOUT; } } @@ -249,16 +259,16 @@ uint8_t InverterAbstract::verifyAllFragments(CommandAbstract& cmd) if (_rxFragmentRetransmitCnt++ < cmd.getMaxRetransmitCount()) { return i + 1; } else { - cmd.gotTimeout(*this); + cmd.gotTimeout(); return FRAGMENT_RETRANSMIT_TIMEOUT; } } } - if (!cmd.handleResponse(*this, _rxFragmentBuffer, _rxFragmentMaxPacketId)) { - cmd.gotTimeout(*this); + if (!cmd.handleResponse(_rxFragmentBuffer, _rxFragmentMaxPacketId)) { + cmd.gotTimeout(); return FRAGMENT_HANDLE_ERROR; } return FRAGMENT_OK; -} \ No newline at end of file +} diff --git a/lib/Hoymiles/src/inverters/InverterAbstract.h b/lib/Hoymiles/src/inverters/InverterAbstract.h index 3d9929d7b..2a51079ba 100644 --- a/lib/Hoymiles/src/inverters/InverterAbstract.h +++ b/lib/Hoymiles/src/inverters/InverterAbstract.h @@ -58,6 +58,9 @@ class InverterAbstract { void setZeroYieldDayOnMidnight(const bool enabled); bool getZeroYieldDayOnMidnight() const; + void setClearEventlogOnMidnight(const bool enabled); + bool getClearEventlogOnMidnight() const; + void clearRxFragmentBuffer(); void addRxFragment(const uint8_t fragment[], const uint8_t len); uint8_t verifyAllFragments(CommandAbstract& cmd); @@ -102,6 +105,7 @@ class InverterAbstract { bool _zeroValuesIfUnreachable = false; bool _zeroYieldDayOnMidnight = false; + bool _clearEventlogOnMidnight = false; std::unique_ptr _alarmLogParser; std::unique_ptr _devInfoParser; @@ -109,4 +113,4 @@ class InverterAbstract { std::unique_ptr _powerCommandParser; std::unique_ptr _statisticsParser; std::unique_ptr _systemConfigParaParser; -}; \ No newline at end of file +}; diff --git a/lib/Hoymiles/src/inverters/README.md b/lib/Hoymiles/src/inverters/README.md index 6d6104a20..8d913deb5 100644 --- a/lib/Hoymiles/src/inverters/README.md +++ b/lib/Hoymiles/src/inverters/README.md @@ -7,7 +7,7 @@ | HM_4CH | HM-1000/1200/1500-4T | 1161 | | HMS_1CH | HMS-300/350/400/450/500-1T | 1124 | | HMS_1CHv2 | HMS-500-1T v2 | 1125 | -| HMS_2CH | HMS-600/700/800/900/1000-2T | 1144 | +| HMS_2CH | HMS-600/700/800/900/1000-2T | 1143, 1144 | | HMS_4CH | HMS-1600/1800/2000-4T | 1164 | | HMT_4CH | HMT-1600/1800/2000-4T | 1361 | | HMT_6CH | HMT-1800/2250-6T | 1382 | diff --git a/lib/Hoymiles/src/parser/AlarmLogParser.cpp b/lib/Hoymiles/src/parser/AlarmLogParser.cpp index 652159002..30b813647 100644 --- a/lib/Hoymiles/src/parser/AlarmLogParser.cpp +++ b/lib/Hoymiles/src/parser/AlarmLogParser.cpp @@ -1,22 +1,42 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022-2023 Thomas Basler and others + * Copyright (C) 2022-2024 Thomas Basler and others */ + +/* +This parser is used to parse the response of 'AlarmDataCommand'. + +Data structure: +* wcode: + * right 8 bit: Event ID + * bit 13: Start time = PM (12h has to be added to start time) + * bit 12: End time = PM (12h has to be added to start time) +* Start: 12h based start time of the event (PM indicator in wcode) +* End: 12h based start time of the event (PM indicator in wcode) + +00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 + 00 01 02 03 04 05 06 07 08 09 10 11 + |<-------------- First log entry -------------->| |<->| +----------------------------------------------------------------------------------------------------------------------------- +95 80 14 82 66 80 14 33 28 01 00 01 80 01 00 01 91 EA 91 EA 00 00 00 00 00 8F 65 -- -- -- -- -- +^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^ ^^ ^^ ^^ ^^^^^ ^^ +ID Source Addr Target Addr Idx ? wcode ? Start End ? ? ? ? wcode CRC8 +*/ #include "AlarmLogParser.h" #include "../Hoymiles.h" #include const std::array AlarmLogParser::_alarmMessages = { { { AlarmMessageType_t::ALL, 1, "Inverter start", "Wechselrichter gestartet", "L'onduleur a démarré" }, - { AlarmMessageType_t::ALL, 2, "Time calibration", "", "" }, + { AlarmMessageType_t::ALL, 2, "Time calibration", "Zeitabgleich", "" }, { AlarmMessageType_t::ALL, 3, "EEPROM reading and writing error during operation", "", "" }, { AlarmMessageType_t::ALL, 4, "Offline", "Offline", "Non connecté" }, - { AlarmMessageType_t::ALL, 11, "Grid voltage surge", "", "" }, - { AlarmMessageType_t::ALL, 12, "Grid voltage sharp drop", "", "" }, - { AlarmMessageType_t::ALL, 13, "Grid frequency mutation", "", "" }, - { AlarmMessageType_t::ALL, 14, "Grid phase mutation", "", "" }, - { AlarmMessageType_t::ALL, 15, "Grid transient fluctuation", "", "" }, + { AlarmMessageType_t::ALL, 11, "Grid voltage surge", "Netz: Überspannungsimpuls", "" }, + { AlarmMessageType_t::ALL, 12, "Grid voltage sharp drop", "Netz: Spannungseinbruch", "" }, + { AlarmMessageType_t::ALL, 13, "Grid frequency mutation", "Netz: Frequenzänderung", "" }, + { AlarmMessageType_t::ALL, 14, "Grid phase mutation", "Netz: Phasenänderung", "" }, + { AlarmMessageType_t::ALL, 15, "Grid transient fluctuation", "Netz: vorübergehende Schwankung", "" }, { AlarmMessageType_t::ALL, 36, "INV overvoltage or overcurrent", "", "" }, diff --git a/lib/Hoymiles/src/parser/DevInfoParser.cpp b/lib/Hoymiles/src/parser/DevInfoParser.cpp index 26e3c9d4f..0c2e15e8d 100644 --- a/lib/Hoymiles/src/parser/DevInfoParser.cpp +++ b/lib/Hoymiles/src/parser/DevInfoParser.cpp @@ -1,7 +1,32 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 - 2023 Thomas Basler and others + * Copyright (C) 2022 - 2024 Thomas Basler and others */ + +/* +This parser is used to parse the response of 'DevInfoAllCommand' and 'DevInfoSimpleCommand'. +It contains version information of the hardware and firmware. It can also be used to determine +the exact inverter type. + +Data structure (DevInfoAllCommand): + +00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 + 00 01 02 03 04 05 06 07 08 09 10 11 12 13 +------------------------------------------------------------------------------------------------------------------------------------------------- +95 80 14 82 66 80 14 33 28 81 27 1C 07 E5 04 01 07 2D 00 01 00 00 00 00 DF DD 1E -- -- -- -- -- +^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^ +ID Source Addr Target Addr Idx FW Version FW Year FW Month/Date FW Hour/Minute Bootloader ? ? CRC16 CRC8 + + +Data structure (DevInfoSimpleCommand): + +00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 + 00 01 02 03 04 05 06 07 08 09 10 11 12 13 +------------------------------------------------------------------------------------------------------------------------------------------------- +95 80 14 82 66 80 14 33 28 81 27 1C 10 12 71 01 01 00 0A 00 20 01 00 00 E5 F8 95 +^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^^^^ ^^^^^^^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^ +ID Source Addr Target Addr Idx FW Version HW Part No. HW Version ? ? ? CRC16 CRC8 +*/ #include "DevInfoParser.h" #include "../Hoymiles.h" #include @@ -37,6 +62,7 @@ const devInfo_t devInfo[] = { { { 0x10, 0x20, 0x71, ALL }, 500, "HMS-500-1T v2" }, // 02 { { 0x10, 0x21, 0x11, ALL }, 600, "HMS-600-2T" }, // 01 { { 0x10, 0x21, 0x41, ALL }, 800, "HMS-800-2T" }, // 00 + { { 0x10, 0x11, 0x41, ALL }, 800, "HMS-800-2T-LV" }, // 00 { { 0x10, 0x11, 0x51, ALL }, 900, "HMS-900-2T" }, // 01 { { 0x10, 0x21, 0x51, ALL }, 900, "HMS-900-2T" }, // 03 { { 0x10, 0x21, 0x71, ALL }, 1000, "HMS-1000-2T" }, // 05 diff --git a/lib/Hoymiles/src/parser/GridProfileParser.cpp b/lib/Hoymiles/src/parser/GridProfileParser.cpp index a7b912a9e..489565e19 100644 --- a/lib/Hoymiles/src/parser/GridProfileParser.cpp +++ b/lib/Hoymiles/src/parser/GridProfileParser.cpp @@ -2,6 +2,23 @@ /* * Copyright (C) 2023 - 2024 Thomas Basler and others */ + +/* +This parser is used to parse the response of 'GridOnProFilePara'. +It contains the whole grid profile of the inverter. + +Data structure: + +00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 + 00 01 02 03 04 05 06 07 08 09 10 11 12 13 + |<---------- Returns till the end of the payload ---------->| +--------------------------------------------------------------------------------------------------------------------------------------------------------------- +95 80 14 82 66 80 14 33 28 01 0A 00 20 01 00 0C 08 FC 07 A3 00 0F 09 E2 00 1E E6 -- -- -- -- -- +^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^^^^ ^^^^^ ^^ ^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^ +ID Source Addr Target Addr Idx Profile ID Profile Version Section ID Section Version Value Value Value Value CRC16 CRC8 + +The number of values depends on the respective section and its version. After the last value of a section follows the next section id. +*/ #include "GridProfileParser.h" #include "../Hoymiles.h" #include @@ -11,10 +28,10 @@ const std::array GridProfileParser::_profileTypes = { { { 0x02, 0x00, "US - NA_IEEE1547_240V" }, { 0x03, 0x00, "DE - DE_VDE4105_2018" }, - { 0x03, 0x01, "XX - unknown" }, + { 0x03, 0x01, "DE - DE_VDE4105_2011" }, { 0x0a, 0x00, "XX - EN 50549-1:2019" }, { 0x0c, 0x00, "AT - AT_TOR_Erzeuger_default" }, - { 0x0d, 0x04, "FR -" }, + { 0x0d, 0x04, "XX - NF_EN_50549-1:2019" }, { 0x10, 0x00, "ES - ES_RD1699" }, { 0x12, 0x00, "PL - EU_EN50438" }, { 0x29, 0x00, "NL - NL_NEN-EN50549-1_2019" }, @@ -82,7 +99,7 @@ constexpr frozen::map itemDefinition { 0x1f, make_value("Start of Frequency Watt Droop (Fstart)", "Hz", 100) }, { 0x20, make_value("FW Droop Slope (Kpower_Freq)", "Pn%/Hz", 10) }, { 0x21, make_value("Recovery Ramp Rate (RRR)", "Pn%/s", 100) }, - { 0x22, make_value("Recovery High Frequency (RVHF)", "Hz", 100) }, + { 0x22, make_value("Recovery High Frequency (RVHF)", "Hz", 10) }, { 0x23, make_value("Recovery Low Frequency (RVLF)", "Hz", 100) }, { 0x24, make_value("VW Function Activated", "bool", 1) }, { 0x25, make_value("Start of Voltage Watt Droop (Vstart)", "V", 10) }, diff --git a/lib/Hoymiles/src/parser/SystemConfigParaParser.cpp b/lib/Hoymiles/src/parser/SystemConfigParaParser.cpp index e866e8749..346b5d468 100644 --- a/lib/Hoymiles/src/parser/SystemConfigParaParser.cpp +++ b/lib/Hoymiles/src/parser/SystemConfigParaParser.cpp @@ -1,7 +1,21 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 - 2023 Thomas Basler and others + * Copyright (C) 2022 - 2024 Thomas Basler and others */ + +/* +This parser is used to parse the response of 'SystemConfigParaCommand'. +It contains the set inverter limit. + +Data structure: + +00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 + 00 01 02 03 04 05 06 07 08 09 10 11 12 13 +--------------------------------------------------------------------------------------------------------------------------------- +95 80 14 82 66 80 14 33 28 81 00 01 03 E8 00 00 03 E8 00 00 00 00 00 00 3C F8 2E -- -- -- -- -- +^^ ^^^^^^^^^^^ ^^^^^^^^^^^ ^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^ +ID Source Addr Target Addr Idx ? Limit percent ? ? ? ? ? CRC16 CRC8 +*/ #include "SystemConfigParaParser.h" #include "../Hoymiles.h" #include diff --git a/lib/SMLParser/sml.cpp b/lib/SMLParser/sml.cpp index 7a378f639..e330ef689 100644 --- a/lib/SMLParser/sml.cpp +++ b/lib/SMLParser/sml.cpp @@ -80,7 +80,7 @@ void pushListBuffer(unsigned char byte) void reduceList() { - if (currentLevel <= MAX_TREE_SIZE && nodes[currentLevel] > 0) + if (currentLevel < MAX_TREE_SIZE && nodes[currentLevel] > 0) nodes[currentLevel]--; } @@ -171,7 +171,7 @@ void checkMagicByte(unsigned char &byte) // Datatype Octet String setState(SML_HDATA, (byte & 0x0F) << 4); } - else if (byte >= 0xF0 /*&& byte <= 0xFF*/) { + else if (byte >= 0xF0) { /* Datatype List of ...*/ setState(SML_LISTEXTENDED, (byte & 0x0F) << 4); } @@ -189,7 +189,13 @@ void checkMagicByte(unsigned char &byte) } } -sml_states_t smlState(unsigned char ¤tByte) +void smlReset(void) +{ + len = 4; // expect start sequence + currentState = SML_START; +} + +sml_states_t smlState(unsigned char currentByte) { unsigned char size; if (len > 0) @@ -317,7 +323,7 @@ void smlOBISManufacturer(unsigned char *str, int maxSize) } } -void smlPow(double &val, signed char &scaler) +void smlPow(float &val, signed char &scaler) { if (scaler < 0) { while (scaler++) { @@ -372,7 +378,7 @@ void smlOBISByUnit(long long int &val, signed char &scaler, sml_units_t unit) } } -void smlOBISWh(double &wh) +void smlOBISWh(float &wh) { long long int val; smlOBISByUnit(val, sc, SML_WATT_HOUR); @@ -380,7 +386,7 @@ void smlOBISWh(double &wh) smlPow(wh, sc); } -void smlOBISW(double &w) +void smlOBISW(float &w) { long long int val; smlOBISByUnit(val, sc, SML_WATT); @@ -388,7 +394,7 @@ void smlOBISW(double &w) smlPow(w, sc); } -void smlOBISVolt(double &v) +void smlOBISVolt(float &v) { long long int val; smlOBISByUnit(val, sc, SML_VOLT); @@ -396,10 +402,26 @@ void smlOBISVolt(double &v) smlPow(v, sc); } -void smlOBISAmpere(double &a) +void smlOBISAmpere(float &a) { long long int val; smlOBISByUnit(val, sc, SML_AMPERE); a = val; smlPow(a, sc); } + +void smlOBISHertz(float &h) +{ + long long int val; + smlOBISByUnit(val, sc, SML_HERTZ); + h = val; + smlPow(h, sc); +} + +void smlOBISDegree(float &d) +{ + long long int val; + smlOBISByUnit(val, sc, SML_DEGREE); + d = val; + smlPow(d, sc); +} diff --git a/lib/SMLParser/sml.h b/lib/SMLParser/sml.h index ac6405dff..de844a4dc 100644 --- a/lib/SMLParser/sml.h +++ b/lib/SMLParser/sml.h @@ -92,15 +92,17 @@ typedef enum { SML_COUNT = 255 } sml_units_t; -sml_states_t smlState(unsigned char &byte); +void smlReset(void); +sml_states_t smlState(unsigned char byte); bool smlOBISCheck(const unsigned char *obis); void smlOBISManufacturer(unsigned char *str, int maxSize); void smlOBISByUnit(long long int &wh, signed char &scaler, sml_units_t unit); -// Be aware that double on Arduino UNO is just 32 bit -void smlOBISWh(double &wh); -void smlOBISW(double &w); -void smlOBISVolt(double &v); -void smlOBISAmpere(double &a); +void smlOBISWh(float &wh); +void smlOBISW(float &w); +void smlOBISVolt(float &v); +void smlOBISAmpere(float &a); +void smlOBISHertz(float &h); +void smlOBISDegree(float &d); #endif diff --git a/lib/SdmEnergyMeter/SDM.cpp b/lib/SdmEnergyMeter/SDM.cpp index 48de0648d..4e12d3c6e 100644 --- a/lib/SdmEnergyMeter/SDM.cpp +++ b/lib/SdmEnergyMeter/SDM.cpp @@ -1,6 +1,6 @@ /* Library for reading SDM 72/120/220/230/630 Modbus Energy meters. * Reading via Hardware or Software Serial library & rs232<->rs485 converter -* 2016-2022 Reaper7 (tested on wemos d1 mini->ESP8266 with Arduino 1.8.10 & 2.5.2 esp8266 core) +* 2016-2023 Reaper7 (tested on wemos d1 mini->ESP8266 with Arduino 1.8.10 & 2.5.2 esp8266 core) * crc calculation by Jaime García (https://github.com/peninquen/Modbus-Energy-Monitor-Arduino/) */ //------------------------------------------------------------------------------ @@ -60,7 +60,7 @@ void SDM::begin(void) { #endif #else #if defined ( ESP8266 ) || defined ( ESP32 ) - sdmSer.begin(_baud, (SoftwareSerialConfig)_config, _rx_pin, _tx_pin); + sdmSer.begin(_baud, (EspSoftwareSerial::Config)_config, _rx_pin, _tx_pin); #else sdmSer.begin(_baud); #endif @@ -77,44 +77,67 @@ void SDM::begin(void) { } float SDM::readVal(uint16_t reg, uint8_t node) { - uint16_t temp; - unsigned long resptime; - uint8_t sdmarr[FRAMESIZE] = {node, SDM_B_02, 0, 0, SDM_B_05, SDM_B_06, 0, 0, 0}; - float res = NAN; - uint16_t readErr = SDM_ERR_NO_ERROR; - - sdmarr[2] = highByte(reg); - sdmarr[3] = lowByte(reg); - - temp = calculateCRC(sdmarr, FRAMESIZE - 3); //calculate out crc only from first 6 bytes - - sdmarr[6] = lowByte(temp); - sdmarr[7] = highByte(temp); - -#if !defined ( USE_HARDWARESERIAL ) - sdmSer.listen(); //enable softserial rx interrupt -#endif + startReadVal(reg, node); - flush(); //read serial if any old data is available + uint16_t readErr = SDM_ERR_STILL_WAITING; - dereSet(HIGH); //transmit to SDM -> DE Enable, /RE Disable (for control MAX485) + while (readErr == SDM_ERR_STILL_WAITING) { + readErr = readValReady(node); + delay(1); + } - delay(2); //fix for issue (nan reading) by sjfaustino: https://github.com/reaper7/SDM_Energy_Meter/issues/7#issuecomment-272111524 + if (readErr != SDM_ERR_NO_ERROR) { //if error then copy temp error value to global val and increment global error counter + readingerrcode = readErr; + readingerrcount++; + } else { + ++readingsuccesscount; + } - sdmSer.write(sdmarr, FRAMESIZE - 1); //send 8 bytes + if (readErr == SDM_ERR_NO_ERROR) { + return decodeFloatValue(); + } - sdmSer.flush(); //clear out tx buffer + constexpr float res = NAN; + return (res); +} - dereSet(LOW); //receive from SDM -> DE Disable, /RE Enable (for control MAX485) +void SDM::startReadVal(uint16_t reg, uint8_t node, uint8_t functionCode) { + uint8_t data[] = { + node, // Address + functionCode, // Modbus function + highByte(reg), // Start address high byte + lowByte(reg), // Start address low byte + SDM_B_05, // Number of points high byte + SDM_B_06, // Number of points low byte + 0, // Checksum low byte + 0}; // Checksum high byte + + constexpr size_t messageLength = sizeof(data) / sizeof(data[0]); + modbusWrite(data, messageLength); +} - resptime = millis(); +uint16_t SDM::readValReady(uint8_t node, uint8_t functionCode) { + uint16_t readErr = SDM_ERR_NO_ERROR; + if (sdmSer.available() < FRAMESIZE && ((millis() - resptime) < msturnaround)) + { + return SDM_ERR_STILL_WAITING; + } while (sdmSer.available() < FRAMESIZE) { - if (millis() - resptime > msturnaround) { + if ((millis() - resptime) > msturnaround) { readErr = SDM_ERR_TIMEOUT; //err debug (4) + + if (sdmSer.available() == 5) { + for(int n=0; n<5; n++) { + sdmarr[n] = sdmSer.read(); + } + if (validChecksum(sdmarr, 5)) { + readErr = sdmarr[2]; + } + } break; } - yield(); + delay(1); } if (readErr == SDM_ERR_NO_ERROR) { //if no timeout... @@ -125,14 +148,10 @@ float SDM::readVal(uint16_t reg, uint8_t node) { sdmarr[n] = sdmSer.read(); } - if (sdmarr[0] == node && sdmarr[1] == SDM_B_02 && sdmarr[2] == SDM_REPLY_BYTE_COUNT) { - - if ((calculateCRC(sdmarr, FRAMESIZE - 2)) == ((sdmarr[8] << 8) | sdmarr[7])) { //calculate crc from first 7 bytes and compare with received crc (bytes 7 & 8) - ((uint8_t*)&res)[3]= sdmarr[3]; - ((uint8_t*)&res)[2]= sdmarr[4]; - ((uint8_t*)&res)[1]= sdmarr[5]; - ((uint8_t*)&res)[0]= sdmarr[6]; - } else { + if (sdmarr[0] == node && + sdmarr[1] == functionCode && + sdmarr[2] == SDM_REPLY_BYTE_COUNT) { + if (!validChecksum(sdmarr, FRAMESIZE)) { readErr = SDM_ERR_CRC_ERROR; //err debug (1) } @@ -159,12 +178,95 @@ float SDM::readVal(uint16_t reg, uint8_t node) { } #if !defined ( USE_HARDWARESERIAL ) - sdmSer.stopListening(); //disable softserial rx interrupt +// sdmSer.stopListening(); //disable softserial rx interrupt #endif + return readErr; +} + +float SDM::decodeFloatValue() const { + if (validChecksum(sdmarr, FRAMESIZE)) { + float res{}; + ((uint8_t*)&res)[3]= sdmarr[3]; + ((uint8_t*)&res)[2]= sdmarr[4]; + ((uint8_t*)&res)[1]= sdmarr[5]; + ((uint8_t*)&res)[0]= sdmarr[6]; + return res; + } + constexpr float res = NAN; + return res; +} + +float SDM::readHoldingRegister(uint16_t reg, uint8_t node) { + startReadVal(reg, node, SDM_READ_HOLDING_REGISTER); + + uint16_t readErr = SDM_ERR_STILL_WAITING; + + while (readErr == SDM_ERR_STILL_WAITING) { + delay(1); + readErr = readValReady(node, SDM_READ_HOLDING_REGISTER); + } + + if (readErr != SDM_ERR_NO_ERROR) { //if error then copy temp error value to global val and increment global error counter + readingerrcode = readErr; + readingerrcount++; + } else { + ++readingsuccesscount; + } + + if (readErr == SDM_ERR_NO_ERROR) { + return decodeFloatValue(); + } + constexpr float res = NAN; return (res); } +bool SDM::writeHoldingRegister(float value, uint16_t reg, uint8_t node) { + { + uint8_t data[] = { + node, // Address + SDM_WRITE_HOLDING_REGISTER, // Function + highByte(reg), // Starting Address High + lowByte(reg), // Starting Address Low + SDM_B_05, // Number of Registers High + SDM_B_06, // Number of Registers Low + 4, // Byte count + ((uint8_t*)&value)[3], + ((uint8_t*)&value)[2], + ((uint8_t*)&value)[1], + ((uint8_t*)&value)[0], + 0, 0}; + + constexpr size_t messageLength = sizeof(data) / sizeof(data[0]); + modbusWrite(data, messageLength); + } + uint16_t readErr = SDM_ERR_STILL_WAITING; + while (readErr == SDM_ERR_STILL_WAITING) { + delay(1); + readErr = readValReady(node, SDM_READ_HOLDING_REGISTER); + } + + if (readErr != SDM_ERR_NO_ERROR) { //if error then copy temp error value to global val and increment global error counter + readingerrcode = readErr; + readingerrcount++; + } else { + ++readingsuccesscount; + } + + return readErr == SDM_ERR_NO_ERROR; +} + +uint32_t SDM::getSerialNumber(uint8_t node) { + uint32_t res{}; + readHoldingRegister(SDM_HOLDING_SERIAL_NUMBER, node); +// if (getErrCode() == SDM_ERR_NO_ERROR) { + for (size_t i = 0; i < 4; ++i) { + res = (res << 8) + sdmarr[3 + i]; + } +// } + return res; +} + uint16_t SDM::getErrCode(bool _clear) { uint16_t _tmp = readingerrcode; if (_clear == true) @@ -224,7 +326,7 @@ uint16_t SDM::getMsTimeout() { return (mstimeout); } -uint16_t SDM::calculateCRC(uint8_t *array, uint8_t len) { +uint16_t SDM::calculateCRC(const uint8_t *array, uint8_t len) const { uint16_t _crc, _flag; _crc = 0xFFFF; for (uint8_t i = 0; i < len; i++) { @@ -241,10 +343,17 @@ uint16_t SDM::calculateCRC(uint8_t *array, uint8_t len) { void SDM::flush(unsigned long _flushtime) { unsigned long flushstart = millis(); - while (sdmSer.available() || (millis() - flushstart < _flushtime)) { - if (sdmSer.available()) //read serial if any old data is available + sdmSer.flush(); + int available = sdmSer.available(); + while (available > 0 || ((millis() - flushstart) < _flushtime)) { + while (available > 0) { + --available; + flushstart = millis(); + //read serial if any old data is available sdmSer.read(); + } delay(1); + available = sdmSer.available(); } } @@ -252,3 +361,58 @@ void SDM::dereSet(bool _state) { if (_dere_pin != NOT_A_PIN) digitalWrite(_dere_pin, _state); //receive from SDM -> DE Disable, /RE Enable (for control MAX485) } + +bool SDM::validChecksum(const uint8_t* data, size_t messageLength) const { + const uint16_t temp = calculateCRC(data, messageLength - 2); //calculate out crc only from first 6 bytes + + return data[messageLength - 2] == lowByte(temp) && + data[messageLength - 1] == highByte(temp); + +} + +void SDM::modbusWrite(uint8_t* data, size_t messageLength) { + const uint16_t temp = calculateCRC(data, messageLength - 2); //calculate out crc only from first 6 bytes + + data[messageLength - 2] = lowByte(temp); + data[messageLength - 1] = highByte(temp); + +#if !defined ( USE_HARDWARESERIAL ) + sdmSer.listen(); //enable softserial rx interrupt +#endif + + flush(); //read serial if any old data is available + + if (_dere_pin != NOT_A_PIN) { + dereSet(HIGH); //transmit to SDM -> DE Enable, /RE Disable (for control MAX485) + + delay(1); //fix for issue (nan reading) by sjfaustino: https://github.com/reaper7/SDM_Energy_Meter/issues/7#issuecomment-272111524 + + // Need to wait for all bytes in TX buffer are sent. + // N.B. flush() on serial port does often only clear the send buffer, not wait till all is sent. + const unsigned long waitForBytesSent_ms = (messageLength * 11000) / _baud + 1; + resptime = millis() + waitForBytesSent_ms; + } + +#if !defined ( USE_HARDWARESERIAL ) + // prevent scheduler from messing up the serial message. this task shall only + // be scheduled after the whole serial message was transmitted. + vTaskSuspendAll(); +#endif + + sdmSer.write(data, messageLength); //send 8 bytes + +#if !defined ( USE_HARDWARESERIAL ) + xTaskResumeAll(); +#endif + + if (_dere_pin != NOT_A_PIN) { + const int32_t timeleft = (int32_t) (resptime - millis()); + if (timeleft > 0) { + delay(timeleft); //clear out tx buffer + } + dereSet(LOW); //receive from SDM -> DE Disable, /RE Enable (for control MAX485) + flush(); + } + + resptime = millis(); +} diff --git a/lib/SdmEnergyMeter/SDM.h b/lib/SdmEnergyMeter/SDM.h index 7a24d1245..dd9c5c1af 100644 --- a/lib/SdmEnergyMeter/SDM.h +++ b/lib/SdmEnergyMeter/SDM.h @@ -1,6 +1,6 @@ /* Library for reading SDM 72/120/220/230/630 Modbus Energy meters. * Reading via Hardware or Software Serial library & rs232<->rs485 converter -* 2016-2022 Reaper7 (tested on wemos d1 mini->ESP8266 with Arduino 1.8.10 & 2.5.2 esp8266 core) +* 2016-2023 Reaper7 (tested on wemos d1 mini->ESP8266 with Arduino 1.8.10 & 2.5.2 esp8266 core) * crc calculation by Jaime García (https://github.com/peninquen/Modbus-Energy-Monitor-Arduino/) */ //------------------------------------------------------------------------------ @@ -66,36 +66,47 @@ #endif #if !defined ( WAITING_TURNAROUND_DELAY ) - #define WAITING_TURNAROUND_DELAY 200 // time in ms to wait for process current request + #define WAITING_TURNAROUND_DELAY 500 // time in ms to wait for process current request #endif #if !defined ( RESPONSE_TIMEOUT ) - #define RESPONSE_TIMEOUT 500 // time in ms to wait for return response from all devices before next request + #define RESPONSE_TIMEOUT 10 // time in ms to wait for return response from all devices before next request #endif #if !defined ( SDM_MIN_DELAY ) - #define SDM_MIN_DELAY 20 // minimum value (in ms) for WAITING_TURNAROUND_DELAY and RESPONSE_TIMEOUT + #define SDM_MIN_DELAY 1 // minimum value (in ms) for WAITING_TURNAROUND_DELAY and RESPONSE_TIMEOUT #endif #if !defined ( SDM_MAX_DELAY ) - #define SDM_MAX_DELAY 5000 // maximum value (in ms) for WAITING_TURNAROUND_DELAY and RESPONSE_TIMEOUT + #define SDM_MAX_DELAY 20 // maximum value (in ms) for WAITING_TURNAROUND_DELAY and RESPONSE_TIMEOUT #endif //------------------------------------------------------------------------------ #define SDM_ERR_NO_ERROR 0 // no error -#define SDM_ERR_CRC_ERROR 1 // crc error -#define SDM_ERR_WRONG_BYTES 2 // bytes b0,b1 or b2 wrong -#define SDM_ERR_NOT_ENOUGHT_BYTES 3 // not enough bytes from sdm -#define SDM_ERR_TIMEOUT 4 // timeout +#define SDM_ERR_ILLEGAL_FUNCTION 1 +#define SDM_ERR_ILLEGAL_DATA_ADDRESS 2 +#define SDM_ERR_ILLEGAL_DATA_VALUE 3 +#define SDM_ERR_SLAVE_DEVICE_FAILURE 5 + +#define SDM_ERR_CRC_ERROR 11 // crc error +#define SDM_ERR_WRONG_BYTES 12 // bytes b0,b1 or b2 wrong +#define SDM_ERR_NOT_ENOUGHT_BYTES 13 // not enough bytes from sdm +#define SDM_ERR_TIMEOUT 14 // timeout +#define SDM_ERR_EXCEPTION 15 +#define SDM_ERR_STILL_WAITING 16 //------------------------------------------------------------------------------ +#define SDM_READ_HOLDING_REGISTER 0x03 +#define SDM_READ_INPUT_REGISTER 0x04 +#define SDM_WRITE_HOLDING_REGISTER 0x10 + #define FRAMESIZE 9 // size of out/in array #define SDM_REPLY_BYTE_COUNT 0x04 // number of bytes with data #define SDM_B_01 0x01 // BYTE 1 -> slave address (default value 1 read from node 1) -#define SDM_B_02 0x04 // BYTE 2 -> function code (default value 0x04 read from 3X input registers) +#define SDM_B_02 SDM_READ_INPUT_REGISTER // BYTE 2 -> function code (default value 0x04 read from 3X input registers) #define SDM_B_05 0x00 // BYTE 5 #define SDM_B_06 0x02 // BYTE 6 // BYTES 3 & 4 (BELOW) @@ -151,6 +162,8 @@ #define SDM_MAXIMUM_TOTAL_SYSTEM_VA_DEMAND 0x0066 // VA | 1 | | | | | | | #define SDM_NEUTRAL_CURRENT_DEMAND 0x0068 // A | 1 | | | | | | | #define SDM_MAXIMUM_NEUTRAL_CURRENT 0x006A // A | 1 | | | | | | | +#define SDM_REACTIVE_POWER_DEMAND 0x006C // VAr | 1 | | | | | | | +#define SDM_MAXIMUM_REACTIVE_POWER_DEMAND 0x006E // VAr | 1 | | | | | | | #define SDM_LINE_1_TO_LINE_2_VOLTS 0x00C8 // V | 1 | | | | | | 1 | #define SDM_LINE_2_TO_LINE_3_VOLTS 0x00CA // V | 1 | | | | | | 1 | #define SDM_LINE_3_TO_LINE_1_VOLTS 0x00CC // V | 1 | | | | | | 1 | @@ -199,7 +212,10 @@ #define SDM_CURRENT_RESETTABLE_TOTAL_REACTIVE_ENERGY 0x0182 // kVArh | | 1 | | | | | | #define SDM_CURRENT_RESETTABLE_IMPORT_ENERGY 0x0184 // kWh | | | | | | 1 | 1 | #define SDM_CURRENT_RESETTABLE_EXPORT_ENERGY 0x0186 // kWh | | | | | | 1 | 1 | +#define SDM_CURRENT_RESETTABLE_IMPORT_REACTIVE_ENERGY 0x0188 // kVArh | | | | | | 1 | 1 | +#define SDM_CURRENT_RESETTABLE_EXPORT_REACTIVE_ENERGY 0x018A // kVArh | | | | | | 1 | 1 | #define SDM_NET_KWH 0x018C // kWh | | | | | | | 1 | +#define SDM_NET_KVARH 0x018E // kVArh | | | | | | | 1 | #define SDM_IMPORT_POWER 0x0500 // W | | | | | | 1 | 1 | #define SDM_EXPORT_POWER 0x0502 // W | | | | | | 1 | 1 | //--------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -229,6 +245,78 @@ //#define DEVNAME_POWER 0x0004 // W | 1 | //--------------------------------------------------------------------------------------------------------- + +//--------------------------------------------------------------------------------------------------------- +// REGISTERS LIST FOR DEVICE SETTINGS | +//--------------------------------------------------------------------------------------------------------- +// REGISTER NAME REGISTER ADDRESS UNIT | DEVNAME | +//--------------------------------------------------------------------------------------------------------- + +// Read minutes into first demand calculation. +// When the Demand Time reaches the Demand Period +// then the demand values are valid. +#define SDM_HOLDING_DEMAND_TIME 0x0000 + +// Write demand period: 0~60 minutes. +// Default 60. +// Range: 0~60, 0 means function disabled +#define SDM_HOLDING_DEMAND_PERIOD 0x0002 + +// Write relay on period in milliseconds: +// 60, 100 or 200 ms. +// default: 100 ms +#define SDM_HOLDING_RELAY_PULSE_WIDTH 0x000C + +// Parity / stop bit settings: +// 0 = One stop bit and no parity, default. +// 1 = One stop bit and even parity. +// 2 = One s top bit and odd parity. +// 3 = Two stop bits and no parity. +// Requires a restart to become effective. +#define SDM_HOLDING_NETWORK_PARITY_STOP 0x0012 + +// Ranges from 1 to 247. Default ID is 1. +#define SDM_HOLDING_METER_ID 0x0014 + +// Write the network port baud rate for MODBUS Protocol, where: +/* +SDM120 / SDM230: + 0 = 2400 baud (default) + 1 = 4800 baud + 2 = 9600 baud + 5 = 1200 baud + +SDM320 / SDM530Y: + 0 = 2400 baud + 1 = 4800 baud + 2 = 9600 baud (default) + 5 = 1200 band + +SDM630 / SDM72 / SDM72V2: + 0 = 2400 baud + 1 = 4800 baud + 2 = 9600 baud (default) + 3 = 19200 baud + 4 = 38400 baud +*/ +#define SDM_HOLDING_BAUD_RATE 0x001C + +// Write MODBUS Protocol input parameter for pulse out 1: +// 1: Import active energy +// 2: Import + export (total) active energy +// 4: Export active energy (default). +// 5: Import reactive energy +// 6: Import + export (total) reactive energy +// 8: Export reactive energy +#define SDM_HOLDING_PULSE_1_OUTPUT_MODE 0x0056 + + + +#define SDM_HOLDING_SERIAL_NUMBER 0xFC00 +#define SDM_HOLDING_SOFTWARE_VERSION 0xFC03 + + + //----------------------------------------------------------------------------------------------------------------------------------------------------------- class SDM { @@ -252,6 +340,16 @@ class SDM { void begin(void); float readVal(uint16_t reg, uint8_t node = SDM_B_01); // read value from register = reg and from deviceId = node + void startReadVal(uint16_t reg, uint8_t node = SDM_B_01, uint8_t functionCode = SDM_B_02); // Start sending out the request to read a register from a specific node (allows for async access) + uint16_t readValReady(uint8_t node = SDM_B_01, uint8_t functionCode = SDM_B_02); // Check to see if a reply is ready reading from a node (allow for async access) + float decodeFloatValue() const; + + float readHoldingRegister(uint16_t reg, uint8_t node = SDM_B_01); + bool writeHoldingRegister(float value, uint16_t reg, uint8_t node = SDM_B_01); + + uint32_t getSerialNumber(uint8_t node = SDM_B_01); + + uint16_t getErrCode(bool _clear = false); // return last errorcode (optional clear this value, default flase) uint32_t getErrCount(bool _clear = false); // return total errors count (optional clear this value, default flase) uint32_t getSuccCount(bool _clear = false); // return total success count (optional clear this value, default false) @@ -264,6 +362,11 @@ class SDM { uint16_t getMsTimeout(); // get current value of RESPONSE_TIMEOUT (ms) private: + + bool validChecksum(const uint8_t* data, size_t messageLength) const; + + void modbusWrite(uint8_t* data, size_t messageLength); + #if defined ( USE_HARDWARESERIAL ) HardwareSerial& sdmSer; #else @@ -292,7 +395,9 @@ class SDM { uint16_t mstimeout = RESPONSE_TIMEOUT; uint32_t readingerrcount = 0; // total errors counter uint32_t readingsuccesscount = 0; // total success counter - uint16_t calculateCRC(uint8_t *array, uint8_t len); + unsigned long resptime = 0; + uint8_t sdmarr[FRAMESIZE] = {}; + uint16_t calculateCRC(const uint8_t *array, uint8_t len) const; void flush(unsigned long _flushtime = 0); // read serial if any old data is available or for a given time in ms void dereSet(bool _state = LOW); // for control MAX485 DE/RE pins, LOW receive from SDM, HIGH transmit to SDM }; diff --git a/lib/SdmEnergyMeter/SDM_Config_User.h b/lib/SdmEnergyMeter/SDM_Config_User.h index 01ff257ff..bd8d4358c 100644 --- a/lib/SdmEnergyMeter/SDM_Config_User.h +++ b/lib/SdmEnergyMeter/SDM_Config_User.h @@ -1,6 +1,6 @@ /* Library for reading SDM 72/120/220/230/630 Modbus Energy meters. * Reading via Hardware or Software Serial library & rs232<->rs485 converter -* 2016-2022 Reaper7 (tested on wemos d1 mini->ESP8266 with Arduino 1.8.10 & 2.5.2 esp8266 core) +* 2016-2023 Reaper7 (tested on wemos d1 mini->ESP8266 with Arduino 1.8.10 & 2.5.2 esp8266 core) * crc calculation by Jaime García (https://github.com/peninquen/Modbus-Energy-Monitor-Arduino/) */ @@ -14,7 +14,7 @@ * define or undefine USE_HARDWARESERIAL (uncomment only one or none) */ //#undef USE_HARDWARESERIAL -#define USE_HARDWARESERIAL +//#define USE_HARDWARESERIAL //------------------------------------------------------------------------------ @@ -32,7 +32,7 @@ #if defined ( USE_HARDWARESERIAL ) #if defined ( ESP32 ) #define SDM_RX_PIN 13 - #define SDM_TX_PIN 32 + #define SDM_TX_PIN 15 #endif #else #if defined ( ESP8266 ) || defined ( ESP32 ) diff --git a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp index 4b258e75b..9de8151df 100644 --- a/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp +++ b/lib/VeDirectFrameHandler/VeDirectFrameHandler.cpp @@ -66,6 +66,7 @@ void VeDirectFrameHandler::init(char const* who, int8_t rx, int8_t tx, Print* msgOut, bool verboseLogging, uint8_t hwSerialPort) { _vedirectSerial = std::make_unique(hwSerialPort); + _vedirectSerial->setRxBufferSize(512); // increased from default (256) to 512 Byte to avoid overflow _vedirectSerial->end(); // make sure the UART will be re-initialized _vedirectSerial->begin(19200, SERIAL_8N1, rx, tx); _vedirectSerial->flush(); diff --git a/partitions_custom_4mb.csv b/partitions_custom_4mb.csv index d18ab18fa..41537eb8d 100644 --- a/partitions_custom_4mb.csv +++ b/partitions_custom_4mb.csv @@ -1,6 +1,5 @@ # Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xe000, 0x2000, -app0, app, ota_0, 0x10000, 0x1E0000, -app1, app, ota_1, 0x1F0000, 0x1E0000, +app0, app, ota_0, 0x10000, 0x3C0000, spiffs, data, spiffs, 0x3D0000, 0x30000, \ No newline at end of file diff --git a/partitions_custom_8mb.csv b/partitions_custom_8mb.csv new file mode 100644 index 000000000..88b79e594 --- /dev/null +++ b/partitions_custom_8mb.csv @@ -0,0 +1,6 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +app0, app, ota_0, 0x10000, 0x3C0000, +spiffs, data, spiffs, 0x3D0000, 0x30000, +app1, app, ota_1, 0x400000, 0x3C0000, diff --git a/patches/async_tcp/event_queue_size.patch b/patches/async_tcp/event_queue_size.patch deleted file mode 100644 index 1280d46a8..000000000 --- a/patches/async_tcp/event_queue_size.patch +++ /dev/null @@ -1,26 +0,0 @@ -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/pio-scripts/auto_firmware_version.py b/pio-scripts/auto_firmware_version.py index c5aa62432..c1fa9771d 100644 --- a/pio-scripts/auto_firmware_version.py +++ b/pio-scripts/auto_firmware_version.py @@ -3,33 +3,27 @@ # Copyright (C) 2022 Thomas Basler and others # import os -import pkg_resources Import("env") -required_pkgs = {'dulwich'} -installed_pkgs = {pkg.key for pkg in pkg_resources.working_set} -missing_pkgs = required_pkgs - installed_pkgs - -if missing_pkgs: +try: + from dulwich import porcelain +except ModuleNotFoundError: env.Execute('"$PYTHONEXE" -m pip install dulwich') - -from dulwich import porcelain + from dulwich import porcelain def updateFileIfChanged(filename, content): mustUpdate = True try: - fp = open(filename, "rb") - if fp.read() == content: - mustUpdate = False - fp.close() + with open(filename, "rb") as fp: + if fp.read() == content: + mustUpdate = False except: pass if mustUpdate: - fp = open(filename, "wb") - fp.write(content) - fp.close() + with open(filename, "wb") as fp: + fp.write(content) return mustUpdate diff --git a/platformio.ini b/platformio.ini index b70c3207a..f9435984c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -9,17 +9,17 @@ ; https://docs.platformio.org/page/projectconf.html [platformio] -default_envs = generic_esp32 +default_envs = generic_esp32s3_usb extra_configs = platformio_override.ini [env] ; Make sure to NOT add any spaces in the custom_ci_action property ; (also the position in the file is important) -custom_ci_action = generic,generic_esp32,generic_esp32s3,generic_esp32s3_usb +custom_ci_action = generic_esp32_4mb_no_ota,generic_esp32_8mb,generic_esp32s3,generic_esp32s3_usb framework = arduino -platform = espressif32@6.6.0 +platform = espressif32@6.8.1 build_flags = -DPIOENV=\"$PIOENV\" @@ -27,6 +27,7 @@ build_flags = -D_TASK_THREAD_SAFE=1 -DCONFIG_ASYNC_TCP_EVENT_QUEUE_SIZE=128 -DCONFIG_ASYNC_TCP_QUEUE_SIZE=128 + -DEMC_TASK_STACK_SIZE=6400 -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 @@ -38,23 +39,22 @@ build_unflags = -std=gnu++11 lib_deps = - mathieucarbou/ESP Async WebServer @ 2.9.5 - bblanchon/ArduinoJson @ 7.0.4 - https://github.com/bertmelis/espMqttClient.git#v1.6.0 - nrf24/RF24 @ 1.4.8 + mathieucarbou/ESPAsyncWebServer @ 3.1.2 + bblanchon/ArduinoJson @ 7.1.0 + https://github.com/bertmelis/espMqttClient.git#v1.7.0 + nrf24/RF24 @ 1.4.9 olikraus/U8g2 @ 2.35.19 buelowp/sunset @ 1.1.7 https://github.com/arkhipenko/TaskScheduler#testing https://github.com/coryjfowler/MCP_CAN_lib - plerup/EspSoftwareSerial @ ^8.0.1 - https://github.com/dok-net/ghostl @ ^1.0.1 + plerup/EspSoftwareSerial @ ^8.2.0 extra_scripts = pre:pio-scripts/auto_firmware_version.py pre:pio-scripts/patch_apply.py post:pio-scripts/create_factory_bin.py -board_build.partitions = partitions_custom_4mb.csv +board_build.partitions = partitions_custom_8mb.csv board_build.filesystem = littlefs board_build.embed_files = webapp_dist/index.html.gz @@ -64,7 +64,7 @@ board_build.embed_files = webapp_dist/js/app.js.gz webapp_dist/site.webmanifest -custom_patches = async_tcp +custom_patches = monitor_filters = esp32_exception_decoder, time, log2file, colorize monitor_speed = 115200 @@ -75,7 +75,13 @@ upload_protocol = esptool ; upload_port = COM4 -[env:generic_esp32] +[env:generic_esp32_4mb_no_ota] +board = esp32dev +build_flags = ${env.build_flags} +board_build.partitions = partitions_custom_4mb.csv + + +[env:generic_esp32_8mb] board = esp32dev build_flags = ${env.build_flags} @@ -92,12 +98,14 @@ build_flags = ${env.build_flags} [env:generic_esp32c3] board = esp32-c3-devkitc-02 +board_build.partitions = partitions_custom_4mb.csv custom_patches = ${env.custom_patches} build_flags = ${env.build_flags} [env:generic_esp32c3_usb] board = esp32-c3-devkitc-02 +board_build.partitions = partitions_custom_4mb.csv custom_patches = ${env.custom_patches} build_flags = ${env.build_flags} -DARDUINO_USB_MODE=1 @@ -117,17 +125,6 @@ build_flags = ${env.build_flags} -DARDUINO_USB_CDC_ON_BOOT=1 -[env:generic] -board = esp32dev -build_flags = ${env.build_flags} - -DHOYMILES_PIN_MISO=19 - -DHOYMILES_PIN_MOSI=23 - -DHOYMILES_PIN_SCLK=18 - -DHOYMILES_PIN_IRQ=16 - -DHOYMILES_PIN_CE=4 - -DHOYMILES_PIN_CS=5 - - [env:olimex_esp32_poe] ; https://www.olimex.com/Products/IoT/ESP32/ESP32-POE/open-source-hardware board = esp32-poe @@ -144,6 +141,7 @@ build_flags = ${env.build_flags} [env:olimex_esp32_evb] ; https://www.olimex.com/Products/IoT/ESP32/ESP32-EVB/open-source-hardware board = esp32-evb +board_build.partitions = partitions_custom_4mb.csv build_flags = ${env.build_flags} -DHOYMILES_PIN_MISO=15 -DHOYMILES_PIN_MOSI=2 @@ -156,16 +154,17 @@ build_flags = ${env.build_flags} [env:d1_mini_esp32] board = wemos_d1_mini32 -build_flags = - ${env.build_flags} - -DHOYMILES_PIN_MISO=19 - -DHOYMILES_PIN_MOSI=23 - -DHOYMILES_PIN_SCLK=18 - -DHOYMILES_PIN_IRQ=16 - -DHOYMILES_PIN_CE=17 - -DHOYMILES_PIN_CS=5 - -DVICTRON_PIN_TX=21 - -DVICTRON_PIN_RX=22 +board_build.partitions = partitions_custom_4mb.csv +build_flags = + ${env.build_flags} + -DHOYMILES_PIN_MISO=19 + -DHOYMILES_PIN_MOSI=23 + -DHOYMILES_PIN_SCLK=18 + -DHOYMILES_PIN_IRQ=16 + -DHOYMILES_PIN_CE=17 + -DHOYMILES_PIN_CS=5 + -DVICTRON_PIN_TX=21 + -DVICTRON_PIN_RX=22 -DPYLONTECH_PIN_RX=27 -DPYLONTECH_PIN_TX=14 -DHUAWEI_PIN_MISO=12 @@ -178,6 +177,7 @@ build_flags = [env:wt32_eth01] ; http://www.wireless-tag.com/portfolio/wt32-eth01/ board = wt32-eth01 +board_build.partitions = partitions_custom_4mb.csv build_flags = ${env.build_flags} -DHOYMILES_PIN_MISO=4 -DHOYMILES_PIN_MOSI=2 @@ -204,6 +204,7 @@ build_flags = ${env.build_flags} ; https://www.makershop.de/plattformen/esp8266/wemos-lolin32/ ; https://www.az-delivery.de/products/esp32-lolin-lolin32 board = lolin32_lite +board_build.partitions = partitions_custom_4mb.csv build_flags = ${env.build_flags} -DHOYMILES_PIN_MISO=19 -DHOYMILES_PIN_MOSI=23 @@ -214,6 +215,7 @@ build_flags = ${env.build_flags} [env:lolin_s2_mini] board = lolin_s2_mini +board_build.partitions = partitions_custom_4mb.csv build_flags = ${env.build_flags} -DHOYMILES_PIN_MISO=13 -DHOYMILES_PIN_MOSI=11 diff --git a/src/Battery.cpp b/src/Battery.cpp index 794afa51c..79ba70020 100644 --- a/src/Battery.cpp +++ b/src/Battery.cpp @@ -5,6 +5,7 @@ #include "JkBmsController.h" #include "VictronSmartShunt.h" #include "MqttBattery.h" +#include "PytesCanReceiver.h" BatteryClass Battery; @@ -57,6 +58,9 @@ void BatteryClass::updateSettings() case 3: _upProvider = std::make_unique(); break; + case 4: + _upProvider = std::make_unique(); + break; default: MessageOutput.printf("[Battery] Unknown provider: %d\r\n", config.Battery.Provider); return; diff --git a/src/BatteryCanReceiver.cpp b/src/BatteryCanReceiver.cpp new file mode 100644 index 000000000..aca563bbb --- /dev/null +++ b/src/BatteryCanReceiver.cpp @@ -0,0 +1,175 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "BatteryCanReceiver.h" +#include "MessageOutput.h" +#include "PinMapping.h" +#include + +bool BatteryCanReceiver::init(bool verboseLogging, char const* providerName) +{ + _verboseLogging = verboseLogging; + _providerName = providerName; + + MessageOutput.printf("[%s] Initialize interface...\r\n", + _providerName); + + const PinMapping_t& pin = PinMapping.get(); + MessageOutput.printf("[%s] Interface rx = %d, tx = %d\r\n", + _providerName, pin.battery_rx, pin.battery_tx); + + if (pin.battery_rx < 0 || pin.battery_tx < 0) { + MessageOutput.printf("[%s] Invalid pin config\r\n", + _providerName); + return false; + } + + auto tx = static_cast(pin.battery_tx); + auto rx = static_cast(pin.battery_rx); + twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(tx, rx, TWAI_MODE_NORMAL); + + // Initialize configuration structures using macro initializers + twai_timing_config_t t_config = TWAI_TIMING_CONFIG_500KBITS(); + twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL(); + + // Install TWAI driver + esp_err_t twaiLastResult = twai_driver_install(&g_config, &t_config, &f_config); + switch (twaiLastResult) { + case ESP_OK: + MessageOutput.printf("[%s] Twai driver installed\r\n", + _providerName); + break; + case ESP_ERR_INVALID_ARG: + MessageOutput.printf("[%s] Twai driver install - invalid arg\r\n", + _providerName); + return false; + break; + case ESP_ERR_NO_MEM: + MessageOutput.printf("[%s] Twai driver install - no memory\r\n", + _providerName); + return false; + break; + case ESP_ERR_INVALID_STATE: + MessageOutput.printf("[%s] Twai driver install - invalid state\r\n", + _providerName); + return false; + break; + } + + // Start TWAI driver + twaiLastResult = twai_start(); + switch (twaiLastResult) { + case ESP_OK: + MessageOutput.printf("[%s] Twai driver started\r\n", + _providerName); + break; + case ESP_ERR_INVALID_STATE: + MessageOutput.printf("[%s] Twai driver start - invalid state\r\n", + _providerName); + return false; + break; + } + + return true; +} + +void BatteryCanReceiver::deinit() +{ + // Stop TWAI driver + esp_err_t twaiLastResult = twai_stop(); + switch (twaiLastResult) { + case ESP_OK: + MessageOutput.printf("[%s] Twai driver stopped\r\n", + _providerName); + break; + case ESP_ERR_INVALID_STATE: + MessageOutput.printf("[%s] Twai driver stop - invalid state\r\n", + _providerName); + break; + } + + // Uninstall TWAI driver + twaiLastResult = twai_driver_uninstall(); + switch (twaiLastResult) { + case ESP_OK: + MessageOutput.printf("[%s] Twai driver uninstalled\r\n", + _providerName); + break; + case ESP_ERR_INVALID_STATE: + MessageOutput.printf("[%s] Twai driver uninstall - invalid state\r\n", + _providerName); + break; + } +} + +void BatteryCanReceiver::loop() +{ + // Check for messages. twai_receive is blocking when there is no data so we return if there are no frames in the buffer + twai_status_info_t status_info; + esp_err_t twaiLastResult = twai_get_status_info(&status_info); + if (twaiLastResult != ESP_OK) { + switch (twaiLastResult) { + case ESP_ERR_INVALID_ARG: + MessageOutput.printf("[%s] Twai driver get status - invalid arg\r\n", + _providerName); + break; + case ESP_ERR_INVALID_STATE: + MessageOutput.printf("[%s] Twai driver get status - invalid state\r\n", + _providerName); + break; + } + return; + } + if (status_info.msgs_to_rx == 0) { + return; + } + + // Wait for message to be received, function is blocking + twai_message_t rx_message; + if (twai_receive(&rx_message, pdMS_TO_TICKS(100)) != ESP_OK) { + MessageOutput.printf("[%s] Failed to receive message", + _providerName); + return; + } + + if (_verboseLogging) { + MessageOutput.printf("[%s] Received CAN message: 0x%04X -", + _providerName, rx_message.identifier); + + for (int i = 0; i < rx_message.data_length_code; i++) { + MessageOutput.printf(" %02X", rx_message.data[i]); + } + + MessageOutput.printf("\r\n"); + } + + onMessage(rx_message); +} + +uint8_t BatteryCanReceiver::readUnsignedInt8(uint8_t *data) +{ + return data[0]; +} + +uint16_t BatteryCanReceiver::readUnsignedInt16(uint8_t *data) +{ + return (data[1] << 8) | data[0]; +} + +int16_t BatteryCanReceiver::readSignedInt16(uint8_t *data) +{ + return this->readUnsignedInt16(data); +} + +uint32_t BatteryCanReceiver::readUnsignedInt32(uint8_t *data) +{ + return (data[3] << 24) | (data[2] << 16) | (data[1] << 8) | data[0]; +} + +float BatteryCanReceiver::scaleValue(int16_t value, float factor) +{ + return value * factor; +} + +bool BatteryCanReceiver::getBit(uint8_t value, uint8_t bit) +{ + return (value & (1 << bit)) >> bit; +} diff --git a/src/BatteryStats.cpp b/src/BatteryStats.cpp index 61f23823f..9f32931b7 100644 --- a/src/BatteryStats.cpp +++ b/src/BatteryStats.cpp @@ -26,9 +26,12 @@ static void addLiveViewValue(JsonVariant& root, std::string const& name, } static void addLiveViewTextInSection(JsonVariant& root, - std::string const& section, std::string const& name, std::string const& text) + std::string const& section, std::string const& name, + std::string const& text, bool translate = true) { - root["values"][section][name] = text; + auto jsonValue = root["values"][section][name]; + jsonValue["value"] = text; + jsonValue["translate"] = translate; } static void addLiveViewTextValue(JsonVariant& root, std::string const& name, @@ -62,6 +65,9 @@ bool BatteryStats::updateAvailable(uint32_t since) const void BatteryStats::getLiveViewData(JsonVariant& root) const { root["manufacturer"] = _manufacturer; + if (!_serial.isEmpty()) { + root["serial"] = _serial; + } if (!_fwversion.isEmpty()) { root["fwversion"] = _fwversion; } @@ -72,6 +78,7 @@ void BatteryStats::getLiveViewData(JsonVariant& root) const addLiveViewValue(root, "SoC", _soc, "%", _socPrecision); addLiveViewValue(root, "voltage", _voltage, "V", 2); + addLiveViewValue(root, "current", _current, "A", _currentPrecision); } void PylontechBatteryStats::getLiveViewData(JsonVariant& root) const @@ -83,7 +90,6 @@ void PylontechBatteryStats::getLiveViewData(JsonVariant& root) const addLiveViewValue(root, "chargeCurrentLimitation", _chargeCurrentLimitation, "A", 1); addLiveViewValue(root, "dischargeCurrentLimitation", _dischargeCurrentLimitation, "A", 1); addLiveViewValue(root, "stateOfHealth", _stateOfHealth, "%", 0); - addLiveViewValue(root, "current", _current, "A", 1); addLiveViewValue(root, "temperature", _temperature, "°C", 1); addLiveViewTextValue(root, "chargeEnabled", (_chargeEnabled?"yes":"no")); @@ -113,6 +119,77 @@ void PylontechBatteryStats::getLiveViewData(JsonVariant& root) const addLiveViewAlarm(root, "bmsInternal", _alarmBmsInternal); } +void PytesBatteryStats::getLiveViewData(JsonVariant& root) const +{ + BatteryStats::getLiveViewData(root); + + // values go into the "Status" card of the web application + addLiveViewValue(root, "chargeVoltage", _chargeVoltageLimit, "V", 1); + addLiveViewValue(root, "chargeCurrentLimitation", _chargeCurrentLimit, "A", 1); + addLiveViewValue(root, "dischargeVoltageLimitation", _dischargeVoltageLimit, "V", 1); + addLiveViewValue(root, "dischargeCurrentLimitation", _dischargeCurrentLimit, "A", 1); + addLiveViewValue(root, "stateOfHealth", _stateOfHealth, "%", 0); + addLiveViewValue(root, "temperature", _temperature, "°C", 1); + + addLiveViewValue(root, "capacity", _totalCapacity, "Ah", 0); + addLiveViewValue(root, "availableCapacity", _availableCapacity, "Ah", 0); + + if (_chargedEnergy != -1) { + addLiveViewValue(root, "chargedEnergy", _chargedEnergy, "kWh", 1); + } + + if (_dischargedEnergy != -1) { + addLiveViewValue(root, "dischargedEnergy", _dischargedEnergy, "kWh", 1); + } + + addLiveViewInSection(root, "cells", "cellMinVoltage", static_cast(_cellMinMilliVolt)/1000, "V", 3); + addLiveViewInSection(root, "cells", "cellMaxVoltage", static_cast(_cellMaxMilliVolt)/1000, "V", 3); + addLiveViewInSection(root, "cells", "cellDiffVoltage", (_cellMaxMilliVolt - _cellMinMilliVolt), "mV", 0); + addLiveViewInSection(root, "cells", "cellMinTemperature", _cellMinTemperature, "°C", 0); + addLiveViewInSection(root, "cells", "cellMaxTemperature", _cellMaxTemperature, "°C", 0); + + addLiveViewTextInSection(root, "cells", "cellMinVoltageName", _cellMinVoltageName.c_str(), false); + addLiveViewTextInSection(root, "cells", "cellMaxVoltageName", _cellMaxVoltageName.c_str(), false); + addLiveViewTextInSection(root, "cells", "cellMinTemperatureName", _cellMinTemperatureName.c_str(), false); + addLiveViewTextInSection(root, "cells", "cellMaxTemperatureName", _cellMaxTemperatureName.c_str(), false); + + addLiveViewInSection(root, "modules", "online", _moduleCountOnline, "", 0); + addLiveViewInSection(root, "modules", "offline", _moduleCountOffline, "", 0); + addLiveViewInSection(root, "modules", "blockingCharge", _moduleCountBlockingCharge, "", 0); + addLiveViewInSection(root, "modules", "blockingDischarge", _moduleCountBlockingDischarge, "", 0); + + // alarms and warnings go into the "Issues" card of the web application + addLiveViewWarning(root, "highCurrentDischarge", _warningHighDischargeCurrent); + addLiveViewAlarm(root, "overCurrentDischarge", _alarmOverCurrentDischarge); + + addLiveViewWarning(root, "highCurrentCharge", _warningHighChargeCurrent); + addLiveViewAlarm(root, "overCurrentCharge", _alarmOverCurrentCharge); + + addLiveViewWarning(root, "lowVoltage", _warningLowVoltage); + addLiveViewAlarm(root, "underVoltage", _alarmUnderVoltage); + + addLiveViewWarning(root, "highVoltage", _warningHighVoltage); + addLiveViewAlarm(root, "overVoltage", _alarmOverVoltage); + + addLiveViewWarning(root, "lowTemperature", _warningLowTemperature); + addLiveViewAlarm(root, "underTemperature", _alarmUnderTemperature); + + addLiveViewWarning(root, "highTemperature", _warningHighTemperature); + addLiveViewAlarm(root, "overTemperature", _alarmOverTemperature); + + addLiveViewWarning(root, "lowTemperatureCharge", _warningLowTemperatureCharge); + addLiveViewAlarm(root, "underTemperatureCharge", _alarmUnderTemperatureCharge); + + addLiveViewWarning(root, "highTemperatureCharge", _warningHighTemperatureCharge); + addLiveViewAlarm(root, "overTemperatureCharge", _alarmOverTemperatureCharge); + + addLiveViewWarning(root, "bmsInternal", _warningInternalFailure); + addLiveViewAlarm(root, "bmsInternal", _alarmInternalFailure); + + addLiveViewWarning(root, "cellDiffVoltage", _warningCellImbalance); + addLiveViewAlarm(root, "cellDiffVoltage", _alarmCellImbalance); +} + void JkBmsBatteryStats::getJsonData(JsonVariant& root, bool verbose) const { BatteryStats::getLiveViewData(root); @@ -120,11 +197,6 @@ void JkBmsBatteryStats::getJsonData(JsonVariant& root, bool verbose) const using Label = JkBms::DataPointLabel; auto oCurrent = _dataPoints.get(); - if (oCurrent.has_value()) { - addLiveViewValue(root, "current", - static_cast(*oCurrent) / 1000, "A", 2); - } - auto oVoltage = _dataPoints.get(); if (oVoltage.has_value() && oCurrent.has_value()) { auto current = static_cast(*oCurrent) / 1000; @@ -226,8 +298,15 @@ void BatteryStats::mqttPublish() const { MqttSettings.publish("battery/manufacturer", _manufacturer); MqttSettings.publish("battery/dataAge", String(getAgeSeconds())); - MqttSettings.publish("battery/stateOfCharge", String(_soc)); - MqttSettings.publish("battery/voltage", String(_voltage)); + if (isSoCValid()) { + MqttSettings.publish("battery/stateOfCharge", String(_soc)); + } + if (isVoltageValid()) { + MqttSettings.publish("battery/voltage", String(_voltage)); + } + if (isCurrentValid()) { + MqttSettings.publish("battery/current", String(_current)); + } } void PylontechBatteryStats::mqttPublish() const @@ -238,7 +317,6 @@ void PylontechBatteryStats::mqttPublish() const 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)); @@ -259,6 +337,67 @@ void PylontechBatteryStats::mqttPublish() const MqttSettings.publish("battery/charging/chargeImmediately", String(_chargeImmediately)); } +void PytesBatteryStats::mqttPublish() const +{ + BatteryStats::mqttPublish(); + + MqttSettings.publish("battery/settings/chargeVoltage", String(_chargeVoltageLimit)); + MqttSettings.publish("battery/settings/chargeCurrentLimitation", String(_chargeCurrentLimit)); + MqttSettings.publish("battery/settings/dischargeCurrentLimitation", String(_dischargeCurrentLimit)); + MqttSettings.publish("battery/settings/dischargeVoltageLimitation", String(_dischargeVoltageLimit)); + + MqttSettings.publish("battery/stateOfHealth", String(_stateOfHealth)); + MqttSettings.publish("battery/temperature", String(_temperature)); + + if (_chargedEnergy != -1) { + MqttSettings.publish("battery/chargedEnergy", String(_chargedEnergy)); + } + + if (_dischargedEnergy != -1) { + MqttSettings.publish("battery/dischargedEnergy", String(_dischargedEnergy)); + } + + MqttSettings.publish("battery/capacity", String(_totalCapacity)); + MqttSettings.publish("battery/availableCapacity", String(_availableCapacity)); + + MqttSettings.publish("battery/CellMinMilliVolt", String(_cellMinMilliVolt)); + MqttSettings.publish("battery/CellMaxMilliVolt", String(_cellMaxMilliVolt)); + MqttSettings.publish("battery/CellDiffMilliVolt", String(_cellMaxMilliVolt - _cellMinMilliVolt)); + MqttSettings.publish("battery/CellMinTemperature", String(_cellMinTemperature)); + MqttSettings.publish("battery/CellMaxTemperature", String(_cellMaxTemperature)); + MqttSettings.publish("battery/CellMinVoltageName", String(_cellMinVoltageName)); + MqttSettings.publish("battery/CellMaxVoltageName", String(_cellMaxVoltageName)); + MqttSettings.publish("battery/CellMinTemperatureName", String(_cellMinTemperatureName)); + MqttSettings.publish("battery/CellMaxTemperatureName", String(_cellMaxTemperatureName)); + + MqttSettings.publish("battery/modulesOnline", String(_moduleCountOnline)); + MqttSettings.publish("battery/modulesOffline", String(_moduleCountOffline)); + MqttSettings.publish("battery/modulesBlockingCharge", String(_moduleCountBlockingCharge)); + MqttSettings.publish("battery/modulesBlockingDischarge", String(_moduleCountBlockingDischarge)); + + MqttSettings.publish("battery/alarm/overCurrentDischarge", String(_alarmOverCurrentDischarge)); + MqttSettings.publish("battery/alarm/overCurrentCharge", String(_alarmOverCurrentCharge)); + MqttSettings.publish("battery/alarm/underVoltage", String(_alarmUnderVoltage)); + MqttSettings.publish("battery/alarm/overVoltage", String(_alarmOverVoltage)); + MqttSettings.publish("battery/alarm/underTemperature", String(_alarmUnderTemperature)); + MqttSettings.publish("battery/alarm/overTemperature", String(_alarmOverTemperature)); + MqttSettings.publish("battery/alarm/underTemperatureCharge", String(_alarmUnderTemperatureCharge)); + MqttSettings.publish("battery/alarm/overTemperatureCharge", String(_alarmOverTemperatureCharge)); + MqttSettings.publish("battery/alarm/bmsInternal", String(_alarmInternalFailure)); + MqttSettings.publish("battery/alarm/cellImbalance", String(_alarmCellImbalance)); + + MqttSettings.publish("battery/warning/highCurrentDischarge", String(_warningHighDischargeCurrent)); + MqttSettings.publish("battery/warning/highCurrentCharge", String(_warningHighChargeCurrent)); + MqttSettings.publish("battery/warning/lowVoltage", String(_warningLowVoltage)); + MqttSettings.publish("battery/warning/highVoltage", String(_warningHighVoltage)); + MqttSettings.publish("battery/warning/lowTemperature", String(_warningLowTemperature)); + MqttSettings.publish("battery/warning/highTemperature", String(_warningHighTemperature)); + MqttSettings.publish("battery/warning/lowTemperatureCharge", String(_warningLowTemperatureCharge)); + MqttSettings.publish("battery/warning/highTemperatureCharge", String(_warningHighTemperatureCharge)); + MqttSettings.publish("battery/warning/bmsInternal", String(_warningInternalFailure)); + MqttSettings.publish("battery/warning/cellImbalance", String(_warningCellImbalance)); +} + void JkBmsBatteryStats::mqttPublish() const { BatteryStats::mqttPublish(); @@ -365,6 +504,13 @@ void JkBmsBatteryStats::updateFrom(JkBms::DataPointContainer const& dp) oVoltageDataPoint->getTimestamp()); } + auto oCurrent = dp.get(); + if (oCurrent.has_value()) { + auto oCurrentDataPoint = dp.getDataPointFor(); + BatteryStats::setCurrent(static_cast(*oCurrent) / 1000, 2/*precision*/, + oCurrentDataPoint->getTimestamp()); + } + _dataPoints.updateFrom(dp); auto oCellVoltages = _dataPoints.get(); @@ -405,9 +551,9 @@ void JkBmsBatteryStats::updateFrom(JkBms::DataPointContainer const& dp) 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()); + BatteryStats::setCurrent(static_cast(shuntData.batteryCurrent_I_mA) / 1000, 2/*precision*/, millis()); _fwversion = shuntData.getFwVersionFormatted(); - _current = static_cast(shuntData.batteryCurrent_I_mA) / 1000; _chargeCycles = shuntData.H4; _timeToGo = shuntData.TTG / 60; _chargedEnergy = static_cast(shuntData.H18) / 100; @@ -434,7 +580,6 @@ void VictronSmartShuntStats::getLiveViewData(JsonVariant& root) const { BatteryStats::getLiveViewData(root); // values go into the "Status" card of the web application - addLiveViewValue(root, "current", _current, "A", 1); addLiveViewValue(root, "chargeCycles", _chargeCycles, "", 0); addLiveViewValue(root, "chargedEnergy", _chargedEnergy, "kWh", 2); addLiveViewValue(root, "dischargedEnergy", _dischargedEnergy, "kWh", 2); @@ -457,7 +602,6 @@ void VictronSmartShuntStats::getLiveViewData(JsonVariant& root) const { void VictronSmartShuntStats::mqttPublish() const { BatteryStats::mqttPublish(); - MqttSettings.publish("battery/current", String(_current)); MqttSettings.publish("battery/chargeCycles", String(_chargeCycles)); MqttSettings.publish("battery/chargedEnergy", String(_chargedEnergy)); MqttSettings.publish("battery/dischargedEnergy", String(_dischargedEnergy)); diff --git a/src/Configuration.cpp b/src/Configuration.cpp index d23ac9ba4..9b3c7d4e4 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -4,9 +4,9 @@ */ #include "Configuration.h" #include "MessageOutput.h" +#include "NetworkSettings.h" #include "Utils.h" #include "defaults.h" -#include #include #include @@ -17,6 +17,63 @@ void ConfigurationClass::init() memset(&config, 0x0, sizeof(config)); } +void ConfigurationClass::serializeHttpRequestConfig(HttpRequestConfig const& source, JsonObject& target) +{ + JsonObject target_http_config = target["http_request"].to(); + target_http_config["url"] = source.Url; + target_http_config["auth_type"] = source.AuthType; + target_http_config["username"] = source.Username; + target_http_config["password"] = source.Password; + target_http_config["header_key"] = source.HeaderKey; + target_http_config["header_value"] = source.HeaderValue; + target_http_config["timeout"] = source.Timeout; +} + +void ConfigurationClass::serializePowerMeterMqttConfig(PowerMeterMqttConfig const& source, JsonObject& target) +{ + JsonArray values = target["values"].to(); + for (size_t i = 0; i < POWERMETER_MQTT_MAX_VALUES; ++i) { + JsonObject t = values.add(); + PowerMeterMqttValue const& s = source.Values[i]; + + t["topic"] = s.Topic; + t["json_path"] = s.JsonPath; + t["unit"] = s.PowerUnit; + t["sign_inverted"] = s.SignInverted; + } +} + +void ConfigurationClass::serializePowerMeterSerialSdmConfig(PowerMeterSerialSdmConfig const& source, JsonObject& target) +{ + target["address"] = source.Address; + target["polling_interval"] = source.PollingInterval; +} + +void ConfigurationClass::serializePowerMeterHttpJsonConfig(PowerMeterHttpJsonConfig const& source, JsonObject& target) +{ + target["polling_interval"] = source.PollingInterval; + target["individual_requests"] = source.IndividualRequests; + + JsonArray values = target["values"].to(); + for (size_t i = 0; i < POWERMETER_HTTP_JSON_MAX_VALUES; ++i) { + JsonObject t = values.add(); + PowerMeterHttpJsonValue const& s = source.Values[i]; + + serializeHttpRequestConfig(s.HttpRequest, t); + + t["enabled"] = s.Enabled; + t["json_path"] = s.JsonPath; + t["unit"] = s.PowerUnit; + t["sign_inverted"] = s.SignInverted; + } +} + +void ConfigurationClass::serializePowerMeterHttpSmlConfig(PowerMeterHttpSmlConfig const& source, JsonObject& target) +{ + target["polling_interval"] = source.PollingInterval; + serializeHttpRequestConfig(source.HttpRequest, target); +} + bool ConfigurationClass::write() { File f = LittleFS.open(CONFIG_FILENAME, "w"); @@ -59,6 +116,7 @@ bool ConfigurationClass::write() mqtt["verbose_logging"] = config.Mqtt.VerboseLogging; mqtt["hostname"] = config.Mqtt.Hostname; mqtt["port"] = config.Mqtt.Port; + mqtt["clientid"] = config.Mqtt.ClientId; mqtt["username"] = config.Mqtt.Username; mqtt["password"] = config.Mqtt.Password; mqtt["topic"] = config.Mqtt.Topic; @@ -130,6 +188,7 @@ bool ConfigurationClass::write() inv["reachable_threshold"] = config.Inverter[i].ReachableThreshold; inv["zero_runtime"] = config.Inverter[i].ZeroRuntimeDataIfUnrechable; inv["zero_day"] = config.Inverter[i].ZeroYieldDayOnMidnight; + inv["clear_eventlog"] = config.Inverter[i].ClearEventlogOnMidnight; inv["yieldday_correction"] = config.Inverter[i].YieldDayCorrection; JsonArray channel = inv["channel"].to(); @@ -149,31 +208,19 @@ bool ConfigurationClass::write() JsonObject powermeter = doc["powermeter"].to(); powermeter["enabled"] = config.PowerMeter.Enabled; powermeter["verbose_logging"] = config.PowerMeter.VerboseLogging; - powermeter["interval"] = config.PowerMeter.Interval; powermeter["source"] = config.PowerMeter.Source; - powermeter["mqtt_topic_powermeter_1"] = config.PowerMeter.MqttTopicPowerMeter1; - powermeter["mqtt_topic_powermeter_2"] = config.PowerMeter.MqttTopicPowerMeter2; - powermeter["mqtt_topic_powermeter_3"] = config.PowerMeter.MqttTopicPowerMeter3; - powermeter["sdmbaudrate"] = config.PowerMeter.SdmBaudrate; - powermeter["sdmaddress"] = config.PowerMeter.SdmAddress; - powermeter["http_individual_requests"] = config.PowerMeter.HttpIndividualRequests; - - JsonArray powermeter_http_phases = powermeter["http_phases"].to(); - for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) { - 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; - powermeter_phase["auth_type"] = config.PowerMeter.Http_Phase[i].AuthType; - powermeter_phase["username"] = config.PowerMeter.Http_Phase[i].Username; - powermeter_phase["password"] = config.PowerMeter.Http_Phase[i].Password; - powermeter_phase["header_key"] = config.PowerMeter.Http_Phase[i].HeaderKey; - 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 powermeter_mqtt = powermeter["mqtt"].to(); + serializePowerMeterMqttConfig(config.PowerMeter.Mqtt, powermeter_mqtt); + + JsonObject powermeter_serial_sdm = powermeter["serial_sdm"].to(); + serializePowerMeterSerialSdmConfig(config.PowerMeter.SerialSdm, powermeter_serial_sdm); + + JsonObject powermeter_http_json = powermeter["http_json"].to(); + serializePowerMeterHttpJsonConfig(config.PowerMeter.HttpJson, powermeter_http_json); + + JsonObject powermeter_http_sml = powermeter["http_sml"].to(); + serializePowerMeterHttpSmlConfig(config.PowerMeter.HttpSml, powermeter_http_sml); JsonObject powerlimiter = doc["powerlimiter"].to(); powerlimiter["enabled"] = config.PowerLimiter.Enabled; @@ -184,6 +231,7 @@ bool ConfigurationClass::write() powerlimiter["interval"] = config.PowerLimiter.Interval; powerlimiter["is_inverter_behind_powermeter"] = config.PowerLimiter.IsInverterBehindPowerMeter; powerlimiter["is_inverter_solar_powered"] = config.PowerLimiter.IsInverterSolarPowered; + powerlimiter["use_overscaling_to_compensate_shading"] = config.PowerLimiter.UseOverscalingToCompensateShading; powerlimiter["inverter_id"] = config.PowerLimiter.InverterId; powerlimiter["inverter_channel_id"] = config.PowerLimiter.InverterChannelId; powerlimiter["target_power_consumption"] = config.PowerLimiter.TargetPowerConsumption; @@ -209,7 +257,10 @@ bool ConfigurationClass::write() battery["jkbms_interface"] = config.Battery.JkBmsInterface; battery["jkbms_polling_interval"] = config.Battery.JkBmsPollingInterval; battery["mqtt_topic"] = config.Battery.MqttSocTopic; + battery["mqtt_json_path"] = config.Battery.MqttSocJsonPath; battery["mqtt_voltage_topic"] = config.Battery.MqttVoltageTopic; + battery["mqtt_voltage_json_path"] = config.Battery.MqttVoltageJsonPath; + battery["mqtt_voltage_unit"] = config.Battery.MqttVoltageUnit; JsonObject huawei = doc["huawei"].to(); huawei["enabled"] = config.Huawei.Enabled; @@ -239,6 +290,69 @@ bool ConfigurationClass::write() return true; } +void ConfigurationClass::deserializeHttpRequestConfig(JsonObject const& source, HttpRequestConfig& target) +{ + JsonObject source_http_config = source["http_request"]; + + // http request parameters of HTTP/JSON power meter were previously stored + // alongside other settings. TODO(schlimmchen): remove in early 2025. + if (source_http_config.isNull()) { source_http_config = source; } + + strlcpy(target.Url, source_http_config["url"] | "", sizeof(target.Url)); + target.AuthType = source_http_config["auth_type"] | HttpRequestConfig::Auth::None; + strlcpy(target.Username, source_http_config["username"] | "", sizeof(target.Username)); + strlcpy(target.Password, source_http_config["password"] | "", sizeof(target.Password)); + strlcpy(target.HeaderKey, source_http_config["header_key"] | "", sizeof(target.HeaderKey)); + strlcpy(target.HeaderValue, source_http_config["header_value"] | "", sizeof(target.HeaderValue)); + target.Timeout = source_http_config["timeout"] | HTTP_REQUEST_TIMEOUT_MS; +} + +void ConfigurationClass::deserializePowerMeterMqttConfig(JsonObject const& source, PowerMeterMqttConfig& target) +{ + for (size_t i = 0; i < POWERMETER_MQTT_MAX_VALUES; ++i) { + PowerMeterMqttValue& t = target.Values[i]; + JsonObject s = source["values"][i]; + + strlcpy(t.Topic, s["topic"] | "", sizeof(t.Topic)); + strlcpy(t.JsonPath, s["json_path"] | "", sizeof(t.JsonPath)); + t.PowerUnit = s["unit"] | PowerMeterMqttValue::Unit::Watts; + t.SignInverted = s["sign_inverted"] | false; + } +} + +void ConfigurationClass::deserializePowerMeterSerialSdmConfig(JsonObject const& source, PowerMeterSerialSdmConfig& target) +{ + target.PollingInterval = source["polling_interval"] | POWERMETER_POLLING_INTERVAL; + target.Address = source["address"] | POWERMETER_SDMADDRESS; +} + +void ConfigurationClass::deserializePowerMeterHttpJsonConfig(JsonObject const& source, PowerMeterHttpJsonConfig& target) +{ + target.PollingInterval = source["polling_interval"] | POWERMETER_POLLING_INTERVAL; + target.IndividualRequests = source["individual_requests"] | false; + + JsonArray values = source["values"].as(); + for (size_t i = 0; i < POWERMETER_HTTP_JSON_MAX_VALUES; ++i) { + PowerMeterHttpJsonValue& t = target.Values[i]; + JsonObject s = values[i]; + + deserializeHttpRequestConfig(s, t.HttpRequest); + + t.Enabled = s["enabled"] | false; + strlcpy(t.JsonPath, s["json_path"] | "", sizeof(t.JsonPath)); + t.PowerUnit = s["unit"] | PowerMeterHttpJsonValue::Unit::Watts; + t.SignInverted = s["sign_inverted"] | false; + } + + target.Values[0].Enabled = true; +} + +void ConfigurationClass::deserializePowerMeterHttpSmlConfig(JsonObject const& source, PowerMeterHttpSmlConfig& target) +{ + target.PollingInterval = source["polling_interval"] | POWERMETER_POLLING_INTERVAL; + deserializeHttpRequestConfig(source, target.HttpRequest); +} + bool ConfigurationClass::read() { File f = LittleFS.open(CONFIG_FILENAME, "r", false); @@ -318,6 +432,7 @@ bool ConfigurationClass::read() config.Mqtt.VerboseLogging = mqtt["verbose_logging"] | VERBOSE_LOGGING; strlcpy(config.Mqtt.Hostname, mqtt["hostname"] | MQTT_HOST, sizeof(config.Mqtt.Hostname)); config.Mqtt.Port = mqtt["port"] | MQTT_PORT; + strlcpy(config.Mqtt.ClientId, mqtt["clientid"] | NetworkSettings.getApName().c_str(), sizeof(config.Mqtt.ClientId)); strlcpy(config.Mqtt.Username, mqtt["username"] | MQTT_USER, sizeof(config.Mqtt.Username)); strlcpy(config.Mqtt.Password, mqtt["password"] | MQTT_PASSWORD, sizeof(config.Mqtt.Password)); strlcpy(config.Mqtt.Topic, mqtt["topic"] | MQTT_TOPIC, sizeof(config.Mqtt.Topic)); @@ -390,6 +505,7 @@ bool ConfigurationClass::read() config.Inverter[i].ReachableThreshold = inv["reachable_threshold"] | REACHABLE_THRESHOLD; config.Inverter[i].ZeroRuntimeDataIfUnrechable = inv["zero_runtime"] | false; config.Inverter[i].ZeroYieldDayOnMidnight = inv["zero_day"] | false; + config.Inverter[i].ClearEventlogOnMidnight = inv["clear_eventlog"] | false; config.Inverter[i].YieldDayCorrection = inv["yieldday_correction"] | false; JsonArray channel = inv["channel"]; @@ -408,30 +524,51 @@ bool ConfigurationClass::read() JsonObject powermeter = doc["powermeter"]; config.PowerMeter.Enabled = powermeter["enabled"] | POWERMETER_ENABLED; config.PowerMeter.VerboseLogging = powermeter["verbose_logging"] | VERBOSE_LOGGING; - config.PowerMeter.Interval = powermeter["interval"] | POWERMETER_INTERVAL; config.PowerMeter.Source = powermeter["source"] | POWERMETER_SOURCE; - strlcpy(config.PowerMeter.MqttTopicPowerMeter1, powermeter["mqtt_topic_powermeter_1"] | "", sizeof(config.PowerMeter.MqttTopicPowerMeter1)); - strlcpy(config.PowerMeter.MqttTopicPowerMeter2, powermeter["mqtt_topic_powermeter_2"] | "", sizeof(config.PowerMeter.MqttTopicPowerMeter2)); - strlcpy(config.PowerMeter.MqttTopicPowerMeter3, powermeter["mqtt_topic_powermeter_3"] | "", sizeof(config.PowerMeter.MqttTopicPowerMeter3)); - config.PowerMeter.SdmBaudrate = powermeter["sdmbaudrate"] | POWERMETER_SDMBAUDRATE; - config.PowerMeter.SdmAddress = powermeter["sdmaddress"] | POWERMETER_SDMADDRESS; - config.PowerMeter.HttpIndividualRequests = powermeter["http_individual_requests"] | false; - - JsonArray powermeter_http_phases = powermeter["http_phases"]; - for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) { - JsonObject powermeter_phase = powermeter_http_phases[i].as(); - - 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"] | 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; + + deserializePowerMeterMqttConfig(powermeter["mqtt"], config.PowerMeter.Mqtt); + + // process settings from legacy config if they are present + // TODO(schlimmchen): remove in early 2025. + if (!powermeter["mqtt_topic_powermeter_1"].isNull()) { + auto& values = config.PowerMeter.Mqtt.Values; + strlcpy(values[0].Topic, powermeter["mqtt_topic_powermeter_1"], sizeof(values[0].Topic)); + strlcpy(values[1].Topic, powermeter["mqtt_topic_powermeter_2"], sizeof(values[1].Topic)); + strlcpy(values[2].Topic, powermeter["mqtt_topic_powermeter_3"], sizeof(values[2].Topic)); + } + + deserializePowerMeterSerialSdmConfig(powermeter["serial_sdm"], config.PowerMeter.SerialSdm); + + // process settings from legacy config if they are present + // TODO(schlimmchen): remove in early 2025. + if (!powermeter["sdmaddress"].isNull()) { + config.PowerMeter.SerialSdm.Address = powermeter["sdmaddress"]; + } + + JsonObject powermeter_http_json = powermeter["http_json"]; + deserializePowerMeterHttpJsonConfig(powermeter_http_json, config.PowerMeter.HttpJson); + + JsonObject powermeter_sml = powermeter["http_sml"]; + deserializePowerMeterHttpSmlConfig(powermeter_sml, config.PowerMeter.HttpSml); + + // process settings from legacy config if they are present + // TODO(schlimmchen): remove in early 2025. + if (!powermeter["http_phases"].isNull()) { + auto& target = config.PowerMeter.HttpJson; + + for (size_t i = 0; i < POWERMETER_HTTP_JSON_MAX_VALUES; ++i) { + PowerMeterHttpJsonValue& t = target.Values[i]; + JsonObject s = powermeter["http_phases"][i]; + + deserializeHttpRequestConfig(s, t.HttpRequest); + + t.Enabled = s["enabled"] | false; + strlcpy(t.JsonPath, s["json_path"] | "", sizeof(t.JsonPath)); + t.PowerUnit = s["unit"] | PowerMeterHttpJsonValue::Unit::Watts; + t.SignInverted = s["sign_inverted"] | false; + } + + target.IndividualRequests = powermeter["http_individual_requests"] | false; } JsonObject powerlimiter = doc["powerlimiter"]; @@ -444,6 +581,7 @@ bool ConfigurationClass::read() config.PowerLimiter.Interval = powerlimiter["interval"] | POWERLIMITER_INTERVAL; config.PowerLimiter.IsInverterBehindPowerMeter = powerlimiter["is_inverter_behind_powermeter"] | POWERLIMITER_IS_INVERTER_BEHIND_POWER_METER; config.PowerLimiter.IsInverterSolarPowered = powerlimiter["is_inverter_solar_powered"] | POWERLIMITER_IS_INVERTER_SOLAR_POWERED; + config.PowerLimiter.UseOverscalingToCompensateShading = powerlimiter["use_overscaling_to_compensate_shading"] | POWERLIMITER_USE_OVERSCALING_TO_COMPENSATE_SHADING; config.PowerLimiter.InverterId = powerlimiter["inverter_id"] | POWERLIMITER_INVERTER_ID; config.PowerLimiter.InverterChannelId = powerlimiter["inverter_channel_id"] | POWERLIMITER_INVERTER_CHANNEL_ID; config.PowerLimiter.TargetPowerConsumption = powerlimiter["target_power_consumption"] | POWERLIMITER_TARGET_POWER_CONSUMPTION; @@ -469,7 +607,10 @@ bool ConfigurationClass::read() config.Battery.JkBmsInterface = battery["jkbms_interface"] | BATTERY_JKBMS_INTERFACE; config.Battery.JkBmsPollingInterval = battery["jkbms_polling_interval"] | BATTERY_JKBMS_POLLING_INTERVAL; strlcpy(config.Battery.MqttSocTopic, battery["mqtt_topic"] | "", sizeof(config.Battery.MqttSocTopic)); + strlcpy(config.Battery.MqttSocJsonPath, battery["mqtt_json_path"] | "", sizeof(config.Battery.MqttSocJsonPath)); strlcpy(config.Battery.MqttVoltageTopic, battery["mqtt_voltage_topic"] | "", sizeof(config.Battery.MqttVoltageTopic)); + strlcpy(config.Battery.MqttVoltageJsonPath, battery["mqtt_voltage_json_path"] | "", sizeof(config.Battery.MqttVoltageJsonPath)); + config.Battery.MqttVoltageUnit = battery["mqtt_voltage_unit"] | BatteryVoltageUnit::Volts; JsonObject huawei = doc["huawei"]; config.Huawei.Enabled = huawei["enabled"] | HUAWEI_ENABLED; diff --git a/src/Display_Graphic.cpp b/src/Display_Graphic.cpp index 1b08aff82..540554f2b 100644 --- a/src/Display_Graphic.cpp +++ b/src/Display_Graphic.cpp @@ -294,7 +294,7 @@ void DisplayGraphicClass::loop() _display->drawBox(0, y, _display->getDisplayWidth(), lineHeight); _display->setDrawColor(1); - auto acPower = PowerMeter.getPowerTotal(false); + auto acPower = PowerMeter.getPowerTotal(); if (acPower > 999) { snprintf(_fmtText, sizeof(_fmtText), i18n_meter_power_kw[_display_language], (acPower / 1000)); } else { diff --git a/src/HttpGetter.cpp b/src/HttpGetter.cpp new file mode 100644 index 000000000..8b2e158e9 --- /dev/null +++ b/src/HttpGetter.cpp @@ -0,0 +1,236 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "HttpGetter.h" +#include +#include "mbedtls/sha256.h" +#include +#include + +template +void HttpGetter::logError(char const* format, Args... args) { + snprintf(_errBuffer, sizeof(_errBuffer), format, args...); +} + +bool HttpGetter::init() +{ + String url(_config.Url); + + int index = url.indexOf(':'); + if (index < 0) { + logError("failed to parse URL protocol: no colon in URL"); + return false; + } + + String protocol = url.substring(0, index); + if (protocol != "http" && protocol != "https") { + logError("failed to parse URL protocol: '%s' is neither 'http' nor 'https'", protocol.c_str()); + return false; + } + + _useHttps = (protocol == "https"); + + // initialize port to default values for http or https. + // port will be overwritten below in case port is explicitly defined + _port = _useHttps ? 443 : 80; + + String slashes = url.substring(index + 1, index + 3); + if (slashes != "//") { + logError("expected two forward slashes after first colon in URL"); + return false; + } + + _uri = url.substring(index + 3); // without protocol identifier + + index = _uri.indexOf('/'); + if (index == -1) { + index = _uri.length(); + _uri += '/'; + } + _host = _uri.substring(0, index); + _uri.remove(0, index); // remove host part + + index = _host.indexOf('@'); + if (index >= 0) { + // basic authentication is only supported through setting username + // and password using the respective inputs, not embedded into the URL. + // to avoid regressions, we remove username and password from the host + // part of the URL. + _host.remove(0, index + 1); // remove auth part including @ + } + + // get port + index = _host.indexOf(':'); + if (index >= 0) { + _host = _host.substring(0, index); // up until colon + _port = _host.substring(index + 1).toInt(); // after colon + } + + if (_useHttps) { + auto secureWifiClient = std::make_shared(); + secureWifiClient->setInsecure(); + _spWiFiClient = std::move(secureWifiClient); + } else { + _spWiFiClient = std::make_shared(); + } + + return true; +} + +HttpRequestResult HttpGetter::performGetRequest() +{ + // hostByName in WiFiGeneric fails to resolve local names. issue described at + // https://github.com/espressif/arduino-esp32/issues/3822 and in analyzed in + // depth at https://github.com/espressif/esp-idf/issues/2507#issuecomment-761836300 + // in conclusion: we cannot rely on _upHttpClient->begin(*wifiClient, url) to resolve + // IP adresses. have to do it manually. + IPAddress ipaddr((uint32_t)0); + + if (!ipaddr.fromString(_host)) { + // host is not an IP address, so try to resolve the name to an address. + // first try locally via mDNS, then via DNS. WiFiGeneric::hostByName() + // will spam the console if done the other way around. + ipaddr = INADDR_NONE; + + if (Configuration.get().Mdns.Enabled) { + ipaddr = MDNS.queryHost(_host); // INADDR_NONE if failed + } + + if (ipaddr == INADDR_NONE && !WiFiGenericClass::hostByName(_host.c_str(), ipaddr)) { + logError("failed to resolve host '%s' via DNS", _host.c_str()); + return { false }; + } + } + + auto upTmpHttpClient = std::make_unique(); + + // use HTTP1.0 to avoid problems with chunked transfer encoding when the + // stream is later used to read the server's response. + upTmpHttpClient->useHTTP10(true); + + if (!upTmpHttpClient->begin(*_spWiFiClient, ipaddr.toString(), _port, _uri, _useHttps)) { + logError("HTTP client begin() failed for %s://%s", + (_useHttps ? "https" : "http"), _host.c_str()); + return { false }; + } + + upTmpHttpClient->setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); + upTmpHttpClient->setUserAgent("OpenDTU-OnBattery"); + upTmpHttpClient->setConnectTimeout(_config.Timeout); + upTmpHttpClient->setTimeout(_config.Timeout); + for (auto const& h : _additionalHeaders) { + upTmpHttpClient->addHeader(h.first.c_str(), h.second.c_str()); + } + + if (strlen(_config.HeaderKey) > 0) { + upTmpHttpClient->addHeader(_config.HeaderKey, _config.HeaderValue); + } + + using Auth_t = HttpRequestConfig::Auth; + switch (_config.AuthType) { + case Auth_t::None: + break; + case Auth_t::Basic: { + String credentials = String(_config.Username) + ":" + _config.Password; + String authorization = "Basic " + base64::encode(credentials); + upTmpHttpClient->addHeader("Authorization", authorization); + break; + } + case Auth_t::Digest: { + const char *headers[1] = {"WWW-Authenticate"}; + upTmpHttpClient->collectHeaders(headers, 1); + break; + } + } + + int httpCode = upTmpHttpClient->GET(); + + if (httpCode == HTTP_CODE_UNAUTHORIZED && _config.AuthType == Auth_t::Digest) { + if (!upTmpHttpClient->hasHeader("WWW-Authenticate")) { + logError("Cannot perform digest authentication as server did " + "not send a WWW-Authenticate header"); + return { false }; + } + String authReq = upTmpHttpClient->header("WWW-Authenticate"); + String authorization = getAuthDigest(authReq, 1); + upTmpHttpClient->addHeader("Authorization", authorization); + httpCode = upTmpHttpClient->GET(); + } + + if (httpCode <= 0) { + logError("HTTP Error: %s", upTmpHttpClient->errorToString(httpCode).c_str()); + return { false }; + } + + if (httpCode != HTTP_CODE_OK) { + logError("Bad HTTP code: %d", httpCode); + return { false }; + } + + return { true, std::move(upTmpHttpClient), _spWiFiClient }; +} + +static String sha256(const String& data) { + uint8_t hash[32]; + + mbedtls_sha256_context ctx; + mbedtls_sha256_init(&ctx); + mbedtls_sha256_starts(&ctx, 0); // select SHA256 + mbedtls_sha256_update(&ctx, reinterpret_cast(data.c_str()), data.length()); + mbedtls_sha256_finish(&ctx, hash); + mbedtls_sha256_free(&ctx); + + char res[sizeof(hash) * 2 + 1]; + for (int i = 0; i < sizeof(hash); i++) { + snprintf(res + (i*2), sizeof(res) - (i*2), "%02x", hash[i]); + } + + return res; +} + +static String extractParam(String const& authReq, String const& param, char delimiter) { + auto begin = authReq.indexOf(param); + if (begin == -1) { return ""; } + auto end = authReq.indexOf(delimiter, begin + param.length()); + return authReq.substring(begin + param.length(), end); +} + +static String getcNonce(int len) { + static const char alphanum[] = "0123456789" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz"; + String s = ""; + + for (int i = 0; i < len; ++i) { s += alphanum[rand() % (sizeof(alphanum) - 1)]; } + + return s; +} + +String HttpGetter::getAuthDigest(String const& authReq, unsigned int counter) { + // extracting required parameters for RFC 2617 Digest + String realm = extractParam(authReq, "realm=\"", '"'); + String nonce = extractParam(authReq, "nonce=\"", '"'); + String cNonce = getcNonce(8); + + char nc[9]; + snprintf(nc, sizeof(nc), "%08x", counter); + + // sha256 of the user:realm:password + String ha1 = sha256(String(_config.Username) + ":" + realm + ":" + _config.Password); + + // sha256 of method:uri + String ha2 = sha256("GET:" + _uri); + + // sha256 of h1:nonce:nc:cNonce:auth:h2 + String response = sha256(ha1 + ":" + nonce + ":" + String(nc) + + ":" + cNonce + ":" + "auth" + ":" + ha2); + + // Final authorization String + return String("Digest username=\"") + _config.Username + + "\", realm=\"" + realm + "\", nonce=\"" + nonce + "\", uri=\"" + + _uri + "\", cnonce=\"" + cNonce + "\", nc=" + nc + + ", qop=auth, response=\"" + response + "\", algorithm=SHA-256"; +} + +void HttpGetter::addHeader(char const* key, char const* value) +{ + _additionalHeaders.push_back({ key, value }); +} diff --git a/src/HttpPowerMeter.cpp b/src/HttpPowerMeter.cpp deleted file mode 100644 index e3033cbb9..000000000 --- a/src/HttpPowerMeter.cpp +++ /dev/null @@ -1,395 +0,0 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -#include "Configuration.h" -#include "HttpPowerMeter.h" -#include "MessageOutput.h" -#include -#include -#include "mbedtls/sha256.h" -#include -#include -#include - -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() -{ - auto const& config = Configuration.get(); - - for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) { - auto const& phaseConfig = config.PowerMeter.Http_Phase[i]; - - if (!phaseConfig.Enabled) { - power[i] = 0.0; - continue; - } - - if (i == 0 || config.PowerMeter.HttpIndividualRequests) { - 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; - } - continue; - } - - 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; - } - } - return true; -} - -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 - //and in depth analyzed in https://github.com/espressif/esp-idf/issues/2507#issuecomment-761836300 - //in conclusion: we cannot rely on httpClient.begin(*wifiClient, url) to resolve IP adresses. - //have to do it manually here. Feels Hacky... - String protocol; - String host; - String uri; - String base64Authorization; - uint16_t port; - extractUrlComponents(config.Url, protocol, host, uri, port, base64Authorization); - - IPAddress ipaddr((uint32_t)0); - //first check if "host" is already an IP adress - if (!ipaddr.fromString(host)) - { - //"host"" is not an IP address so try to resolve the IP adress - //first try locally via mDNS, then via DNS. WiFiGeneric::hostByName() will spam the console if done the otherway around. - const bool mdnsEnabled = Configuration.get().Mdns.Enabled; - if (!mdnsEnabled) { - snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Error resolving host %s via DNS, try to enable mDNS in Network Settings"), host.c_str()); - //ensure we try resolving via DNS even if mDNS is disabled - if(!WiFiGenericClass::hostByName(host.c_str(), ipaddr)){ - snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Error resolving host %s via DNS"), host.c_str()); - } - } - else - { - ipaddr = MDNS.queryHost(host); - if (ipaddr == INADDR_NONE){ - snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Error resolving host %s via mDNS"), host.c_str()); - //when we cannot find local server via mDNS, try resolving via DNS - if(!WiFiGenericClass::hostByName(host.c_str(), ipaddr)){ - snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Error resolving host %s via DNS"), host.c_str()); - } - } - } - } - - // secureWifiClient MUST be created before HTTPClient - // see discussion: https://github.com/helgeerbe/OpenDTU-OnBattery/issues/381 - std::unique_ptr wifiClient; - - bool https = protocol == "https"; - if (https) { - auto secureWifiClient = std::make_unique(); - secureWifiClient->setInsecure(); - wifiClient = std::move(secureWifiClient); - } else { - wifiClient = std::make_unique(); - } - - 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, 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(config.Timeout, config.HeaderKey, config.HeaderValue); - if (config.AuthType == Auth_t::Digest) { - const char *headers[1] = {"WWW-Authenticate"}; - httpClient.collectHeaders(headers, 1); - } else if (config.AuthType == Auth_t::Basic) { - String authString = config.Username; - authString += ":"; - authString += config.Password; - String auth = "Basic "; - auth.concat(base64::encode(authString)); - httpClient.addHeader("Authorization", auth); - } - int httpCode = httpClient.GET(); - - 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(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(config.Timeout, config.HeaderKey, config.HeaderValue); - httpClient.addHeader("Authorization", authorization); - httpCode = httpClient.GET(); - } - } - - if (httpCode <= 0) { - snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("HTTP Error %s"), httpClient.errorToString(httpCode).c_str()); - return false; - } - - if (httpCode != HTTP_CODE_OK) { - snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("Bad HTTP code: %d"), httpCode); - return false; - } - - httpResponse = httpClient.getString(); // very unfortunate that we cannot parse WifiClient stream directly - httpClient.end(); - - // 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) { - int _begin = authReq.indexOf(param); - if (_begin == -1) { return ""; } - return authReq.substring(_begin + param.length(), authReq.indexOf(delimit, _begin + param.length())); -} - -String HttpPowerMeterClass::getcNonce(const int len) { - static const char alphanum[] = "0123456789" - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "abcdefghijklmnopqrstuvwxyz"; - String s = ""; - - for (int i = 0; i < len; ++i) { s += alphanum[rand() % (sizeof(alphanum) - 1)]; } - - return s; -} - -String HttpPowerMeterClass::getDigestAuth(String& authReq, const String& username, const String& password, const String& method, const String& uri, unsigned int counter) { - // extracting required parameters for RFC 2617 Digest - String realm = extractParam(authReq, "realm=\"", '"'); - String nonce = extractParam(authReq, "nonce=\"", '"'); - String cNonce = getcNonce(8); - - char nc[9]; - snprintf(nc, sizeof(nc), "%08x", counter); - - //sha256 of the user:realm:password - String ha1 = sha256(username + ":" + realm + ":" + password); - - //sha256 of method:uri - String ha2 = sha256(method + ":" + uri); - - //sha256 of h1:nonce:nc:cNonce:auth:h2 - String response = sha256(ha1 + ":" + nonce + ":" + String(nc) + ":" + cNonce + ":" + "auth" + ":" + ha2); - - //Final authorization String; - String authorization = "Digest username=\""; - authorization += username; - authorization += "\", realm=\""; - authorization += realm; - authorization += "\", nonce=\""; - authorization += nonce; - authorization += "\", uri=\""; - authorization += uri; - authorization += "\", cnonce=\""; - authorization += cNonce; - authorization += "\", nc="; - authorization += String(nc); - authorization += ", qop=auth, response=\""; - authorization += response; - authorization += "\", algorithm=SHA-256"; - - return authorization; -} - -bool HttpPowerMeterClass::tryGetFloatValueForPhase(int phase, String jsonPath, Unit_t unit, bool signInverted) -{ - JsonDocument root; - const DeserializationError error = deserializeJson(root, httpResponse); - if (error) { - snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), - PSTR("[HttpPowerMeter] Unable to parse server response as JSON")); - return false; - } - - constexpr char delimiter = '/'; - int start = 0; - int end = jsonPath.indexOf(delimiter); - auto value = root.as(); - - auto getNext = [this, &value, &jsonPath, &start](String const& key) -> bool { - // handle double forward slashes and paths starting or ending with a slash - if (key.isEmpty()) { return true; } - - if (key[0] == '[' && key[key.length() - 1] == ']') { - if (!value.is()) { - snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), - PSTR("[HttpPowerMeter] Cannot access non-array JSON node " - "using array index '%s' (JSON path '%s', position %i)"), - key.c_str(), jsonPath.c_str(), start); - return false; - } - - auto idx = key.substring(1, key.length() - 1).toInt(); - value = value[idx]; - - if (value.isNull()) { - snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), - PSTR("[HttpPowerMeter] Unable to access JSON array " - "index %li (JSON path '%s', position %i)"), - idx, jsonPath.c_str(), start); - return false; - } - - return true; - } - - value = value[key]; - - if (value.isNull()) { - snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), - PSTR("[HttpPowerMeter] Unable to access JSON key " - "'%s' (JSON path '%s', position %i)"), - key.c_str(), jsonPath.c_str(), start); - return false; - } - - return true; - }; - - // NOTE: "Because ArduinoJson implements the Null Object Pattern, it is - // always safe to read the object: if the key doesn't exist, it returns an - // empty value." - while (end != -1) { - if (!getNext(jsonPath.substring(start, end))) { return false; } - start = end + 1; - end = jsonPath.indexOf(delimiter, start); - } - - if (!getNext(jsonPath.substring(start))) { return false; } - - if (!value.is()) { - snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), - PSTR("[HttpPowerMeter] not a float: '%s'"), - value.as().c_str()); - return false; - } - - // this value is supposed to be in Watts and positive if energy is consumed. - power[phase] = value.as(); - - 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; -} - -//extract url component as done by httpClient::begin(String url, const char* expectedProtocol) https://github.com/espressif/arduino-esp32/blob/da6325dd7e8e152094b19fe63190907f38ef1ff0/libraries/HTTPClient/src/HTTPClient.cpp#L250 -bool HttpPowerMeterClass::extractUrlComponents(String url, String& _protocol, String& _host, String& _uri, uint16_t& _port, String& _base64Authorization) -{ - // check for : (http: or https: - int index = url.indexOf(':'); - if(index < 0) { - snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError), PSTR("failed to parse protocol")); - return false; - } - - _protocol = url.substring(0, index); - - //initialize port to default values for http or https. - //port will be overwritten below in case port is explicitly defined - _port = (_protocol == "https" ? 443 : 80); - - url.remove(0, (index + 3)); // remove http:// or https:// - - index = url.indexOf('/'); - if (index == -1) { - index = url.length(); - url += '/'; - } - String host = url.substring(0, index); - url.remove(0, index); // remove host part - - // get Authorization - index = host.indexOf('@'); - if(index >= 0) { - // auth info - String auth = host.substring(0, index); - host.remove(0, index + 1); // remove auth part including @ - _base64Authorization = base64::encode(auth); - } - - // get port - index = host.indexOf(':'); - String the_host; - if(index >= 0) { - the_host = host.substring(0, index); // hostname - host.remove(0, (index + 1)); // remove hostname + : - _port = host.toInt(); // get port - } else { - the_host = host; - } - - _host = the_host; - _uri = url; - return true; -} - -String HttpPowerMeterClass::sha256(const String& data) { - uint8_t hash[32]; - - mbedtls_sha256_context ctx; - mbedtls_sha256_init(&ctx); - mbedtls_sha256_starts(&ctx, 0); // select SHA256 - mbedtls_sha256_update(&ctx, reinterpret_cast(data.c_str()), data.length()); - mbedtls_sha256_finish(&ctx, hash); - mbedtls_sha256_free(&ctx); - - char res[sizeof(hash) * 2 + 1]; - for (int i = 0; i < sizeof(hash); i++) { - snprintf(res + (i*2), sizeof(res) - (i*2), "%02x", hash[i]); - } - - return res; -} - -void HttpPowerMeterClass::prepareRequest(uint32_t timeout, const char* httpHeader, const char* httpValue) { - httpClient.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); - httpClient.setUserAgent("OpenDTU-OnBattery"); - httpClient.setConnectTimeout(timeout); - httpClient.setTimeout(timeout); - httpClient.addHeader("Content-Type", "application/json"); - httpClient.addHeader("Accept", "application/json"); - - if (strlen(httpHeader) > 0) { - httpClient.addHeader(httpHeader, httpValue); - } -} - -HttpPowerMeterClass HttpPowerMeter; diff --git a/src/Huawei_can.cpp b/src/Huawei_can.cpp index 20b013881..aef57a198 100644 --- a/src/Huawei_can.cpp +++ b/src/Huawei_can.cpp @@ -1,526 +1,526 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * 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; -HuaweiCanCommClass HuaweiCanComm; - -// ******************************************************* -// Huawei CAN Communication -// ******************************************************* - -// Using a C function to avoid static C++ member -void HuaweiCanCommunicationTask(void* parameter) { - for( ;; ) { - HuaweiCanComm.loop(); - yield(); - } -} - -bool HuaweiCanCommClass::init(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, - uint8_t huawei_irq, uint8_t huawei_cs, uint32_t frequency) { - SPI = new SPIClass(HSPI); - SPI->begin(huawei_clk, huawei_miso, huawei_mosi, huawei_cs); - pinMode(huawei_cs, OUTPUT); - digitalWrite(huawei_cs, HIGH); - - pinMode(huawei_irq, INPUT_PULLUP); - _huaweiIrq = huawei_irq; - - auto mcp_frequency = MCP_8MHZ; - if (16000000UL == frequency) { mcp_frequency = MCP_16MHZ; } - else if (8000000UL != frequency) { - MessageOutput.printf("Huawei CAN: unknown frequency %d Hz, using 8 MHz\r\n", mcp_frequency); - } - - _CAN = new MCP_CAN(SPI, huawei_cs); - if (!_CAN->begin(MCP_STDEXT, CAN_125KBPS, mcp_frequency) == CAN_OK) { - return false; - } - - const uint32_t myMask = 0xFFFFFFFF; // Look at all incoming bits and... - const uint32_t myFilter = 0x1081407F; // filter for this message only - _CAN->init_Mask(0, 1, myMask); - _CAN->init_Filt(0, 1, myFilter); - _CAN->init_Mask(1, 1, myMask); - - // Change to normal mode to allow messages to be transmitted - _CAN->setMode(MCP_NORMAL); - - return true; -} - -// Public methods need to obtain semaphore - -void HuaweiCanCommClass::loop() -{ - std::lock_guard lock(_mutex); - - INT32U rxId; - unsigned char len = 0; - unsigned char rxBuf[8]; - uint8_t i; - - if (!digitalRead(_huaweiIrq)) { - // If CAN_INT pin is low, read receive buffer - _CAN->readMsgBuf(&rxId, &len, rxBuf); // Read data: len = data length, buf = data byte(s) - if((rxId & 0x80000000) == 0x80000000) { // Determine if ID is standard (11 bits) or extended (29 bits) - if ((rxId & 0x1FFFFFFF) == 0x1081407F && len == 8) { - - uint32_t value = __bswap32(* reinterpret_cast (rxBuf + 4)); - - // Input power 0x70, Input frequency 0x71, Input current 0x72 - // Output power 0x73, Efficiency 0x74, Output Voltage 0x75 and Output Current 0x76 - if(rxBuf[1] >= 0x70 && rxBuf[1] <= 0x76 ) { - _recValues[rxBuf[1] - 0x70] = value; - } - - // Input voltage - if(rxBuf[1] == 0x78 ) { - _recValues[HUAWEI_INPUT_VOLTAGE_IDX] = value; - } - - // Output Temperature - if(rxBuf[1] == 0x7F ) { - _recValues[HUAWEI_OUTPUT_TEMPERATURE_IDX] = value; - } - - // Input Temperature 0x80, Output Current 1 0x81 and Output Current 2 0x82 - if(rxBuf[1] >= 0x80 && rxBuf[1] <= 0x82 ) { - _recValues[rxBuf[1] - 0x80 + HUAWEI_INPUT_TEMPERATURE_IDX] = value; - } - - // This is the last value that is send - if(rxBuf[1] == 0x81) { - _completeUpdateReceived = true; - } - } - } - // Other emitted codes not handled here are: 0x1081407E (Ack), 0x1081807E (Ack Frame), 0x1081D27F (Description), 0x1001117E (Whr meter), 0x100011FE (unclear), 0x108111FE (output enabled), 0x108081FE (unclear). See: - // https://github.com/craigpeacock/Huawei_R4850G2_CAN/blob/main/r4850.c - // https://www.beyondlogic.org/review-huawei-r4850g2-power-supply-53-5vdc-3kw/ - } - - // Transmit values - for (i = 0; i < HUAWEI_OFFLINE_CURRENT; i++) { - 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 - byte sndStat = _CAN->sendMsgBuf(0x108180FE, 1, 8, data); - if (sndStat == CAN_OK) { - _hasNewTxValue[i] = false; - } else { - _errorCode |= HUAWEI_ERROR_CODE_TX; - } - } - } - - if (_nextRequestMillis < millis()) { - sendRequest(); - _nextRequestMillis = millis() + HUAWEI_DATA_REQUEST_INTERVAL_MS; - } - -} - -uint32_t HuaweiCanCommClass::getParameterValue(uint8_t parameter) -{ - std::lock_guard lock(_mutex); - uint32_t v = 0; - if (parameter < HUAWEI_OUTPUT_CURRENT1_IDX) { - v = _recValues[parameter]; - } - return v; -} - -bool HuaweiCanCommClass::gotNewRxDataFrame(bool clear) -{ - std::lock_guard lock(_mutex); - bool b = false; - b = _completeUpdateReceived; - if (clear) { - _completeUpdateReceived = false; - } - return b; -} - -uint8_t HuaweiCanCommClass::getErrorCode(bool clear) -{ - std::lock_guard lock(_mutex); - uint8_t e = 0; - e = _errorCode; - if (clear) { - _errorCode = 0; - } - return e; -} - -void HuaweiCanCommClass::setParameterValue(uint16_t in, uint8_t parameterType) -{ - std::lock_guard lock(_mutex); - if (parameterType < HUAWEI_OFFLINE_CURRENT) { - _txValues[parameterType] = in; - _hasNewTxValue[parameterType] = true; - } -} - -// Private methods -// Requests current values from Huawei unit. Response is handled in onReceive -void HuaweiCanCommClass::sendRequest() -{ - uint8_t data[8] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - //Send extended message - byte sndStat = _CAN->sendMsgBuf(0x108040FE, 1, 8, data); - if(sndStat != CAN_OK) { - _errorCode |= HUAWEI_ERROR_CODE_RX; - } -} - -// ******************************************************* -// Huawei CAN Controller -// ******************************************************* - -void HuaweiCanClass::init(Scheduler& scheduler, uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, uint8_t huawei_irq, uint8_t huawei_cs, uint8_t huawei_power) -{ - scheduler.addTask(_loopTask); - _loopTask.setCallback(std::bind(&HuaweiCanClass::loop, this)); - _loopTask.setIterations(TASK_FOREVER); - _loopTask.enable(); - - this->updateSettings(huawei_miso, huawei_mosi, huawei_clk, huawei_irq, huawei_cs, huawei_power); -} - -void HuaweiCanClass::updateSettings(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, uint8_t huawei_irq, uint8_t huawei_cs, uint8_t huawei_power) -{ - if (_initialized) { - return; - } - - const CONFIG_T& config = Configuration.get(); - - if (!config.Huawei.Enabled) { - return; - } - - if (!HuaweiCanComm.init(huawei_miso, huawei_mosi, huawei_clk, huawei_irq, huawei_cs, config.Huawei.CAN_Controller_Frequency)) { - MessageOutput.println("[HuaweiCanClass::init] Error Initializing Huawei CAN communication..."); - return; - }; - - pinMode(huawei_power, OUTPUT); - digitalWrite(huawei_power, HIGH); - _huaweiPower = huawei_power; - - if (config.Huawei.Auto_Power_Enabled) { - _mode = HUAWEI_MODE_AUTO_INT; - } - - xTaskCreate(HuaweiCanCommunicationTask,"HUAWEI_CAN_0",1000,NULL,0,&_HuaweiCanCommunicationTaskHdl); - - MessageOutput.println("[HuaweiCanClass::init] MCP2515 Initialized Successfully!"); - _initialized = true; -} - -RectifierParameters_t * HuaweiCanClass::get() -{ - return &_rp; -} - - -void HuaweiCanClass::processReceivedParameters() -{ - _rp.input_power = HuaweiCanComm.getParameterValue(HUAWEI_INPUT_POWER_IDX) / 1024.0; - _rp.input_frequency = HuaweiCanComm.getParameterValue(HUAWEI_INPUT_FREQ_IDX) / 1024.0; - _rp.input_current = HuaweiCanComm.getParameterValue(HUAWEI_INPUT_CURRENT_IDX) / 1024.0; - _rp.output_power = HuaweiCanComm.getParameterValue(HUAWEI_OUTPUT_POWER_IDX) / 1024.0; - _rp.efficiency = HuaweiCanComm.getParameterValue(HUAWEI_EFFICIENCY_IDX) / 1024.0; - _rp.output_voltage = HuaweiCanComm.getParameterValue(HUAWEI_OUTPUT_VOLTAGE_IDX) / 1024.0; - _rp.max_output_current = static_cast(HuaweiCanComm.getParameterValue(HUAWEI_OUTPUT_CURRENT_MAX_IDX)) / MAX_CURRENT_MULTIPLIER; - _rp.input_voltage = HuaweiCanComm.getParameterValue(HUAWEI_INPUT_VOLTAGE_IDX) / 1024.0; - _rp.output_temp = HuaweiCanComm.getParameterValue(HUAWEI_OUTPUT_TEMPERATURE_IDX) / 1024.0; - _rp.input_temp = HuaweiCanComm.getParameterValue(HUAWEI_INPUT_TEMPERATURE_IDX) / 1024.0; - _rp.output_current = HuaweiCanComm.getParameterValue(HUAWEI_OUTPUT_CURRENT_IDX) / 1024.0; - - if (HuaweiCanComm.gotNewRxDataFrame(true)) { - _lastUpdateReceivedMillis = millis(); - } -} - - -void HuaweiCanClass::loop() -{ - const CONFIG_T& config = Configuration.get(); - - if (!config.Huawei.Enabled || !_initialized) { - return; - } - - bool verboseLogging = config.Huawei.VerboseLogging; - - processReceivedParameters(); - - uint8_t com_error = HuaweiCanComm.getErrorCode(true); - if (com_error & HUAWEI_ERROR_CODE_RX) { - MessageOutput.println("[HuaweiCanClass::loop] Data request error"); - } - if (com_error & HUAWEI_ERROR_CODE_TX) { - MessageOutput.println("[HuaweiCanClass::loop] Data set error"); - } - - // Print updated data - 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); - } - - // Internal PSU power pin (slot detect) control - if (_rp.output_current > HUAWEI_AUTO_MODE_SHUTDOWN_CURRENT) { - _outputCurrentOnSinceMillis = millis(); - } - if (_outputCurrentOnSinceMillis + HUAWEI_AUTO_MODE_SHUTDOWN_DELAY < millis() && - (_mode == HUAWEI_MODE_AUTO_EXT || _mode == HUAWEI_MODE_AUTO_INT)) { - digitalWrite(_huaweiPower, 1); - } - - - if (_mode == HUAWEI_MODE_AUTO_INT || _batteryEmergencyCharging) { - - // 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; - } - - 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; - } - - // Re-enable automatic power control if the output voltage has dropped below threshold - if(_rp.output_voltage < config.Huawei.Auto_Power_Enable_Voltage_Limit ) { - _autoPowerEnabledCounter = 10; - } - - - // Check if inverter used by the power limiter is active - std::shared_ptr inverter = - Hoymiles.getInverterBySerial(config.PowerLimiter.InverterId); - - if (inverter == nullptr && config.PowerLimiter.InverterId < INV_MAX_COUNT) { - // we previously had an index saved as InverterId. fall back to the - // respective positional lookup if InverterId is not a known serial. - inverter = Hoymiles.getInverterByPos(config.PowerLimiter.InverterId); - } - - if (inverter != nullptr) { - if(inverter->isProducing()) { - _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 - _autoModeBlockedTillMillis = millis() + 1000; - MessageOutput.printf("[HuaweiCanClass::loop] Inverter is active, disable\r\n"); - return; - } - } - - if (PowerMeter.getLastPowerMeterUpdate() > _lastPowerMeterUpdateReceivedMillis && - _autoPowerEnabledCounter > 0) { - // We have received a new PowerMeter value. Also we're _autoPowerEnabled - // So we're good to calculate a new limit - - _lastPowerMeterUpdateReceivedMillis = PowerMeter.getLastPowerMeterUpdate(); - - // Calculate new power limit - float newPowerLimit = -1 * round(PowerMeter.getPowerTotal()); - 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); - } - - // Check whether the battery SoC limit setting is enabled - if (config.Battery.Enabled && config.Huawei.Auto_Power_BatterySoC_Limits_Enabled) { - uint8_t _batterySoC = Battery.getStats()->getSoC(); - // Sets power limit to 0 if the BMS reported SoC reaches or exceeds the user configured value - 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) { - - // Check if the output power has dropped below the lower limit (i.e. the battery is full) - // and if the PSU should be turned off. Also we use a simple counter mechanism here to be able - // to ramp up from zero output power when starting up - if (_rp.output_power < config.Huawei.Auto_Power_Lower_Power_Limit) { - MessageOutput.printf("[HuaweiCanClass::loop] Power and voltage limit reached. Disabling automatic power control .... \r\n"); - _autoPowerEnabledCounter--; - if (_autoPowerEnabledCounter == 0) { - _autoPowerEnabled = false; - _setValue(0, HUAWEI_ONLINE_CURRENT); - return; - } - } else { - _autoPowerEnabledCounter = 10; - } - - // Limit power to maximum - if (newPowerLimit > config.Huawei.Auto_Power_Upper_Power_Limit) { - newPowerLimit = config.Huawei.Auto_Power_Upper_Power_Limit; - } - - // 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, e.g. Victron MPPT charger - 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); - - // Don't run auto mode some time to allow for output stabilization after issuing a new value - _autoModeBlockedTillMillis = millis() + 2 * HUAWEI_DATA_REQUEST_INTERVAL_MS; - } else { - // requested PL is below minium. Set current to 0 - _autoPowerEnabled = false; - _setValue(0.0, HUAWEI_ONLINE_CURRENT); - } - } - } -} - -void HuaweiCanClass::setValue(float in, uint8_t parameterType) -{ - if (_mode != HUAWEI_MODE_AUTO_INT) { - _setValue(in, parameterType); - } -} - -void HuaweiCanClass::_setValue(float in, uint8_t parameterType) -{ - - const CONFIG_T& config = Configuration.get(); - - if (!config.Huawei.Enabled) { - return; - } - - uint16_t value; - - 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 && - (_mode == HUAWEI_MODE_AUTO_EXT || _mode == HUAWEI_MODE_AUTO_INT)) { - digitalWrite(_huaweiPower, 0); - _outputCurrentOnSinceMillis = millis(); - } - - if (parameterType == HUAWEI_OFFLINE_VOLTAGE || parameterType == HUAWEI_ONLINE_VOLTAGE) { - value = in * 1024; - } else if (parameterType == HUAWEI_OFFLINE_CURRENT || parameterType == HUAWEI_ONLINE_CURRENT) { - value = in * MAX_CURRENT_MULTIPLIER; - } else { - return; - } - - HuaweiCanComm.setParameterValue(value, parameterType); -} - -void HuaweiCanClass::setMode(uint8_t mode) { - const CONFIG_T& config = Configuration.get(); - - if (!config.Huawei.Enabled) { - return; - } - - if(mode == HUAWEI_MODE_OFF) { - digitalWrite(_huaweiPower, 1); - _mode = HUAWEI_MODE_OFF; - } - if(mode == HUAWEI_MODE_ON) { - digitalWrite(_huaweiPower, 0); - _mode = HUAWEI_MODE_ON; - } - - if (mode == HUAWEI_MODE_AUTO_INT && !config.Huawei.Auto_Power_Enabled ) { - MessageOutput.println("[HuaweiCanClass::setMode] WARNING: Trying to setmode to internal automatic power control without being enabled in the UI. Ignoring command"); - return; - } - - if (_mode == HUAWEI_MODE_AUTO_INT && mode != HUAWEI_MODE_AUTO_INT) { - _autoPowerEnabled = false; - _setValue(0, HUAWEI_ONLINE_CURRENT); - } - - if(mode == HUAWEI_MODE_AUTO_EXT || mode == HUAWEI_MODE_AUTO_INT) { - _mode = mode; - } -} - - - +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * 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; +HuaweiCanCommClass HuaweiCanComm; + +// ******************************************************* +// Huawei CAN Communication +// ******************************************************* + +// Using a C function to avoid static C++ member +void HuaweiCanCommunicationTask(void* parameter) { + for( ;; ) { + HuaweiCanComm.loop(); + yield(); + } +} + +bool HuaweiCanCommClass::init(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, + uint8_t huawei_irq, uint8_t huawei_cs, uint32_t frequency) { + SPI = new SPIClass(HSPI); + SPI->begin(huawei_clk, huawei_miso, huawei_mosi, huawei_cs); + pinMode(huawei_cs, OUTPUT); + digitalWrite(huawei_cs, HIGH); + + pinMode(huawei_irq, INPUT_PULLUP); + _huaweiIrq = huawei_irq; + + auto mcp_frequency = MCP_8MHZ; + if (16000000UL == frequency) { mcp_frequency = MCP_16MHZ; } + else if (8000000UL != frequency) { + MessageOutput.printf("Huawei CAN: unknown frequency %d Hz, using 8 MHz\r\n", mcp_frequency); + } + + _CAN = new MCP_CAN(SPI, huawei_cs); + if (!_CAN->begin(MCP_STDEXT, CAN_125KBPS, mcp_frequency) == CAN_OK) { + return false; + } + + const uint32_t myMask = 0xFFFFFFFF; // Look at all incoming bits and... + const uint32_t myFilter = 0x1081407F; // filter for this message only + _CAN->init_Mask(0, 1, myMask); + _CAN->init_Filt(0, 1, myFilter); + _CAN->init_Mask(1, 1, myMask); + + // Change to normal mode to allow messages to be transmitted + _CAN->setMode(MCP_NORMAL); + + return true; +} + +// Public methods need to obtain semaphore + +void HuaweiCanCommClass::loop() +{ + std::lock_guard lock(_mutex); + + INT32U rxId; + unsigned char len = 0; + unsigned char rxBuf[8]; + uint8_t i; + + if (!digitalRead(_huaweiIrq)) { + // If CAN_INT pin is low, read receive buffer + _CAN->readMsgBuf(&rxId, &len, rxBuf); // Read data: len = data length, buf = data byte(s) + if((rxId & 0x80000000) == 0x80000000) { // Determine if ID is standard (11 bits) or extended (29 bits) + if ((rxId & 0x1FFFFFFF) == 0x1081407F && len == 8) { + + uint32_t value = __bswap32(* reinterpret_cast (rxBuf + 4)); + + // Input power 0x70, Input frequency 0x71, Input current 0x72 + // Output power 0x73, Efficiency 0x74, Output Voltage 0x75 and Output Current 0x76 + if(rxBuf[1] >= 0x70 && rxBuf[1] <= 0x76 ) { + _recValues[rxBuf[1] - 0x70] = value; + } + + // Input voltage + if(rxBuf[1] == 0x78 ) { + _recValues[HUAWEI_INPUT_VOLTAGE_IDX] = value; + } + + // Output Temperature + if(rxBuf[1] == 0x7F ) { + _recValues[HUAWEI_OUTPUT_TEMPERATURE_IDX] = value; + } + + // Input Temperature 0x80, Output Current 1 0x81 and Output Current 2 0x82 + if(rxBuf[1] >= 0x80 && rxBuf[1] <= 0x82 ) { + _recValues[rxBuf[1] - 0x80 + HUAWEI_INPUT_TEMPERATURE_IDX] = value; + } + + // This is the last value that is send + if(rxBuf[1] == 0x81) { + _completeUpdateReceived = true; + } + } + } + // Other emitted codes not handled here are: 0x1081407E (Ack), 0x1081807E (Ack Frame), 0x1081D27F (Description), 0x1001117E (Whr meter), 0x100011FE (unclear), 0x108111FE (output enabled), 0x108081FE (unclear). See: + // https://github.com/craigpeacock/Huawei_R4850G2_CAN/blob/main/r4850.c + // https://www.beyondlogic.org/review-huawei-r4850g2-power-supply-53-5vdc-3kw/ + } + + // Transmit values + for (i = 0; i < HUAWEI_OFFLINE_CURRENT; i++) { + 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 + byte sndStat = _CAN->sendMsgBuf(0x108180FE, 1, 8, data); + if (sndStat == CAN_OK) { + _hasNewTxValue[i] = false; + } else { + _errorCode |= HUAWEI_ERROR_CODE_TX; + } + } + } + + if (_nextRequestMillis < millis()) { + sendRequest(); + _nextRequestMillis = millis() + HUAWEI_DATA_REQUEST_INTERVAL_MS; + } + +} + +uint32_t HuaweiCanCommClass::getParameterValue(uint8_t parameter) +{ + std::lock_guard lock(_mutex); + uint32_t v = 0; + if (parameter < HUAWEI_OUTPUT_CURRENT1_IDX) { + v = _recValues[parameter]; + } + return v; +} + +bool HuaweiCanCommClass::gotNewRxDataFrame(bool clear) +{ + std::lock_guard lock(_mutex); + bool b = false; + b = _completeUpdateReceived; + if (clear) { + _completeUpdateReceived = false; + } + return b; +} + +uint8_t HuaweiCanCommClass::getErrorCode(bool clear) +{ + std::lock_guard lock(_mutex); + uint8_t e = 0; + e = _errorCode; + if (clear) { + _errorCode = 0; + } + return e; +} + +void HuaweiCanCommClass::setParameterValue(uint16_t in, uint8_t parameterType) +{ + std::lock_guard lock(_mutex); + if (parameterType < HUAWEI_OFFLINE_CURRENT) { + _txValues[parameterType] = in; + _hasNewTxValue[parameterType] = true; + } +} + +// Private methods +// Requests current values from Huawei unit. Response is handled in onReceive +void HuaweiCanCommClass::sendRequest() +{ + uint8_t data[8] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + //Send extended message + byte sndStat = _CAN->sendMsgBuf(0x108040FE, 1, 8, data); + if(sndStat != CAN_OK) { + _errorCode |= HUAWEI_ERROR_CODE_RX; + } +} + +// ******************************************************* +// Huawei CAN Controller +// ******************************************************* + +void HuaweiCanClass::init(Scheduler& scheduler, uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, uint8_t huawei_irq, uint8_t huawei_cs, uint8_t huawei_power) +{ + scheduler.addTask(_loopTask); + _loopTask.setCallback(std::bind(&HuaweiCanClass::loop, this)); + _loopTask.setIterations(TASK_FOREVER); + _loopTask.enable(); + + this->updateSettings(huawei_miso, huawei_mosi, huawei_clk, huawei_irq, huawei_cs, huawei_power); +} + +void HuaweiCanClass::updateSettings(uint8_t huawei_miso, uint8_t huawei_mosi, uint8_t huawei_clk, uint8_t huawei_irq, uint8_t huawei_cs, uint8_t huawei_power) +{ + if (_initialized) { + return; + } + + const CONFIG_T& config = Configuration.get(); + + if (!config.Huawei.Enabled) { + return; + } + + if (!HuaweiCanComm.init(huawei_miso, huawei_mosi, huawei_clk, huawei_irq, huawei_cs, config.Huawei.CAN_Controller_Frequency)) { + MessageOutput.println("[HuaweiCanClass::init] Error Initializing Huawei CAN communication..."); + return; + }; + + pinMode(huawei_power, OUTPUT); + digitalWrite(huawei_power, HIGH); + _huaweiPower = huawei_power; + + if (config.Huawei.Auto_Power_Enabled) { + _mode = HUAWEI_MODE_AUTO_INT; + } + + xTaskCreate(HuaweiCanCommunicationTask,"HUAWEI_CAN_0",1000,NULL,0,&_HuaweiCanCommunicationTaskHdl); + + MessageOutput.println("[HuaweiCanClass::init] MCP2515 Initialized Successfully!"); + _initialized = true; +} + +RectifierParameters_t * HuaweiCanClass::get() +{ + return &_rp; +} + + +void HuaweiCanClass::processReceivedParameters() +{ + _rp.input_power = HuaweiCanComm.getParameterValue(HUAWEI_INPUT_POWER_IDX) / 1024.0; + _rp.input_frequency = HuaweiCanComm.getParameterValue(HUAWEI_INPUT_FREQ_IDX) / 1024.0; + _rp.input_current = HuaweiCanComm.getParameterValue(HUAWEI_INPUT_CURRENT_IDX) / 1024.0; + _rp.output_power = HuaweiCanComm.getParameterValue(HUAWEI_OUTPUT_POWER_IDX) / 1024.0; + _rp.efficiency = HuaweiCanComm.getParameterValue(HUAWEI_EFFICIENCY_IDX) / 1024.0; + _rp.output_voltage = HuaweiCanComm.getParameterValue(HUAWEI_OUTPUT_VOLTAGE_IDX) / 1024.0; + _rp.max_output_current = static_cast(HuaweiCanComm.getParameterValue(HUAWEI_OUTPUT_CURRENT_MAX_IDX)) / MAX_CURRENT_MULTIPLIER; + _rp.input_voltage = HuaweiCanComm.getParameterValue(HUAWEI_INPUT_VOLTAGE_IDX) / 1024.0; + _rp.output_temp = HuaweiCanComm.getParameterValue(HUAWEI_OUTPUT_TEMPERATURE_IDX) / 1024.0; + _rp.input_temp = HuaweiCanComm.getParameterValue(HUAWEI_INPUT_TEMPERATURE_IDX) / 1024.0; + _rp.output_current = HuaweiCanComm.getParameterValue(HUAWEI_OUTPUT_CURRENT_IDX) / 1024.0; + + if (HuaweiCanComm.gotNewRxDataFrame(true)) { + _lastUpdateReceivedMillis = millis(); + } +} + + +void HuaweiCanClass::loop() +{ + const CONFIG_T& config = Configuration.get(); + + if (!config.Huawei.Enabled || !_initialized) { + return; + } + + bool verboseLogging = config.Huawei.VerboseLogging; + + processReceivedParameters(); + + uint8_t com_error = HuaweiCanComm.getErrorCode(true); + if (com_error & HUAWEI_ERROR_CODE_RX) { + MessageOutput.println("[HuaweiCanClass::loop] Data request error"); + } + if (com_error & HUAWEI_ERROR_CODE_TX) { + MessageOutput.println("[HuaweiCanClass::loop] Data set error"); + } + + // Print updated data + 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); + } + + // Internal PSU power pin (slot detect) control + if (_rp.output_current > HUAWEI_AUTO_MODE_SHUTDOWN_CURRENT) { + _outputCurrentOnSinceMillis = millis(); + } + if (_outputCurrentOnSinceMillis + HUAWEI_AUTO_MODE_SHUTDOWN_DELAY < millis() && + (_mode == HUAWEI_MODE_AUTO_EXT || _mode == HUAWEI_MODE_AUTO_INT)) { + digitalWrite(_huaweiPower, 1); + } + + + if (_mode == HUAWEI_MODE_AUTO_INT || _batteryEmergencyCharging) { + + // 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; + } + + 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; + } + + // Re-enable automatic power control if the output voltage has dropped below threshold + if(_rp.output_voltage < config.Huawei.Auto_Power_Enable_Voltage_Limit ) { + _autoPowerEnabledCounter = 10; + } + + + // Check if inverter used by the power limiter is active + std::shared_ptr inverter = + Hoymiles.getInverterBySerial(config.PowerLimiter.InverterId); + + if (inverter == nullptr && config.PowerLimiter.InverterId < INV_MAX_COUNT) { + // we previously had an index saved as InverterId. fall back to the + // respective positional lookup if InverterId is not a known serial. + inverter = Hoymiles.getInverterByPos(config.PowerLimiter.InverterId); + } + + if (inverter != nullptr) { + if(inverter->isProducing()) { + _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 + _autoModeBlockedTillMillis = millis() + 1000; + MessageOutput.printf("[HuaweiCanClass::loop] Inverter is active, disable\r\n"); + return; + } + } + + if (PowerMeter.getLastUpdate() > _lastPowerMeterUpdateReceivedMillis && + _autoPowerEnabledCounter > 0) { + // We have received a new PowerMeter value. Also we're _autoPowerEnabled + // So we're good to calculate a new limit + + _lastPowerMeterUpdateReceivedMillis = PowerMeter.getLastUpdate(); + + // Calculate new power limit + float newPowerLimit = -1 * round(PowerMeter.getPowerTotal()); + 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); + } + + // Check whether the battery SoC limit setting is enabled + if (config.Battery.Enabled && config.Huawei.Auto_Power_BatterySoC_Limits_Enabled) { + uint8_t _batterySoC = Battery.getStats()->getSoC(); + // Sets power limit to 0 if the BMS reported SoC reaches or exceeds the user configured value + 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) { + + // Check if the output power has dropped below the lower limit (i.e. the battery is full) + // and if the PSU should be turned off. Also we use a simple counter mechanism here to be able + // to ramp up from zero output power when starting up + if (_rp.output_power < config.Huawei.Auto_Power_Lower_Power_Limit) { + MessageOutput.printf("[HuaweiCanClass::loop] Power and voltage limit reached. Disabling automatic power control .... \r\n"); + _autoPowerEnabledCounter--; + if (_autoPowerEnabledCounter == 0) { + _autoPowerEnabled = false; + _setValue(0, HUAWEI_ONLINE_CURRENT); + return; + } + } else { + _autoPowerEnabledCounter = 10; + } + + // Limit power to maximum + if (newPowerLimit > config.Huawei.Auto_Power_Upper_Power_Limit) { + newPowerLimit = config.Huawei.Auto_Power_Upper_Power_Limit; + } + + // 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, e.g. Victron MPPT charger + 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); + + // Don't run auto mode some time to allow for output stabilization after issuing a new value + _autoModeBlockedTillMillis = millis() + 2 * HUAWEI_DATA_REQUEST_INTERVAL_MS; + } else { + // requested PL is below minium. Set current to 0 + _autoPowerEnabled = false; + _setValue(0.0, HUAWEI_ONLINE_CURRENT); + } + } + } +} + +void HuaweiCanClass::setValue(float in, uint8_t parameterType) +{ + if (_mode != HUAWEI_MODE_AUTO_INT) { + _setValue(in, parameterType); + } +} + +void HuaweiCanClass::_setValue(float in, uint8_t parameterType) +{ + + const CONFIG_T& config = Configuration.get(); + + if (!config.Huawei.Enabled) { + return; + } + + uint16_t value; + + 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 && + (_mode == HUAWEI_MODE_AUTO_EXT || _mode == HUAWEI_MODE_AUTO_INT)) { + digitalWrite(_huaweiPower, 0); + _outputCurrentOnSinceMillis = millis(); + } + + if (parameterType == HUAWEI_OFFLINE_VOLTAGE || parameterType == HUAWEI_ONLINE_VOLTAGE) { + value = in * 1024; + } else if (parameterType == HUAWEI_OFFLINE_CURRENT || parameterType == HUAWEI_ONLINE_CURRENT) { + value = in * MAX_CURRENT_MULTIPLIER; + } else { + return; + } + + HuaweiCanComm.setParameterValue(value, parameterType); +} + +void HuaweiCanClass::setMode(uint8_t mode) { + const CONFIG_T& config = Configuration.get(); + + if (!config.Huawei.Enabled) { + return; + } + + if(mode == HUAWEI_MODE_OFF) { + digitalWrite(_huaweiPower, 1); + _mode = HUAWEI_MODE_OFF; + } + if(mode == HUAWEI_MODE_ON) { + digitalWrite(_huaweiPower, 0); + _mode = HUAWEI_MODE_ON; + } + + if (mode == HUAWEI_MODE_AUTO_INT && !config.Huawei.Auto_Power_Enabled ) { + MessageOutput.println("[HuaweiCanClass::setMode] WARNING: Trying to setmode to internal automatic power control without being enabled in the UI. Ignoring command"); + return; + } + + if (_mode == HUAWEI_MODE_AUTO_INT && mode != HUAWEI_MODE_AUTO_INT) { + _autoPowerEnabled = false; + _setValue(0, HUAWEI_ONLINE_CURRENT); + } + + if(mode == HUAWEI_MODE_AUTO_EXT || mode == HUAWEI_MODE_AUTO_INT) { + _mode = mode; + } +} + + + diff --git a/src/InverterSettings.cpp b/src/InverterSettings.cpp index 19605af14..3be1927c7 100644 --- a/src/InverterSettings.cpp +++ b/src/InverterSettings.cpp @@ -85,6 +85,7 @@ void InverterSettingsClass::init(Scheduler& scheduler) inv->setReachableThreshold(config.Inverter[i].ReachableThreshold); inv->setZeroValuesIfUnreachable(config.Inverter[i].ZeroRuntimeDataIfUnrechable); inv->setZeroYieldDayOnMidnight(config.Inverter[i].ZeroYieldDayOnMidnight); + inv->setClearEventlogOnMidnight(config.Inverter[i].ClearEventlogOnMidnight); inv->Statistics()->setYieldDayCorrection(config.Inverter[i].YieldDayCorrection); for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { inv->Statistics()->setStringMaxPower(c, config.Inverter[i].channel[c].MaxChannelPower); diff --git a/src/JkBmsController.cpp b/src/JkBmsController.cpp index c39664e00..db1a2d5b7 100644 --- a/src/JkBmsController.cpp +++ b/src/JkBmsController.cpp @@ -10,195 +10,6 @@ namespace JkBms { -#ifdef JKBMS_DUMMY_SERIAL -class DummySerial { - public: - DummySerial() = default; - void begin(uint32_t, uint32_t, int8_t, int8_t) { - MessageOutput.println("JK BMS Dummy Serial: begin()"); - } - void end() { MessageOutput.println("JK BMS Dummy Serial: end()"); } - void flush() { } - bool availableForWrite() const { return true; } - size_t write(const uint8_t *buffer, size_t size) { - MessageOutput.printf("JK BMS Dummy Serial: write(%d Bytes)\r\n", size); - _byte_idx = 0; - _msg_idx = (_msg_idx + 1) % _data.size(); - return size; - } - bool available() const { - return _byte_idx < _data[_msg_idx].size(); - } - int read() { - if (_byte_idx >= _data[_msg_idx].size()) { return 0; } - return _data[_msg_idx][_byte_idx++]; - } - - private: - std::vector> const _data = - { - { - 0x4e, 0x57, 0x01, 0x21, 0x00, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x01, 0x79, 0x30, 0x01, 0x0c, 0xfb, - 0x02, 0x0c, 0xfb, 0x03, 0x0c, 0xfb, 0x04, 0x0c, - 0xfb, 0x05, 0x0c, 0xfb, 0x06, 0x0c, 0xfb, 0x07, - 0x0c, 0xfb, 0x08, 0x0c, 0xf7, 0x09, 0x0d, 0x01, - 0x0a, 0x0c, 0xf9, 0x0b, 0x0c, 0xfb, 0x0c, 0x0c, - 0xfb, 0x0d, 0x0c, 0xfb, 0x0e, 0x0c, 0xf8, 0x0f, - 0x0c, 0xf9, 0x10, 0x0c, 0xfb, 0x80, 0x00, 0x1a, - 0x81, 0x00, 0x12, 0x82, 0x00, 0x12, 0x83, 0x14, - 0xc3, 0x84, 0x83, 0xf4, 0x85, 0x2e, 0x86, 0x02, - 0x87, 0x00, 0x15, 0x89, 0x00, 0x00, 0x13, 0x52, - 0x8a, 0x00, 0x10, 0x8b, 0x00, 0x00, 0x8c, 0x00, - 0x03, 0x8e, 0x16, 0x80, 0x8f, 0x12, 0xc0, 0x90, - 0x0e, 0x10, 0x91, 0x0c, 0xda, 0x92, 0x00, 0x05, - 0x93, 0x0b, 0xb8, 0x94, 0x0c, 0x80, 0x95, 0x00, - 0x05, 0x96, 0x01, 0x2c, 0x97, 0x00, 0x28, 0x98, - 0x01, 0x2c, 0x99, 0x00, 0x28, 0x9a, 0x00, 0x1e, - 0x9b, 0x0b, 0xb8, 0x9c, 0x00, 0x0a, 0x9d, 0x01, - 0x9e, 0x00, 0x64, 0x9f, 0x00, 0x50, 0xa0, 0x00, - 0x64, 0xa1, 0x00, 0x64, 0xa2, 0x00, 0x14, 0xa3, - 0x00, 0x46, 0xa4, 0x00, 0x46, 0xa5, 0x00, 0x00, - 0xa6, 0x00, 0x02, 0xa7, 0xff, 0xec, 0xa8, 0xff, - 0xf6, 0xa9, 0x10, 0xaa, 0x00, 0x00, 0x00, 0xe6, - 0xab, 0x01, 0xac, 0x01, 0xad, 0x04, 0x4d, 0xae, - 0x01, 0xaf, 0x00, 0xb0, 0x00, 0x0a, 0xb1, 0x14, - 0xb2, 0x32, 0x32, 0x31, 0x31, 0x38, 0x37, 0x00, - 0x00, 0x00, 0x00, 0xb3, 0x00, 0xb4, 0x62, 0x65, - 0x6b, 0x69, 0x00, 0x00, 0x00, 0x00, 0xb5, 0x32, - 0x33, 0x30, 0x36, 0xb6, 0x00, 0x01, 0x4a, 0xc3, - 0xb7, 0x31, 0x31, 0x2e, 0x58, 0x57, 0x5f, 0x53, - 0x31, 0x31, 0x2e, 0x32, 0x36, 0x32, 0x48, 0x5f, - 0xb8, 0x00, 0xb9, 0x00, 0x00, 0x00, 0xe6, 0xba, - 0x62, 0x65, 0x6b, 0x69, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x4a, 0x4b, 0x5f, 0x42, - 0x31, 0x41, 0x32, 0x34, 0x53, 0x31, 0x35, 0x50, - 0xc0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x68, 0x00, - 0x00, 0x53, 0xbb - }, - { - 0x4e, 0x57, 0x01, 0x21, 0x00, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x01, 0x79, 0x30, 0x01, 0x0c, 0xc0, - 0x02, 0x0c, 0xc1, 0x03, 0x0c, 0xc0, 0x04, 0x0c, - 0xc4, 0x05, 0x0c, 0xc4, 0x06, 0x0c, 0xc2, 0x07, - 0x0c, 0xc2, 0x08, 0x0c, 0xc1, 0x09, 0x0c, 0xba, - 0x0a, 0x0c, 0xc1, 0x0b, 0x0c, 0xc2, 0x0c, 0x0c, - 0xc2, 0x0d, 0x0c, 0xc2, 0x0e, 0x0c, 0xc4, 0x0f, - 0x0c, 0xc2, 0x10, 0x0c, 0xc1, 0x80, 0x00, 0x1b, - 0x81, 0x00, 0x1b, 0x82, 0x00, 0x1a, 0x83, 0x14, - 0x68, 0x84, 0x03, 0x70, 0x85, 0x3c, 0x86, 0x02, - 0x87, 0x00, 0x19, 0x89, 0x00, 0x00, 0x16, 0x86, - 0x8a, 0x00, 0x10, 0x8b, 0x00, 0x00, 0x8c, 0x00, - 0x07, 0x8e, 0x16, 0x80, 0x8f, 0x12, 0xc0, 0x90, - 0x0e, 0x10, 0x91, 0x0c, 0xda, 0x92, 0x00, 0x05, - 0x93, 0x0b, 0xb8, 0x94, 0x0c, 0x80, 0x95, 0x00, - 0x05, 0x96, 0x01, 0x2c, 0x97, 0x00, 0x28, 0x98, - 0x01, 0x2c, 0x99, 0x00, 0x28, 0x9a, 0x00, 0x1e, - 0x9b, 0x0b, 0xb8, 0x9c, 0x00, 0x0a, 0x9d, 0x01, - 0x9e, 0x00, 0x64, 0x9f, 0x00, 0x50, 0xa0, 0x00, - 0x64, 0xa1, 0x00, 0x64, 0xa2, 0x00, 0x14, 0xa3, - 0x00, 0x46, 0xa4, 0x00, 0x46, 0xa5, 0x00, 0x00, - 0xa6, 0x00, 0x02, 0xa7, 0xff, 0xec, 0xa8, 0xff, - 0xf6, 0xa9, 0x10, 0xaa, 0x00, 0x00, 0x00, 0xe6, - 0xab, 0x01, 0xac, 0x01, 0xad, 0x04, 0x4d, 0xae, - 0x01, 0xaf, 0x00, 0xb0, 0x00, 0x0a, 0xb1, 0x14, - 0xb2, 0x32, 0x32, 0x31, 0x31, 0x38, 0x37, 0x00, - 0x00, 0x00, 0x00, 0xb3, 0x00, 0xb4, 0x62, 0x65, - 0x6b, 0x69, 0x00, 0x00, 0x00, 0x00, 0xb5, 0x32, - 0x33, 0x30, 0x36, 0xb6, 0x00, 0x01, 0x7f, 0x2a, - 0xb7, 0x31, 0x31, 0x2e, 0x58, 0x57, 0x5f, 0x53, - 0x31, 0x31, 0x2e, 0x32, 0x36, 0x32, 0x48, 0x5f, - 0xb8, 0x00, 0xb9, 0x00, 0x00, 0x00, 0xe6, 0xba, - 0x62, 0x65, 0x6b, 0x69, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x4a, 0x4b, 0x5f, 0x42, - 0x31, 0x41, 0x32, 0x34, 0x53, 0x31, 0x35, 0x50, - 0xc0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x68, 0x00, - 0x00, 0x4f, 0xc1 - }, - { - 0x4e, 0x57, 0x01, 0x21, 0x00, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x01, 0x79, 0x30, 0x01, 0x0c, 0x13, - 0x02, 0x0c, 0x12, 0x03, 0x0c, 0x0f, 0x04, 0x0c, - 0x15, 0x05, 0x0c, 0x0d, 0x06, 0x0c, 0x13, 0x07, - 0x0c, 0x16, 0x08, 0x0c, 0x13, 0x09, 0x0b, 0xdb, - 0x0a, 0x0b, 0xf6, 0x0b, 0x0c, 0x17, 0x0c, 0x0b, - 0xf5, 0x0d, 0x0c, 0x16, 0x0e, 0x0c, 0x1a, 0x0f, - 0x0c, 0x1b, 0x10, 0x0c, 0x1c, 0x80, 0x00, 0x18, - 0x81, 0x00, 0x18, 0x82, 0x00, 0x18, 0x83, 0x13, - 0x49, 0x84, 0x00, 0x00, 0x85, 0x00, 0x86, 0x02, - 0x87, 0x00, 0x23, 0x89, 0x00, 0x00, 0x20, 0x14, - 0x8a, 0x00, 0x10, 0x8b, 0x00, 0x08, 0x8c, 0x00, - 0x05, 0x8e, 0x16, 0x80, 0x8f, 0x12, 0xc0, 0x90, - 0x0e, 0x10, 0x91, 0x0c, 0xda, 0x92, 0x00, 0x05, - 0x93, 0x0b, 0xb8, 0x94, 0x0c, 0x80, 0x95, 0x00, - 0x05, 0x96, 0x01, 0x2c, 0x97, 0x00, 0x28, 0x98, - 0x01, 0x2c, 0x99, 0x00, 0x28, 0x9a, 0x00, 0x1e, - 0x9b, 0x0b, 0xb8, 0x9c, 0x00, 0x0a, 0x9d, 0x01, - 0x9e, 0x00, 0x64, 0x9f, 0x00, 0x50, 0xa0, 0x00, - 0x64, 0xa1, 0x00, 0x64, 0xa2, 0x00, 0x14, 0xa3, - 0x00, 0x46, 0xa4, 0x00, 0x46, 0xa5, 0x00, 0x00, - 0xa6, 0x00, 0x02, 0xa7, 0xff, 0xec, 0xa8, 0xff, - 0xf6, 0xa9, 0x10, 0xaa, 0x00, 0x00, 0x00, 0xe6, - 0xab, 0x01, 0xac, 0x01, 0xad, 0x04, 0x4d, 0xae, - 0x01, 0xaf, 0x00, 0xb0, 0x00, 0x0a, 0xb1, 0x14, - 0xb2, 0x32, 0x32, 0x31, 0x31, 0x38, 0x37, 0x00, - 0x00, 0x00, 0x00, 0xb3, 0x00, 0xb4, 0x62, 0x65, - 0x6b, 0x69, 0x00, 0x00, 0x00, 0x00, 0xb5, 0x32, - 0x33, 0x30, 0x36, 0xb6, 0x00, 0x02, 0x17, 0x10, - 0xb7, 0x31, 0x31, 0x2e, 0x58, 0x57, 0x5f, 0x53, - 0x31, 0x31, 0x2e, 0x32, 0x36, 0x32, 0x48, 0x5f, - 0xb8, 0x00, 0xb9, 0x00, 0x00, 0x00, 0xe6, 0xba, - 0x62, 0x65, 0x6b, 0x69, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x4a, 0x4b, 0x5f, 0x42, - 0x31, 0x41, 0x32, 0x34, 0x53, 0x31, 0x35, 0x50, - 0xc0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x68, 0x00, - 0x00, 0x45, 0xce - }, - { - 0x4e, 0x57, 0x01, 0x21, 0x00, 0x00, 0x00, 0x00, - 0x06, 0x00, 0x01, 0x79, 0x30, 0x01, 0x0c, 0x07, - 0x02, 0x0c, 0x0a, 0x03, 0x0c, 0x0b, 0x04, 0x0c, - 0x08, 0x05, 0x0c, 0x05, 0x06, 0x0c, 0x0b, 0x07, - 0x0c, 0x07, 0x08, 0x0c, 0x0a, 0x09, 0x0c, 0x08, - 0x0a, 0x0c, 0x06, 0x0b, 0x0c, 0x0a, 0x0c, 0x0c, - 0x05, 0x0d, 0x0c, 0x0a, 0x0e, 0x0c, 0x0a, 0x0f, - 0x0c, 0x0a, 0x10, 0x0c, 0x0a, 0x80, 0x00, 0x06, - 0x81, 0x00, 0x03, 0x82, 0x00, 0x03, 0x83, 0x13, - 0x40, 0x84, 0x00, 0x00, 0x85, 0x29, 0x86, 0x02, - 0x87, 0x00, 0x01, 0x89, 0x00, 0x00, 0x01, 0x0a, - 0x8a, 0x00, 0x10, 0x8b, 0x02, 0x00, 0x8c, 0x00, - 0x02, 0x8e, 0x16, 0x80, 0x8f, 0x10, 0x40, 0x90, - 0x0e, 0x10, 0x91, 0x0d, 0xde, 0x92, 0x00, 0x05, - 0x93, 0x0a, 0x28, 0x94, 0x0a, 0x5a, 0x95, 0x00, - 0x05, 0x96, 0x01, 0x2c, 0x97, 0x00, 0x28, 0x98, - 0x01, 0x2c, 0x99, 0x00, 0x28, 0x9a, 0x00, 0x1e, - 0x9b, 0x0b, 0xb8, 0x9c, 0x00, 0x0a, 0x9d, 0x01, - 0x9e, 0x00, 0x5a, 0x9f, 0x00, 0x50, 0xa0, 0x00, - 0x64, 0xa1, 0x00, 0x64, 0xa2, 0x00, 0x14, 0xa3, - 0x00, 0x37, 0xa4, 0x00, 0x37, 0xa5, 0x00, 0x03, - 0xa6, 0x00, 0x05, 0xa7, 0xff, 0xec, 0xa8, 0xff, - 0xf6, 0xa9, 0x10, 0xaa, 0x00, 0x00, 0x00, 0xe6, - 0xab, 0x01, 0xac, 0x01, 0xad, 0x04, 0x4d, 0xae, - 0x01, 0xaf, 0x00, 0xb0, 0x00, 0x0a, 0xb1, 0x14, - 0xb2, 0x32, 0x32, 0x31, 0x31, 0x38, 0x37, 0x00, - 0x00, 0x00, 0x00, 0xb3, 0x00, 0xb4, 0x62, 0x65, - 0x6b, 0x69, 0x00, 0x00, 0x00, 0x00, 0xb5, 0x32, - 0x33, 0x30, 0x36, 0xb6, 0x00, 0x03, 0xb7, 0x2d, - 0xb7, 0x31, 0x31, 0x2e, 0x58, 0x57, 0x5f, 0x53, - 0x31, 0x31, 0x2e, 0x32, 0x36, 0x32, 0x48, 0x5f, - 0xb8, 0x00, 0xb9, 0x00, 0x00, 0x00, 0xe6, 0xba, - 0x62, 0x65, 0x6b, 0x69, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x4a, 0x4b, 0x5f, 0x42, - 0x31, 0x41, 0x32, 0x34, 0x53, 0x31, 0x35, 0x50, - 0xc0, 0x01, 0x00, 0x00, 0x00, 0x00, 0x68, 0x00, - 0x00, 0x41, 0x7b - } - }; - size_t _msg_idx = 0; - size_t _byte_idx = 0; -}; -#endif - bool Controller::init(bool verboseLogging) { _verboseLogging = verboseLogging; diff --git a/src/MessageOutput.cpp b/src/MessageOutput.cpp index 850dbccab..04e9ddd44 100644 --- a/src/MessageOutput.cpp +++ b/src/MessageOutput.cpp @@ -1,116 +1,114 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * Copyright (C) 2022-2024 Thomas Basler and others - */ -#include -#include "MessageOutput.h" - -MessageOutputClass MessageOutput; - -MessageOutputClass::MessageOutputClass() - : _loopTask(TASK_IMMEDIATE, TASK_FOREVER, std::bind(&MessageOutputClass::loop, this)) -{ -} - -void MessageOutputClass::init(Scheduler& scheduler) -{ - scheduler.addTask(_loopTask); - _loopTask.enable(); -} - -void MessageOutputClass::register_ws_output(AsyncWebSocket* output) -{ - std::lock_guard lock(_msgLock); - - _ws = output; -} - -void MessageOutputClass::serialWrite(MessageOutputClass::message_t const& m) -{ - // on ESP32-S3, Serial.flush() blocks until a serial console is attached. - // operator bool() of HWCDC returns false if the device is not attached to - // a USB host. in general it makes sense to skip writing entirely if the - // default serial port is not ready. - if (!Serial) { return; } - - size_t written = 0; - while (written < m.size()) { - written += Serial.write(m.data() + written, m.size() - written); - } - Serial.flush(); -} - -size_t MessageOutputClass::write(uint8_t c) -{ - std::lock_guard lock(_msgLock); - - auto res = _task_messages.emplace(xTaskGetCurrentTaskHandle(), message_t()); - auto iter = res.first; - auto& message = iter->second; - - message.push_back(c); - - if (c == '\n') { - serialWrite(message); - _lines.emplace(std::move(message)); - _task_messages.erase(iter); - } - - return 1; -} - -size_t MessageOutputClass::write(const uint8_t *buffer, size_t size) -{ - std::lock_guard lock(_msgLock); - - auto res = _task_messages.emplace(xTaskGetCurrentTaskHandle(), message_t()); - auto iter = res.first; - auto& message = iter->second; - - message.reserve(message.size() + size); - - for (size_t idx = 0; idx < size; ++idx) { - uint8_t c = buffer[idx]; - - message.push_back(c); - - if (c == '\n') { - serialWrite(message); - _lines.emplace(std::move(message)); - message.clear(); - message.reserve(size - idx - 1); - } - } - - if (message.empty()) { _task_messages.erase(iter); } - - return size; -} - -void MessageOutputClass::loop() -{ - std::lock_guard lock(_msgLock); - - // clean up (possibly filled) buffers of deleted tasks - auto map_iter = _task_messages.begin(); - while (map_iter != _task_messages.end()) { - if (eTaskGetState(map_iter->first) == eDeleted) { - map_iter = _task_messages.erase(map_iter); - continue; - } - - ++map_iter; - } - - if (!_ws) { - while (!_lines.empty()) { - _lines.pop(); // do not hog memory - } - return; - } - - while (!_lines.empty() && _ws->availableForWriteAll()) { - _ws->textAll(std::make_shared(std::move(_lines.front()))); - _lines.pop(); - } -} +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022-2024 Thomas Basler and others + */ +#include +#include "MessageOutput.h" + +MessageOutputClass MessageOutput; + +MessageOutputClass::MessageOutputClass() + : _loopTask(TASK_IMMEDIATE, TASK_FOREVER, std::bind(&MessageOutputClass::loop, this)) +{ +} + +void MessageOutputClass::init(Scheduler& scheduler) +{ + scheduler.addTask(_loopTask); + _loopTask.enable(); +} + +void MessageOutputClass::register_ws_output(AsyncWebSocket* output) +{ + std::lock_guard lock(_msgLock); + + _ws = output; +} + +void MessageOutputClass::serialWrite(MessageOutputClass::message_t const& m) +{ + // operator bool() of HWCDC returns false if the device is not attached to + // a USB host. in general it makes sense to skip writing entirely if the + // default serial port is not ready. + if (!Serial) { return; } + + size_t written = 0; + while (written < m.size()) { + written += Serial.write(m.data() + written, m.size() - written); + } +} + +size_t MessageOutputClass::write(uint8_t c) +{ + std::lock_guard lock(_msgLock); + + auto res = _task_messages.emplace(xTaskGetCurrentTaskHandle(), message_t()); + auto iter = res.first; + auto& message = iter->second; + + message.push_back(c); + + if (c == '\n') { + serialWrite(message); + _lines.emplace(std::move(message)); + _task_messages.erase(iter); + } + + return 1; +} + +size_t MessageOutputClass::write(const uint8_t *buffer, size_t size) +{ + std::lock_guard lock(_msgLock); + + auto res = _task_messages.emplace(xTaskGetCurrentTaskHandle(), message_t()); + auto iter = res.first; + auto& message = iter->second; + + message.reserve(message.size() + size); + + for (size_t idx = 0; idx < size; ++idx) { + uint8_t c = buffer[idx]; + + message.push_back(c); + + if (c == '\n') { + serialWrite(message); + _lines.emplace(std::move(message)); + message.clear(); + message.reserve(size - idx - 1); + } + } + + if (message.empty()) { _task_messages.erase(iter); } + + return size; +} + +void MessageOutputClass::loop() +{ + std::lock_guard lock(_msgLock); + + // clean up (possibly filled) buffers of deleted tasks + auto map_iter = _task_messages.begin(); + while (map_iter != _task_messages.end()) { + if (eTaskGetState(map_iter->first) == eDeleted) { + map_iter = _task_messages.erase(map_iter); + continue; + } + + ++map_iter; + } + + if (!_ws) { + while (!_lines.empty()) { + _lines.pop(); // do not hog memory + } + return; + } + + while (!_lines.empty() && _ws->availableForWriteAll()) { + _ws->textAll(std::make_shared(std::move(_lines.front()))); + _lines.pop(); + } +} diff --git a/src/MqttBattery.cpp b/src/MqttBattery.cpp index 03e141e2f..544ff0322 100644 --- a/src/MqttBattery.cpp +++ b/src/MqttBattery.cpp @@ -4,6 +4,7 @@ #include "MqttBattery.h" #include "MqttSettings.h" #include "MessageOutput.h" +#include "Utils.h" bool MqttBattery::init(bool verboseLogging) { @@ -17,7 +18,8 @@ bool MqttBattery::init(bool verboseLogging) std::bind(&MqttBattery::onMqttMessageSoC, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, - std::placeholders::_5, std::placeholders::_6) + std::placeholders::_5, std::placeholders::_6, + config.Battery.MqttSocJsonPath) ); if (_verboseLogging) { @@ -32,7 +34,8 @@ bool MqttBattery::init(bool verboseLogging) std::bind(&MqttBattery::onMqttMessageVoltage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, - std::placeholders::_5, std::placeholders::_6) + std::placeholders::_5, std::placeholders::_6, + config.Battery.MqttVoltageJsonPath) ); if (_verboseLogging) { @@ -55,25 +58,14 @@ void MqttBattery::deinit() } } -std::optional MqttBattery::getFloat(std::string const& src, char const* topic) { - float res = 0; - - try { - res = std::stof(src); - } - catch(std::invalid_argument const& e) { - MessageOutput.printf("MqttBattery: Cannot parse payload '%s' in topic '%s' as float\r\n", - src.c_str(), topic); - return std::nullopt; - } - - return res; -} - void MqttBattery::onMqttMessageSoC(espMqttClientTypes::MessageProperties const& properties, - char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total) + char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total, + char const* jsonPath) { - auto soc = getFloat(std::string(reinterpret_cast(payload), len), topic); + auto soc = Utils::getNumericValueFromMqttPayload("MqttBattery", + std::string(reinterpret_cast(payload), len), topic, + jsonPath); + if (!soc.has_value()) { return; } if (*soc < 0 || *soc > 100) { @@ -91,11 +83,32 @@ void MqttBattery::onMqttMessageSoC(espMqttClientTypes::MessageProperties const& } void MqttBattery::onMqttMessageVoltage(espMqttClientTypes::MessageProperties const& properties, - char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total) + char const* topic, uint8_t const* payload, size_t len, size_t index, size_t total, + char const* jsonPath) { - auto voltage = getFloat(std::string(reinterpret_cast(payload), len), topic); + auto voltage = Utils::getNumericValueFromMqttPayload("MqttBattery", + std::string(reinterpret_cast(payload), len), topic, + jsonPath); + + if (!voltage.has_value()) { return; } + auto const& config = Configuration.get(); + using Unit_t = BatteryVoltageUnit; + switch (config.Battery.MqttVoltageUnit) { + case Unit_t::DeciVolts: + *voltage /= 10; + break; + case Unit_t::CentiVolts: + *voltage /= 100; + break; + case Unit_t::MilliVolts: + *voltage /= 1000; + break; + default: + break; + } + // since this project is revolving around Hoymiles microinverters, which can // only handle up to 65V of input voltage at best, it is safe to assume that // an even higher voltage is implausible. diff --git a/src/MqttHandleBatteryHass.cpp b/src/MqttHandleBatteryHass.cpp index c91b855af..9f24abe42 100644 --- a/src/MqttHandleBatteryHass.cpp +++ b/src/MqttHandleBatteryHass.cpp @@ -5,6 +5,7 @@ #include "MqttHandleBatteryHass.h" #include "Configuration.h" #include "MqttSettings.h" +#include "MqttHandleHass.h" #include "Utils.h" #include "__compiled_constants.h" @@ -125,6 +126,61 @@ void MqttHandleBatteryHassClass::loop() publishSensor("Midpoint Voltage", NULL, "midpointVoltage", "voltage", "measurement", "V"); publishSensor("Midpoint Deviation", NULL, "midpointDeviation", "battery", "measurement", "%"); break; + case 4: // Pytes Battery + publishSensor("Charge voltage (BMS)", NULL, "settings/chargeVoltage", "voltage", "measurement", "V"); + publishSensor("Charge current limit", NULL, "settings/chargeCurrentLimitation", "current", "measurement", "A"); + publishSensor("Discharge current limit", NULL, "settings/dischargeCurrentLimitation", "current", "measurement", "A"); + publishSensor("Discharge voltage limit", NULL, "settings/dischargeVoltageLimitation", "voltage", "measurement", "V"); + + publishSensor("Voltage", "mdi:battery-charging", "voltage", "voltage", "measurement", "V"); + publishSensor("Current", "mdi:current-dc", "current", "current", "measurement", "A"); + publishSensor("State of Health (SOH)", "mdi:heart-plus", "stateOfHealth", NULL, "measurement", "%"); + publishSensor("Temperature", "mdi:thermometer", "temperature", "temperature", "measurement", "°C"); + + publishSensor("Charged Energy", NULL, "chargedEnergy", "energy", "total_increasing", "kWh"); + publishSensor("Discharged Energy", NULL, "dischargedEnergy", "energy", "total_increasing", "kWh"); + + publishSensor("Total Capacity", NULL, "capacity"); + publishSensor("Available Capacity", NULL, "availableCapacity"); + + publishSensor("Cell Min Voltage", NULL, "CellMinMilliVolt", "voltage", "measurement", "mV"); + publishSensor("Cell Max Voltage", NULL, "CellMaxMilliVolt", "voltage", "measurement", "mV"); + publishSensor("Cell Voltage Diff", "mdi:battery-alert", "CellDiffMilliVolt", "voltage", "measurement", "mV"); + publishSensor("Cell Min Temperature", NULL, "CellMinTemperature", "temperature", "measurement", "°C"); + publishSensor("Cell Max Temperature", NULL, "CellMaxTemperature", "temperature", "measurement", "°C"); + + publishSensor("Cell Min Voltage Label", NULL, "CellMinVoltageName"); + publishSensor("Cell Max Voltage Label", NULL, "CellMaxVoltageName"); + publishSensor("Cell Min Temperature Label", NULL, "CellMinTemperatureName"); + publishSensor("Cell Max Temperature Label", NULL, "CellMaxTemperatureName"); + + publishSensor("Modules Online", "mdi:counter", "modulesOnline"); + publishSensor("Modules Offline", "mdi:counter", "modulesOffline"); + publishSensor("Modules Blocking Charge", "mdi:counter", "modulesBlockingCharge"); + publishSensor("Modules Blocking Discharge", "mdi:counter", "modulesBlockingDischarge"); + + publishBinarySensor("Alarm Discharge current", "mdi:alert", "alarm/overCurrentDischarge", "1", "0"); + publishBinarySensor("Alarm High charge current", "mdi:alert", "alarm/overCurrentCharge", "1", "0"); + publishBinarySensor("Alarm Voltage low", "mdi:alert", "alarm/underVoltage", "1", "0"); + publishBinarySensor("Alarm Voltage high", "mdi:alert", "alarm/overVoltage", "1", "0"); + publishBinarySensor("Alarm Temperature low", "mdi:thermometer-low", "alarm/underTemperature", "1", "0"); + publishBinarySensor("Alarm Temperature high", "mdi:thermometer-high", "alarm/overTemperature", "1", "0"); + publishBinarySensor("Alarm Temperature low (charge)", "mdi:thermometer-low", "alarm/underTemperatureCharge", "1", "0"); + publishBinarySensor("Alarm Temperature high (charge)", "mdi:thermometer-high", "alarm/overTemperatureCharge", "1", "0"); + publishBinarySensor("Alarm BMS internal", "mdi:alert", "alarm/bmsInternal", "1", "0"); + publishBinarySensor("Alarm Cell Imbalance", "mdi:alert-outline", "alarm/cellImbalance", "1", "0"); + + publishBinarySensor("Warning Discharge current", "mdi:alert-outline", "warning/highCurrentDischarge", "1", "0"); + publishBinarySensor("Warning High charge current", "mdi:alert-outline", "warning/highCurrentCharge", "1", "0"); + publishBinarySensor("Warning Voltage low", "mdi:alert-outline", "warning/lowVoltage", "1", "0"); + publishBinarySensor("Warning Voltage high", "mdi:alert-outline", "warning/highVoltage", "1", "0"); + publishBinarySensor("Warning Temperature low", "mdi:thermometer-low", "warning/lowTemperature", "1", "0"); + publishBinarySensor("Warning Temperature high", "mdi:thermometer-high", "warning/highTemperature", "1", "0"); + publishBinarySensor("Warning Temperature low (charge)", "mdi:thermometer-low", "warning/lowTemperatureCharge", "1", "0"); + publishBinarySensor("Warning Temperature high (charge)", "mdi:thermometer-high", "warning/highTemperatureCharge", "1", "0"); + publishBinarySensor("Warning BMS internal", "mdi:alert-outline", "warning/bmsInternal", "1", "0"); + publishBinarySensor("Warning Cell Imbalance", "mdi:alert-outline", "warning/cellImbalance", "1", "0"); + break; } _doPublish = false; @@ -239,10 +295,11 @@ void MqttHandleBatteryHassClass::createDeviceInfo(JsonObject& object) } object["ids"] = serial; - object["cu"] = String("http://") + NetworkSettings.localIP().toString(); + object["cu"] = MqttHandleHass.getDtuUrl(); object["mf"] = "OpenDTU"; object["mdl"] = Battery.getStats()->getManufacturer(); object["sw"] = __COMPILED_GIT_HASH__; + object["via_device"] = MqttHandleHass.getDtuUniqueId(); } void MqttHandleBatteryHassClass::publish(const String& subtopic, const String& payload) diff --git a/src/MqttHandleHass.cpp b/src/MqttHandleHass.cpp index 119571889..87f1ccb9b 100644 --- a/src/MqttHandleHass.cpp +++ b/src/MqttHandleHass.cpp @@ -73,8 +73,8 @@ void MqttHandleHassClass::publishConfig() publishInverterButton(inv, "Turn Inverter On", "mdi:power-plug", "config", "", "cmd/power", "1"); publishInverterButton(inv, "Restart Inverter", "", "config", "restart", "cmd/restart", "1"); - publishInverterNumber(inv, "Limit NonPersistent Relative", "mdi:speedometer", "config", "cmd/limit_nonpersistent_relative", "status/limit_relative", "%", 0, 100); - publishInverterNumber(inv, "Limit Persistent Relative", "mdi:speedometer", "config", "cmd/limit_persistent_relative", "status/limit_relative", "%", 0, 100); + publishInverterNumber(inv, "Limit NonPersistent Relative", "mdi:speedometer", "config", "cmd/limit_nonpersistent_relative", "status/limit_relative", "%", 0, 100, 0.1); + publishInverterNumber(inv, "Limit Persistent Relative", "mdi:speedometer", "config", "cmd/limit_persistent_relative", "status/limit_relative", "%", 0, 100, 0.1); publishInverterNumber(inv, "Limit NonPersistent Absolute", "mdi:speedometer", "config", "cmd/limit_nonpersistent_absolute", "status/limit_absolute", "W", 0, MAX_INVERTER_LIMIT); publishInverterNumber(inv, "Limit Persistent Absolute", "mdi:speedometer", "config", "cmd/limit_persistent_absolute", "status/limit_absolute", "W", 0, MAX_INVERTER_LIMIT); @@ -215,7 +215,7 @@ void MqttHandleHassClass::publishInverterButton(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, const int16_t max) + const int16_t min, const int16_t max, float step) { const String serial = inv->serialString(); @@ -243,6 +243,7 @@ void MqttHandleHassClass::publishInverterNumber( root["unit_of_meas"] = unitOfMeasure; root["min"] = min; root["max"] = max; + root["step"] = step; createInverterInfo(root, inv); diff --git a/src/MqttHandleHuawei.cpp b/src/MqttHandleHuawei.cpp index 1f0f7ddb7..2afab15dc 100644 --- a/src/MqttHandleHuawei.cpp +++ b/src/MqttHandleHuawei.cpp @@ -1,162 +1,162 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * Copyright (C) 2022 Thomas Basler and others - */ -#include "MqttHandleHuawei.h" -#include "MessageOutput.h" -#include "MqttSettings.h" -#include "Huawei_can.h" -// #include "Failsafe.h" -#include "WebApi_Huawei.h" -#include - -MqttHandleHuaweiClass MqttHandleHuawei; - -void MqttHandleHuaweiClass::init(Scheduler& scheduler) -{ - scheduler.addTask(_loopTask); - _loopTask.setCallback(std::bind(&MqttHandleHuaweiClass::loop, this)); - _loopTask.setIterations(TASK_FOREVER); - _loopTask.enable(); - - String const& prefix = MqttSettings.getPrefix(); - - auto subscribe = [&prefix, this](char const* subTopic, Topic t) { - String fullTopic(prefix + "huawei/cmd/" + subTopic); - MqttSettings.subscribe(fullTopic.c_str(), 0, - std::bind(&MqttHandleHuaweiClass::onMqttMessage, this, t, - std::placeholders::_1, std::placeholders::_2, - std::placeholders::_3, std::placeholders::_4, - std::placeholders::_5, std::placeholders::_6)); - }; - - subscribe("limit_online_voltage", Topic::LimitOnlineVoltage); - subscribe("limit_online_current", Topic::LimitOnlineCurrent); - subscribe("limit_offline_voltage", Topic::LimitOfflineVoltage); - subscribe("limit_offline_current", Topic::LimitOfflineCurrent); - subscribe("mode", Topic::Mode); - - _lastPublish = millis(); - -} - - -void MqttHandleHuaweiClass::loop() -{ - const CONFIG_T& config = Configuration.get(); - - std::unique_lock mqttLock(_mqttMutex); - - if (!config.Huawei.Enabled) { - _mqttCallbacks.clear(); - return; - } - - for (auto& callback : _mqttCallbacks) { callback(); } - _mqttCallbacks.clear(); - - mqttLock.unlock(); - - if (!MqttSettings.getConnected() ) { - return; - } - - const RectifierParameters_t *rp = HuaweiCan.get(); - - if ((millis() - _lastPublish) > (config.Mqtt.PublishInterval * 1000) ) { - MqttSettings.publish("huawei/data_age", String((millis() - HuaweiCan.getLastUpdate()) / 1000)); - MqttSettings.publish("huawei/input_voltage", String(rp->input_voltage)); - MqttSettings.publish("huawei/input_current", String(rp->input_current)); - MqttSettings.publish("huawei/input_power", String(rp->input_power)); - MqttSettings.publish("huawei/output_voltage", String(rp->output_voltage)); - MqttSettings.publish("huawei/output_current", String(rp->output_current)); - MqttSettings.publish("huawei/max_output_current", String(rp->max_output_current)); - MqttSettings.publish("huawei/output_power", String(rp->output_power)); - 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(); - _lastPublish = millis(); - } -} - - -void MqttHandleHuaweiClass::onMqttMessage(Topic t, - const espMqttClientTypes::MessageProperties& properties, - const char* topic, const uint8_t* payload, size_t len, - size_t index, size_t total) -{ - std::string strValue(reinterpret_cast(payload), len); - float payload_val = -1; - try { - payload_val = std::stof(strValue); - } - catch (std::invalid_argument const& e) { - MessageOutput.printf("Huawei MQTT handler: cannot parse payload of topic '%s' as float: %s\r\n", - topic, strValue.c_str()); - return; - } - - std::lock_guard mqttLock(_mqttMutex); - - switch (t) { - case Topic::LimitOnlineVoltage: - MessageOutput.printf("Limit Voltage: %f V\r\n", payload_val); - _mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setValue, - &HuaweiCan, payload_val, HUAWEI_ONLINE_VOLTAGE)); - break; - - case Topic::LimitOfflineVoltage: - MessageOutput.printf("Offline Limit Voltage: %f V\r\n", payload_val); - _mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setValue, - &HuaweiCan, payload_val, HUAWEI_OFFLINE_VOLTAGE)); - break; - - case Topic::LimitOnlineCurrent: - MessageOutput.printf("Limit Current: %f A\r\n", payload_val); - _mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setValue, - &HuaweiCan, payload_val, HUAWEI_ONLINE_CURRENT)); - break; - - case Topic::LimitOfflineCurrent: - MessageOutput.printf("Offline Limit Current: %f A\r\n", payload_val); - _mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setValue, - &HuaweiCan, payload_val, HUAWEI_OFFLINE_CURRENT)); - break; - - case Topic::Mode: - switch (static_cast(payload_val)) { - case 3: - MessageOutput.println("[Huawei MQTT::] Received MQTT msg. New mode: Full internal control"); - _mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setMode, - &HuaweiCan, HUAWEI_MODE_AUTO_INT)); - break; - - case 2: - MessageOutput.println("[Huawei MQTT::] Received MQTT msg. New mode: Internal on/off control, external power limit"); - _mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setMode, - &HuaweiCan, HUAWEI_MODE_AUTO_EXT)); - break; - - case 1: - MessageOutput.println("[Huawei MQTT::] Received MQTT msg. New mode: Turned ON"); - _mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setMode, - &HuaweiCan, HUAWEI_MODE_ON)); - break; - - case 0: - MessageOutput.println("[Huawei MQTT::] Received MQTT msg. New mode: Turned OFF"); - _mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setMode, - &HuaweiCan, HUAWEI_MODE_OFF)); - break; - - default: - MessageOutput.printf("[Huawei MQTT::] Invalid mode %.0f\r\n", payload_val); - break; - } - break; - } -} +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler and others + */ +#include "MqttHandleHuawei.h" +#include "MessageOutput.h" +#include "MqttSettings.h" +#include "Huawei_can.h" +// #include "Failsafe.h" +#include "WebApi_Huawei.h" +#include + +MqttHandleHuaweiClass MqttHandleHuawei; + +void MqttHandleHuaweiClass::init(Scheduler& scheduler) +{ + scheduler.addTask(_loopTask); + _loopTask.setCallback(std::bind(&MqttHandleHuaweiClass::loop, this)); + _loopTask.setIterations(TASK_FOREVER); + _loopTask.enable(); + + String const& prefix = MqttSettings.getPrefix(); + + auto subscribe = [&prefix, this](char const* subTopic, Topic t) { + String fullTopic(prefix + "huawei/cmd/" + subTopic); + MqttSettings.subscribe(fullTopic.c_str(), 0, + std::bind(&MqttHandleHuaweiClass::onMqttMessage, this, t, + std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4, + std::placeholders::_5, std::placeholders::_6)); + }; + + subscribe("limit_online_voltage", Topic::LimitOnlineVoltage); + subscribe("limit_online_current", Topic::LimitOnlineCurrent); + subscribe("limit_offline_voltage", Topic::LimitOfflineVoltage); + subscribe("limit_offline_current", Topic::LimitOfflineCurrent); + subscribe("mode", Topic::Mode); + + _lastPublish = millis(); + +} + + +void MqttHandleHuaweiClass::loop() +{ + const CONFIG_T& config = Configuration.get(); + + std::unique_lock mqttLock(_mqttMutex); + + if (!config.Huawei.Enabled) { + _mqttCallbacks.clear(); + return; + } + + for (auto& callback : _mqttCallbacks) { callback(); } + _mqttCallbacks.clear(); + + mqttLock.unlock(); + + if (!MqttSettings.getConnected() ) { + return; + } + + const RectifierParameters_t *rp = HuaweiCan.get(); + + if ((millis() - _lastPublish) > (config.Mqtt.PublishInterval * 1000) ) { + MqttSettings.publish("huawei/data_age", String((millis() - HuaweiCan.getLastUpdate()) / 1000)); + MqttSettings.publish("huawei/input_voltage", String(rp->input_voltage)); + MqttSettings.publish("huawei/input_current", String(rp->input_current)); + MqttSettings.publish("huawei/input_power", String(rp->input_power)); + MqttSettings.publish("huawei/output_voltage", String(rp->output_voltage)); + MqttSettings.publish("huawei/output_current", String(rp->output_current)); + MqttSettings.publish("huawei/max_output_current", String(rp->max_output_current)); + MqttSettings.publish("huawei/output_power", String(rp->output_power)); + 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(); + _lastPublish = millis(); + } +} + + +void MqttHandleHuaweiClass::onMqttMessage(Topic t, + const espMqttClientTypes::MessageProperties& properties, + const char* topic, const uint8_t* payload, size_t len, + size_t index, size_t total) +{ + std::string strValue(reinterpret_cast(payload), len); + float payload_val = -1; + try { + payload_val = std::stof(strValue); + } + catch (std::invalid_argument const& e) { + MessageOutput.printf("Huawei MQTT handler: cannot parse payload of topic '%s' as float: %s\r\n", + topic, strValue.c_str()); + return; + } + + std::lock_guard mqttLock(_mqttMutex); + + switch (t) { + case Topic::LimitOnlineVoltage: + MessageOutput.printf("Limit Voltage: %f V\r\n", payload_val); + _mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setValue, + &HuaweiCan, payload_val, HUAWEI_ONLINE_VOLTAGE)); + break; + + case Topic::LimitOfflineVoltage: + MessageOutput.printf("Offline Limit Voltage: %f V\r\n", payload_val); + _mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setValue, + &HuaweiCan, payload_val, HUAWEI_OFFLINE_VOLTAGE)); + break; + + case Topic::LimitOnlineCurrent: + MessageOutput.printf("Limit Current: %f A\r\n", payload_val); + _mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setValue, + &HuaweiCan, payload_val, HUAWEI_ONLINE_CURRENT)); + break; + + case Topic::LimitOfflineCurrent: + MessageOutput.printf("Offline Limit Current: %f A\r\n", payload_val); + _mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setValue, + &HuaweiCan, payload_val, HUAWEI_OFFLINE_CURRENT)); + break; + + case Topic::Mode: + switch (static_cast(payload_val)) { + case 3: + MessageOutput.println("[Huawei MQTT::] Received MQTT msg. New mode: Full internal control"); + _mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setMode, + &HuaweiCan, HUAWEI_MODE_AUTO_INT)); + break; + + case 2: + MessageOutput.println("[Huawei MQTT::] Received MQTT msg. New mode: Internal on/off control, external power limit"); + _mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setMode, + &HuaweiCan, HUAWEI_MODE_AUTO_EXT)); + break; + + case 1: + MessageOutput.println("[Huawei MQTT::] Received MQTT msg. New mode: Turned ON"); + _mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setMode, + &HuaweiCan, HUAWEI_MODE_ON)); + break; + + case 0: + MessageOutput.println("[Huawei MQTT::] Received MQTT msg. New mode: Turned OFF"); + _mqttCallbacks.push_back(std::bind(&HuaweiCanClass::setMode, + &HuaweiCan, HUAWEI_MODE_OFF)); + break; + + default: + MessageOutput.printf("[Huawei MQTT::] Invalid mode %.0f\r\n", payload_val); + break; + } + break; + } +} diff --git a/src/MqttHandleInverter.cpp b/src/MqttHandleInverter.cpp index de2778d10..d099b4443 100644 --- a/src/MqttHandleInverter.cpp +++ b/src/MqttHandleInverter.cpp @@ -25,20 +25,7 @@ MqttHandleInverterClass::MqttHandleInverterClass() void MqttHandleInverterClass::init(Scheduler& scheduler) { - using std::placeholders::_1; - using std::placeholders::_2; - using std::placeholders::_3; - using std::placeholders::_4; - using std::placeholders::_5; - using std::placeholders::_6; - - const String topic = MqttSettings.getPrefix(); - MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_PERSISTENT_RELATIVE), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); - MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_PERSISTENT_ABSOLUTE), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); - MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); - MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); - MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_POWER), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); - MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_RESTART), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); + subscribeTopics(); scheduler.addTask(_loopTask); _loopTask.setInterval(Configuration.get().Mqtt.PublishInterval * TASK_SECOND); @@ -196,7 +183,7 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro char* strlimit = new char[len + 1]; memcpy(strlimit, payload, len); strlimit[len] = '\0'; - const int32_t payload_val = strtol(strlimit, NULL, 10); + const float payload_val = strtof(strlimit, NULL); delete[] strlimit; if (payload_val < 0) { @@ -206,17 +193,17 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro if (!strcmp(setting, TOPIC_SUB_LIMIT_PERSISTENT_RELATIVE)) { // Set inverter limit relative persistent - MessageOutput.printf("Limit Persistent: %d %%\r\n", payload_val); + MessageOutput.printf("Limit Persistent: %.1f %%\r\n", payload_val); inv->sendActivePowerControlRequest(payload_val, PowerLimitControlType::RelativPersistent); } else if (!strcmp(setting, TOPIC_SUB_LIMIT_PERSISTENT_ABSOLUTE)) { // Set inverter limit absolute persistent - MessageOutput.printf("Limit Persistent: %d W\r\n", payload_val); + MessageOutput.printf("Limit Persistent: %.1f W\r\n", payload_val); inv->sendActivePowerControlRequest(payload_val, PowerLimitControlType::AbsolutPersistent); } else if (!strcmp(setting, TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE)) { // Set inverter limit relative non persistent - MessageOutput.printf("Limit Non-Persistent: %d %%\r\n", payload_val); + MessageOutput.printf("Limit Non-Persistent: %.1f %%\r\n", payload_val); if (!properties.retain) { inv->sendActivePowerControlRequest(payload_val, PowerLimitControlType::RelativNonPersistent); } else { @@ -225,7 +212,7 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro } else if (!strcmp(setting, TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE)) { // Set inverter limit absolute non persistent - MessageOutput.printf("Limit Non-Persistent: %d W\r\n", payload_val); + MessageOutput.printf("Limit Non-Persistent: %.1f W\r\n", payload_val); if (!properties.retain) { inv->sendActivePowerControlRequest(payload_val, PowerLimitControlType::AbsolutNonPersistent); } else { @@ -234,8 +221,8 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro } else if (!strcmp(setting, TOPIC_SUB_POWER)) { // Turn inverter on or off - MessageOutput.printf("Set inverter power to: %d\r\n", payload_val); - inv->sendPowerControlRequest(payload_val > 0); + MessageOutput.printf("Set inverter power to: %d\r\n", static_cast(payload_val)); + inv->sendPowerControlRequest(static_cast(payload_val) > 0); } else if (!strcmp(setting, TOPIC_SUB_RESTART)) { // Restart inverter @@ -243,7 +230,36 @@ void MqttHandleInverterClass::onMqttMessage(const espMqttClientTypes::MessagePro if (!properties.retain && payload_val == 1) { inv->sendRestartControlRequest(); } else { - MessageOutput.println("Ignored because retained"); + MessageOutput.println("Ignored because retained or numeric value not '1'"); } } } + +void MqttHandleInverterClass::subscribeTopics() +{ + using std::placeholders::_1; + using std::placeholders::_2; + using std::placeholders::_3; + using std::placeholders::_4; + using std::placeholders::_5; + using std::placeholders::_6; + + const String topic = MqttSettings.getPrefix(); + MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_PERSISTENT_RELATIVE), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); + MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_PERSISTENT_ABSOLUTE), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); + MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); + MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); + MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_POWER), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); + MqttSettings.subscribe(String(topic + "+/cmd/" + TOPIC_SUB_RESTART), 0, std::bind(&MqttHandleInverterClass::onMqttMessage, this, _1, _2, _3, _4, _5, _6)); +} + +void MqttHandleInverterClass::unsubscribeTopics() +{ + const String topic = MqttSettings.getPrefix(); + MqttSettings.unsubscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_PERSISTENT_RELATIVE)); + MqttSettings.unsubscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_PERSISTENT_ABSOLUTE)); + MqttSettings.unsubscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_RELATIVE)); + MqttSettings.unsubscribe(String(topic + "+/cmd/" + TOPIC_SUB_LIMIT_NONPERSISTENT_ABSOLUTE)); + MqttSettings.unsubscribe(String(topic + "+/cmd/" + TOPIC_SUB_POWER)); + MqttSettings.unsubscribe(String(topic + "+/cmd/" + TOPIC_SUB_RESTART)); +} diff --git a/src/MqttHandlePowerLimiter.cpp b/src/MqttHandlePowerLimiter.cpp index 95f90db2f..f01a9b146 100644 --- a/src/MqttHandlePowerLimiter.cpp +++ b/src/MqttHandlePowerLimiter.cpp @@ -1,181 +1,198 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * Copyright (C) 2022 Thomas Basler, Malte Schmidt and others - */ -#include "MessageOutput.h" -#include "MqttSettings.h" -#include "MqttHandlePowerLimiter.h" -#include "PowerLimiter.h" -#include -#include - -MqttHandlePowerLimiterClass MqttHandlePowerLimiter; - -void MqttHandlePowerLimiterClass::init(Scheduler& scheduler) -{ - scheduler.addTask(_loopTask); - _loopTask.setCallback(std::bind(&MqttHandlePowerLimiterClass::loop, this)); - _loopTask.setIterations(TASK_FOREVER); - _loopTask.enable(); - - using std::placeholders::_1; - using std::placeholders::_2; - using std::placeholders::_3; - using std::placeholders::_4; - using std::placeholders::_5; - using std::placeholders::_6; - - String const& prefix = MqttSettings.getPrefix(); - - auto subscribe = [&prefix, this](char const* subTopic, MqttPowerLimiterCommand command) { - String fullTopic(prefix + "powerlimiter/cmd/" + subTopic); - MqttSettings.subscribe(fullTopic.c_str(), 0, - std::bind(&MqttHandlePowerLimiterClass::onMqttCmd, this, command, - std::placeholders::_1, std::placeholders::_2, - std::placeholders::_3, std::placeholders::_4, - std::placeholders::_5, std::placeholders::_6)); - }; - - subscribe("threshold/soc/start", MqttPowerLimiterCommand::BatterySoCStartThreshold); - subscribe("threshold/soc/stop", MqttPowerLimiterCommand::BatterySoCStopThreshold); - subscribe("threshold/soc/full_solar_passthrough", MqttPowerLimiterCommand::FullSolarPassthroughSoC); - subscribe("threshold/voltage/start", MqttPowerLimiterCommand::VoltageStartThreshold); - subscribe("threshold/voltage/stop", MqttPowerLimiterCommand::VoltageStopThreshold); - subscribe("threshold/voltage/full_solar_passthrough_start", MqttPowerLimiterCommand::FullSolarPassThroughStartVoltage); - subscribe("threshold/voltage/full_solar_passthrough_stop", MqttPowerLimiterCommand::FullSolarPassThroughStopVoltage); - subscribe("mode", MqttPowerLimiterCommand::Mode); - - _lastPublish = millis(); -} - - -void MqttHandlePowerLimiterClass::loop() -{ - std::unique_lock mqttLock(_mqttMutex); - - const CONFIG_T& config = Configuration.get(); - - if (!config.PowerLimiter.Enabled) { - _mqttCallbacks.clear(); - return; - } - - for (auto& callback : _mqttCallbacks) { callback(); } - _mqttCallbacks.clear(); - - mqttLock.unlock(); - - if (!MqttSettings.getConnected() ) { return; } - - if ((millis() - _lastPublish) < (config.Mqtt.PublishInterval * 1000)) { - return; - } - - _lastPublish = millis(); - - 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; } - - MqttSettings.publish("powerlimiter/status/threshold/voltage/start", String(config.PowerLimiter.VoltageStartThreshold)); - MqttSettings.publish("powerlimiter/status/threshold/voltage/stop", String(config.PowerLimiter.VoltageStopThreshold)); - - if (config.Vedirect.Enabled) { - MqttSettings.publish("powerlimiter/status/threshold/voltage/full_solar_passthrough_start", String(config.PowerLimiter.FullSolarPassThroughStartVoltage)); - MqttSettings.publish("powerlimiter/status/threshold/voltage/full_solar_passthrough_stop", String(config.PowerLimiter.FullSolarPassThroughStopVoltage)); - } - - if (!config.Battery.Enabled || config.PowerLimiter.IgnoreSoc) { return; } - - MqttSettings.publish("powerlimiter/status/threshold/soc/start", String(config.PowerLimiter.BatterySocStartThreshold)); - MqttSettings.publish("powerlimiter/status/threshold/soc/stop", String(config.PowerLimiter.BatterySocStopThreshold)); - - if (config.Vedirect.Enabled) { - MqttSettings.publish("powerlimiter/status/threshold/soc/full_solar_passthrough", String(config.PowerLimiter.FullSolarPassThroughSoc)); - } -} - -void MqttHandlePowerLimiterClass::onMqttCmd(MqttPowerLimiterCommand command, const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) -{ - CONFIG_T& config = Configuration.get(); - - std::string strValue(reinterpret_cast(payload), len); - float payload_val = -1; - try { - payload_val = std::stof(strValue); - } - catch (std::invalid_argument const& e) { - MessageOutput.printf("PowerLimiter MQTT handler: cannot parse payload of topic '%s' as float: %s\r\n", - topic, strValue.c_str()); - return; - } - const int intValue = static_cast(payload_val); - - std::lock_guard mqttLock(_mqttMutex); - - switch (command) { - case MqttPowerLimiterCommand::Mode: - { - using Mode = PowerLimiterClass::Mode; - Mode mode = static_cast(intValue); - if (mode == Mode::UnconditionalFullSolarPassthrough) { - MessageOutput.println("Power limiter unconditional full solar PT"); - _mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode, - &PowerLimiter, Mode::UnconditionalFullSolarPassthrough)); - } else if (mode == Mode::Disabled) { - MessageOutput.println("Power limiter disabled (override)"); - _mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode, - &PowerLimiter, Mode::Disabled)); - } else if (mode == Mode::Normal) { - MessageOutput.println("Power limiter normal operation"); - _mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode, - &PowerLimiter, Mode::Normal)); - } else { - MessageOutput.printf("PowerLimiter - unknown mode %d\r\n", intValue); - } - return; - } - case MqttPowerLimiterCommand::BatterySoCStartThreshold: - if (config.PowerLimiter.BatterySocStartThreshold == intValue) { return; } - MessageOutput.printf("Setting battery SoC start threshold to: %d %%\r\n", intValue); - config.PowerLimiter.BatterySocStartThreshold = intValue; - break; - case MqttPowerLimiterCommand::BatterySoCStopThreshold: - if (config.PowerLimiter.BatterySocStopThreshold == intValue) { return; } - MessageOutput.printf("Setting battery SoC stop threshold to: %d %%\r\n", intValue); - config.PowerLimiter.BatterySocStopThreshold = intValue; - break; - case MqttPowerLimiterCommand::FullSolarPassthroughSoC: - if (config.PowerLimiter.FullSolarPassThroughSoc == intValue) { return; } - MessageOutput.printf("Setting full solar passthrough SoC to: %d %%\r\n", intValue); - config.PowerLimiter.FullSolarPassThroughSoc = intValue; - break; - case MqttPowerLimiterCommand::VoltageStartThreshold: - if (config.PowerLimiter.VoltageStartThreshold == payload_val) { return; } - MessageOutput.printf("Setting voltage start threshold to: %.2f V\r\n", payload_val); - config.PowerLimiter.VoltageStartThreshold = payload_val; - break; - case MqttPowerLimiterCommand::VoltageStopThreshold: - if (config.PowerLimiter.VoltageStopThreshold == payload_val) { return; } - MessageOutput.printf("Setting voltage stop threshold to: %.2f V\r\n", payload_val); - config.PowerLimiter.VoltageStopThreshold = payload_val; - break; - case MqttPowerLimiterCommand::FullSolarPassThroughStartVoltage: - if (config.PowerLimiter.FullSolarPassThroughStartVoltage == payload_val) { return; } - MessageOutput.printf("Setting full solar passthrough start voltage to: %.2f V\r\n", payload_val); - config.PowerLimiter.FullSolarPassThroughStartVoltage = payload_val; - break; - case MqttPowerLimiterCommand::FullSolarPassThroughStopVoltage: - if (config.PowerLimiter.FullSolarPassThroughStopVoltage == payload_val) { return; } - MessageOutput.printf("Setting full solar passthrough stop voltage to: %.2f V\r\n", payload_val); - config.PowerLimiter.FullSolarPassThroughStopVoltage = payload_val; - break; - } - - // not reached if the value did not change - Configuration.write(); -} +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022 Thomas Basler, Malte Schmidt and others + */ +#include "MessageOutput.h" +#include "MqttSettings.h" +#include "MqttHandlePowerLimiter.h" +#include "PowerLimiter.h" +#include +#include + +MqttHandlePowerLimiterClass MqttHandlePowerLimiter; + +void MqttHandlePowerLimiterClass::init(Scheduler& scheduler) +{ + scheduler.addTask(_loopTask); + _loopTask.setCallback(std::bind(&MqttHandlePowerLimiterClass::loop, this)); + _loopTask.setIterations(TASK_FOREVER); + _loopTask.enable(); + + using std::placeholders::_1; + using std::placeholders::_2; + using std::placeholders::_3; + using std::placeholders::_4; + using std::placeholders::_5; + using std::placeholders::_6; + + String const& prefix = MqttSettings.getPrefix(); + + auto subscribe = [&prefix, this](char const* subTopic, MqttPowerLimiterCommand command) { + String fullTopic(prefix + "powerlimiter/cmd/" + subTopic); + MqttSettings.subscribe(fullTopic.c_str(), 0, + std::bind(&MqttHandlePowerLimiterClass::onMqttCmd, this, command, + std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4, + std::placeholders::_5, std::placeholders::_6)); + }; + + subscribe("threshold/soc/start", MqttPowerLimiterCommand::BatterySoCStartThreshold); + subscribe("threshold/soc/stop", MqttPowerLimiterCommand::BatterySoCStopThreshold); + subscribe("threshold/soc/full_solar_passthrough", MqttPowerLimiterCommand::FullSolarPassthroughSoC); + subscribe("threshold/voltage/start", MqttPowerLimiterCommand::VoltageStartThreshold); + subscribe("threshold/voltage/stop", MqttPowerLimiterCommand::VoltageStopThreshold); + subscribe("threshold/voltage/full_solar_passthrough_start", MqttPowerLimiterCommand::FullSolarPassThroughStartVoltage); + subscribe("threshold/voltage/full_solar_passthrough_stop", MqttPowerLimiterCommand::FullSolarPassThroughStopVoltage); + subscribe("mode", MqttPowerLimiterCommand::Mode); + subscribe("upper_power_limit", MqttPowerLimiterCommand::UpperPowerLimit); + subscribe("target_power_consumption", MqttPowerLimiterCommand::TargetPowerConsumption); + + _lastPublish = millis(); +} + + +void MqttHandlePowerLimiterClass::loop() +{ + std::unique_lock mqttLock(_mqttMutex); + + const CONFIG_T& config = Configuration.get(); + + if (!config.PowerLimiter.Enabled) { + _mqttCallbacks.clear(); + return; + } + + for (auto& callback : _mqttCallbacks) { callback(); } + _mqttCallbacks.clear(); + + mqttLock.unlock(); + + if (!MqttSettings.getConnected() ) { return; } + + if ((millis() - _lastPublish) < (config.Mqtt.PublishInterval * 1000)) { + return; + } + + _lastPublish = millis(); + + auto val = static_cast(PowerLimiter.getMode()); + MqttSettings.publish("powerlimiter/status/mode", String(val)); + + MqttSettings.publish("powerlimiter/status/upper_power_limit", String(config.PowerLimiter.UpperPowerLimit)); + + MqttSettings.publish("powerlimiter/status/target_power_consumption", String(config.PowerLimiter.TargetPowerConsumption)); + + MqttSettings.publish("powerlimiter/status/inverter_update_timeouts", String(PowerLimiter.getInverterUpdateTimeouts())); + + // no thresholds are relevant for setups without a battery + if (config.PowerLimiter.IsInverterSolarPowered) { return; } + + MqttSettings.publish("powerlimiter/status/threshold/voltage/start", String(config.PowerLimiter.VoltageStartThreshold)); + MqttSettings.publish("powerlimiter/status/threshold/voltage/stop", String(config.PowerLimiter.VoltageStopThreshold)); + + if (config.Vedirect.Enabled) { + MqttSettings.publish("powerlimiter/status/full_solar_passthrough_active", String(PowerLimiter.getFullSolarPassThroughEnabled())); + MqttSettings.publish("powerlimiter/status/threshold/voltage/full_solar_passthrough_start", String(config.PowerLimiter.FullSolarPassThroughStartVoltage)); + MqttSettings.publish("powerlimiter/status/threshold/voltage/full_solar_passthrough_stop", String(config.PowerLimiter.FullSolarPassThroughStopVoltage)); + } + + if (!config.Battery.Enabled || config.PowerLimiter.IgnoreSoc) { return; } + + MqttSettings.publish("powerlimiter/status/threshold/soc/start", String(config.PowerLimiter.BatterySocStartThreshold)); + MqttSettings.publish("powerlimiter/status/threshold/soc/stop", String(config.PowerLimiter.BatterySocStopThreshold)); + + if (config.Vedirect.Enabled) { + MqttSettings.publish("powerlimiter/status/threshold/soc/full_solar_passthrough", String(config.PowerLimiter.FullSolarPassThroughSoc)); + } +} + +void MqttHandlePowerLimiterClass::onMqttCmd(MqttPowerLimiterCommand command, const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) +{ + CONFIG_T& config = Configuration.get(); + + std::string strValue(reinterpret_cast(payload), len); + float payload_val = -1; + try { + payload_val = std::stof(strValue); + } + catch (std::invalid_argument const& e) { + MessageOutput.printf("PowerLimiter MQTT handler: cannot parse payload of topic '%s' as float: %s\r\n", + topic, strValue.c_str()); + return; + } + const int intValue = static_cast(payload_val); + + std::lock_guard mqttLock(_mqttMutex); + + switch (command) { + case MqttPowerLimiterCommand::Mode: + { + using Mode = PowerLimiterClass::Mode; + Mode mode = static_cast(intValue); + if (mode == Mode::UnconditionalFullSolarPassthrough) { + MessageOutput.println("Power limiter unconditional full solar PT"); + _mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode, + &PowerLimiter, Mode::UnconditionalFullSolarPassthrough)); + } else if (mode == Mode::Disabled) { + MessageOutput.println("Power limiter disabled (override)"); + _mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode, + &PowerLimiter, Mode::Disabled)); + } else if (mode == Mode::Normal) { + MessageOutput.println("Power limiter normal operation"); + _mqttCallbacks.push_back(std::bind(&PowerLimiterClass::setMode, + &PowerLimiter, Mode::Normal)); + } else { + MessageOutput.printf("PowerLimiter - unknown mode %d\r\n", intValue); + } + return; + } + case MqttPowerLimiterCommand::BatterySoCStartThreshold: + if (config.PowerLimiter.BatterySocStartThreshold == intValue) { return; } + MessageOutput.printf("Setting battery SoC start threshold to: %d %%\r\n", intValue); + config.PowerLimiter.BatterySocStartThreshold = intValue; + break; + case MqttPowerLimiterCommand::BatterySoCStopThreshold: + if (config.PowerLimiter.BatterySocStopThreshold == intValue) { return; } + MessageOutput.printf("Setting battery SoC stop threshold to: %d %%\r\n", intValue); + config.PowerLimiter.BatterySocStopThreshold = intValue; + break; + case MqttPowerLimiterCommand::FullSolarPassthroughSoC: + if (config.PowerLimiter.FullSolarPassThroughSoc == intValue) { return; } + MessageOutput.printf("Setting full solar passthrough SoC to: %d %%\r\n", intValue); + config.PowerLimiter.FullSolarPassThroughSoc = intValue; + break; + case MqttPowerLimiterCommand::VoltageStartThreshold: + if (config.PowerLimiter.VoltageStartThreshold == payload_val) { return; } + MessageOutput.printf("Setting voltage start threshold to: %.2f V\r\n", payload_val); + config.PowerLimiter.VoltageStartThreshold = payload_val; + break; + case MqttPowerLimiterCommand::VoltageStopThreshold: + if (config.PowerLimiter.VoltageStopThreshold == payload_val) { return; } + MessageOutput.printf("Setting voltage stop threshold to: %.2f V\r\n", payload_val); + config.PowerLimiter.VoltageStopThreshold = payload_val; + break; + case MqttPowerLimiterCommand::FullSolarPassThroughStartVoltage: + if (config.PowerLimiter.FullSolarPassThroughStartVoltage == payload_val) { return; } + MessageOutput.printf("Setting full solar passthrough start voltage to: %.2f V\r\n", payload_val); + config.PowerLimiter.FullSolarPassThroughStartVoltage = payload_val; + break; + case MqttPowerLimiterCommand::FullSolarPassThroughStopVoltage: + if (config.PowerLimiter.FullSolarPassThroughStopVoltage == payload_val) { return; } + MessageOutput.printf("Setting full solar passthrough stop voltage to: %.2f V\r\n", payload_val); + config.PowerLimiter.FullSolarPassThroughStopVoltage = payload_val; + break; + case MqttPowerLimiterCommand::UpperPowerLimit: + if (config.PowerLimiter.UpperPowerLimit == intValue) { return; } + MessageOutput.printf("Setting upper power limit to: %d W\r\n", intValue); + config.PowerLimiter.UpperPowerLimit = intValue; + break; + case MqttPowerLimiterCommand::TargetPowerConsumption: + if (config.PowerLimiter.TargetPowerConsumption == intValue) { return; } + MessageOutput.printf("Setting target power consumption to: %d W\r\n", intValue); + config.PowerLimiter.TargetPowerConsumption = intValue; + break; + } + + // not reached if the value did not change + Configuration.write(); +} diff --git a/src/MqttHandlePowerLimiterHass.cpp b/src/MqttHandlePowerLimiterHass.cpp index dfc773226..e5706cd85 100644 --- a/src/MqttHandlePowerLimiterHass.cpp +++ b/src/MqttHandlePowerLimiterHass.cpp @@ -3,6 +3,7 @@ * Copyright (C) 2022 Thomas Basler and others */ #include "MqttHandlePowerLimiterHass.h" +#include "MqttHandleHass.h" #include "Configuration.h" #include "MqttSettings.h" #include "NetworkSettings.h" @@ -69,32 +70,36 @@ void MqttHandlePowerLimiterHassClass::publishConfig() // as this project revolves around Hoymiles inverters, 16 - 60 V is a reasonable voltage range publishNumber("DPL battery voltage start threshold", "mdi:battery-charging", - "config", "threshold/voltage/start", "threshold/voltage/start", "V", 16, 60); + "config", "threshold/voltage/start", "threshold/voltage/start", "V", 16, 60, 0.1); publishNumber("DPL battery voltage stop threshold", "mdi:battery-charging", - "config", "threshold/voltage/stop", "threshold/voltage/stop", "V", 16, 60); + "config", "threshold/voltage/stop", "threshold/voltage/stop", "V", 16, 60, 0.1); if (config.Vedirect.Enabled) { + publishBinarySensor("full solar passthrough active", + "mdi:transmission-tower-import", + "full_solar_passthrough_active", "1", "0"); + publishNumber("DPL full solar passthrough start voltage", "mdi:transmission-tower-import", "config", "threshold/voltage/full_solar_passthrough_start", - "threshold/voltage/full_solar_passthrough_start", "V", 16, 60); + "threshold/voltage/full_solar_passthrough_start", "V", 16, 60, 0.1); publishNumber("DPL full solar passthrough stop voltage", "mdi:transmission-tower-import", "config", "threshold/voltage/full_solar_passthrough_stop", - "threshold/voltage/full_solar_passthrough_stop", "V", 16, 60); + "threshold/voltage/full_solar_passthrough_stop", "V", 16, 60, 0.1); } if (config.Battery.Enabled && !config.PowerLimiter.IgnoreSoc) { publishNumber("DPL battery SoC start threshold", "mdi:battery-charging", - "config", "threshold/soc/start", "threshold/soc/start", "%", 0, 100); + "config", "threshold/soc/start", "threshold/soc/start", "%", 0, 100, 1.0); publishNumber("DPL battery SoC stop threshold", "mdi:battery-charging", - "config", "threshold/soc/stop", "threshold/soc/stop", "%", 0, 100); + "config", "threshold/soc/stop", "threshold/soc/stop", "%", 0, 100, 1.0); if (config.Vedirect.Enabled) { publishNumber("DPL full solar passthrough SoC", "mdi:transmission-tower-import", "config", "threshold/soc/full_solar_passthrough", - "threshold/soc/full_solar_passthrough", "%", 0, 100); + "threshold/soc/full_solar_passthrough", "%", 0, 100, 1.0); } } } @@ -108,7 +113,7 @@ void MqttHandlePowerLimiterHassClass::publishSelect( selectId.replace(" ", "_"); selectId.toLowerCase(); - const String configTopic = "select/powerlimiter/" + selectId + "/config"; + const String configTopic = "select/" + MqttHandleHass.getDtuUniqueId() + "/" + selectId + "/config"; const String cmdTopic = MqttSettings.getPrefix() + "powerlimiter/cmd/" + commandTopic; const String statTopic = MqttSettings.getPrefix() + "powerlimiter/status/" + stateTopic; @@ -116,7 +121,7 @@ void MqttHandlePowerLimiterHassClass::publishSelect( JsonDocument root; root["name"] = caption; - root["uniq_id"] = selectId; + root["uniq_id"] = MqttHandleHass.getDtuUniqueId() + "_" + selectId; if (strcmp(icon, "")) { root["ic"] = icon; } @@ -128,8 +133,7 @@ void MqttHandlePowerLimiterHassClass::publishSelect( options.add("1"); options.add("2"); - JsonObject deviceObj = root["dev"].to(); - createDeviceInfo(deviceObj); + createDeviceInfo(root); if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { return; @@ -143,14 +147,14 @@ void MqttHandlePowerLimiterHassClass::publishSelect( void MqttHandlePowerLimiterHassClass::publishNumber( const char* caption, const char* icon, const char* category, const char* commandTopic, const char* stateTopic, const char* unitOfMeasure, - const int16_t min, const int16_t max) + const int16_t min, const int16_t max, const float step) { String numberId = caption; numberId.replace(" ", "_"); numberId.toLowerCase(); - const String configTopic = "number/powerlimiter/" + numberId + "/config"; + const String configTopic = "number/" + MqttHandleHass.getDtuUniqueId() + "/" + numberId + "/config"; const String cmdTopic = MqttSettings.getPrefix() + "powerlimiter/cmd/" + commandTopic; const String statTopic = MqttSettings.getPrefix() + "powerlimiter/status/" + stateTopic; @@ -158,7 +162,7 @@ void MqttHandlePowerLimiterHassClass::publishNumber( JsonDocument root; root["name"] = caption; - root["uniq_id"] = numberId; + root["uniq_id"] = MqttHandleHass.getDtuUniqueId() + "_" + numberId; if (strcmp(icon, "")) { root["ic"] = icon; } @@ -168,6 +172,7 @@ void MqttHandlePowerLimiterHassClass::publishNumber( root["unit_of_meas"] = unitOfMeasure; root["min"] = min; root["max"] = max; + root["step"] = step; root["mode"] = "box"; auto const& config = Configuration.get(); @@ -175,8 +180,47 @@ void MqttHandlePowerLimiterHassClass::publishNumber( root["exp_aft"] = config.Mqtt.PublishInterval * 3; } - JsonObject deviceObj = root["dev"].to(); - createDeviceInfo(deviceObj); + createDeviceInfo(root); + + if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { + return; + } + + String buffer; + serializeJson(root, buffer); + publish(configTopic, buffer); +} + +void MqttHandlePowerLimiterHassClass::publishBinarySensor( + const char* caption, const char* icon, + const char* stateTopic, const char* payload_on, const char* payload_off) +{ + + String numberId = caption; + numberId.replace(" ", "_"); + numberId.toLowerCase(); + + const String configTopic = "binary_sensor/" + MqttHandleHass.getDtuUniqueId() + "/" + numberId + "/config"; + + const String statTopic = MqttSettings.getPrefix() + "powerlimiter/status/" + stateTopic; + + JsonDocument root; + + root["name"] = caption; + root["uniq_id"] = MqttHandleHass.getDtuUniqueId() + "_" + numberId; + if (strcmp(icon, "")) { + root["ic"] = icon; + } + root["stat_t"] = statTopic; + root["pl_on"] = payload_on; + root["pl_off"] = payload_off; + + auto const& config = Configuration.get(); + if (config.Mqtt.Hass.Expire) { + root["exp_aft"] = config.Mqtt.PublishInterval * 3; + } + + createDeviceInfo(root); if (!Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { return; @@ -187,14 +231,17 @@ void MqttHandlePowerLimiterHassClass::publishNumber( publish(configTopic, buffer); } -void MqttHandlePowerLimiterHassClass::createDeviceInfo(JsonObject& object) + +void MqttHandlePowerLimiterHassClass::createDeviceInfo(JsonDocument& root) { + JsonObject object = root["dev"].to(); object["name"] = "Dynamic Power Limiter"; - object["ids"] = "0002"; - object["cu"] = String("http://") + NetworkSettings.localIP().toString(); + object["ids"] = MqttHandleHass.getDtuUniqueId() + "_DPL"; + object["cu"] = MqttHandleHass.getDtuUrl(); object["mf"] = "OpenDTU"; object["mdl"] = "Dynamic Power Limiter"; object["sw"] = __COMPILED_GIT_HASH__; + object["via_device"] = MqttHandleHass.getDtuUniqueId(); } void MqttHandlePowerLimiterHassClass::publish(const String& subtopic, const String& payload) diff --git a/src/MqttHandlVedirectHass.cpp b/src/MqttHandleVedirectHass.cpp similarity index 98% rename from src/MqttHandlVedirectHass.cpp rename to src/MqttHandleVedirectHass.cpp index 678b6b529..883705ad9 100644 --- a/src/MqttHandlVedirectHass.cpp +++ b/src/MqttHandleVedirectHass.cpp @@ -5,6 +5,7 @@ #include "MqttHandleVedirectHass.h" #include "Configuration.h" #include "MqttSettings.h" +#include "MqttHandleHass.h" #include "NetworkSettings.h" #include "MessageOutput.h" #include "VictronMppt.h" @@ -212,10 +213,11 @@ void MqttHandleVedirectHassClass::createDeviceInfo(JsonObject &object, String serial = mpptData.serialNr_SER; object["name"] = "Victron(" + serial + ")"; object["ids"] = serial; - object["cu"] = String("http://") + NetworkSettings.localIP().toString(); + object["cu"] = MqttHandleHass.getDtuUrl(); object["mf"] = "OpenDTU"; object["mdl"] = mpptData.getPidAsString(); object["sw"] = __COMPILED_GIT_HASH__; + object["via_device"] = MqttHandleHass.getDtuUniqueId(); } void MqttHandleVedirectHassClass::publish(const String& subtopic, const String& payload) diff --git a/src/MqttSettings.cpp b/src/MqttSettings.cpp index c2b94662a..acd2704f3 100644 --- a/src/MqttSettings.cpp +++ b/src/MqttSettings.cpp @@ -118,7 +118,7 @@ void MqttSettingsClass::performConnect() const CONFIG_T& config = Configuration.get(); _verboseLogging = config.Mqtt.VerboseLogging; const String willTopic = getPrefix() + config.Mqtt.Lwt.Topic; - const String clientId = NetworkSettings.getApName(); + String clientId = getClientId(); if (config.Mqtt.Tls.Enabled) { static_cast(_mqttClient)->setCACert(config.Mqtt.Tls.RootCaCert); static_cast(_mqttClient)->setServer(config.Mqtt.Hostname, config.Mqtt.Port); @@ -183,6 +183,15 @@ String MqttSettingsClass::getPrefix() const return Configuration.get().Mqtt.Topic; } +String MqttSettingsClass::getClientId() +{ + String clientId = Configuration.get().Mqtt.ClientId; + if (clientId == "") { + clientId = NetworkSettings.getApName(); + } + return clientId; +} + void MqttSettingsClass::publish(const String& subtopic, const String& payload) { String topic = getPrefix(); diff --git a/src/NetworkSettings.cpp b/src/NetworkSettings.cpp index dac3ecb0a..55ea428e5 100644 --- a/src/NetworkSettings.cpp +++ b/src/NetworkSettings.cpp @@ -27,6 +27,8 @@ void NetworkSettingsClass::init(Scheduler& scheduler) WiFi.setScanMethod(WIFI_ALL_CHANNEL_SCAN); WiFi.setSortMethod(WIFI_CONNECT_AP_BY_SIGNAL); + WiFi.disconnect(true, true); + WiFi.onEvent(std::bind(&NetworkSettingsClass::NetworkEvent, this, _1)); setupMode(); @@ -77,7 +79,8 @@ void NetworkSettingsClass::NetworkEvent(const WiFiEvent_t event) MessageOutput.println("WiFi disconnected"); if (_networkMode == network_mode::WiFi) { MessageOutput.println("Try reconnecting"); - WiFi.reconnect(); + WiFi.disconnect(true, false); + WiFi.begin(); raiseEvent(network_event::NETWORK_DISCONNECTED); } break; diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index 7ce0d8bd9..1c471a3dc 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -17,11 +17,12 @@ #include #include #include +#include "SunPosition.h" PowerLimiterClass PowerLimiter; -void PowerLimiterClass::init(Scheduler& scheduler) -{ +void PowerLimiterClass::init(Scheduler& scheduler) +{ scheduler.addTask(_loopTask); _loopTask.setCallback(std::bind(&PowerLimiterClass::loop, this)); _loopTask.setIterations(TASK_FOREVER); @@ -201,7 +202,7 @@ void PowerLimiterClass::loop() // 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)) { + if (PowerMeter.isDataValid() && PowerMeter.getLastUpdate() <= (*_oInverterStatsMillis + 2000)) { return announceStatus(Status::PowerMeterPending); } @@ -239,19 +240,23 @@ void PowerLimiterClass::loop() auto getBatteryPower = [this,&config]() -> bool { if (config.PowerLimiter.IsInverterSolarPowered) { return false; } + auto isDayPeriod = SunPosition.isSunsetAvailable() ? SunPosition.isDayPeriod() : getSolarPower() > 0; + + if (_nighttimeDischarging && isDayPeriod) { + _nighttimeDischarging = false; + return isStartThresholdReached(); + } + if (isStopThresholdReached()) { return false; } if (isStartThresholdReached()) { return true; } - // with solar passthrough, and the respective switch enabled, we - // may start discharging the battery when it is nighttime. we also - // stop the discharge cycle if it becomes daytime again. - // TODO(schlimmchen): should be supported by sunrise and sunset, such - // that a thunderstorm or other events that drastically lower the solar - // power do not cause the start of a discharge cycle during the day. if (config.PowerLimiter.SolarPassThroughEnabled && - config.PowerLimiter.BatteryAlwaysUseAtNight) { - return getSolarPower() == 0; + config.PowerLimiter.BatteryAlwaysUseAtNight && + !isDayPeriod && + !_batteryDischargeEnabled) { + _nighttimeDischarging = true; + return true; } // we are between start and stop threshold and keep the state that was @@ -337,6 +342,16 @@ float PowerLimiterClass::getBatteryVoltage(bool log) { return res; } +static float getInverterEfficiency(std::shared_ptr inverter) +{ + float inverterEfficiencyPercent = inverter->Statistics()->getChannelFieldValue( + 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) + return (inverterEfficiencyPercent > 0) ? inverterEfficiencyPercent/100 : 0.967; +} + /** * calculate the AC output power (limit) to set, such that the inverter uses * the given power on its DC side, i.e., adjust the power for the inverter's @@ -346,12 +361,7 @@ int32_t PowerLimiterClass::inverterPowerDcToAc(std::shared_ptr { CONFIG_T& config = Configuration.get(); - float inverterEfficiencyPercent = inverter->Statistics()->getChannelFieldValue( - 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) - float inverterEfficiencyFactor = (inverterEfficiencyPercent > 0) ? inverterEfficiencyPercent/100 : 0.967; + float inverterEfficiencyFactor = getInverterEfficiency(inverter); // account for losses between solar charger and inverter (cables, junctions...) float lossesFactor = 1.00 - static_cast(config.PowerLimiter.SolarPassThroughLosses)/100; @@ -687,10 +697,9 @@ bool PowerLimiterClass::updateInverter() * * TODO(schlimmchen): the current implementation is broken and is in need of * refactoring. currently it only works for inverters that provide one MPPT for - * each input. it also does not work as expected if any input produces *some* - * energy, but is limited by its respective solar input. + * each input. */ -static int32_t scalePowerLimit(std::shared_ptr inverter, int32_t newLimit, int32_t currentLimitWatts) +static int32_t scalePowerLimit(std::shared_ptr inverter, int32_t newLimit, int32_t currentLimitWatts, bool log) { // prevent scaling if inverter is not producing, as input channels are not // producing energy and hence are detected as not-producing, causing @@ -713,6 +722,76 @@ static int32_t scalePowerLimit(std::shared_ptr inverter, int32 // producing very little due to the very low limit. if (currentLimitWatts < dcTotalChnls * 10) { return newLimit; } + auto const& config = Configuration.get(); + auto allowOverscaling = config.PowerLimiter.UseOverscalingToCompensateShading; + auto isInverterSolarPowered = config.PowerLimiter.IsInverterSolarPowered; + + // overscalling allows us to compensate for shaded panels by increasing the + // total power limit, if the inverter is solar powered. + if (allowOverscaling && isInverterSolarPowered) { + auto inverterOutputAC = inverter->Statistics()->getChannelFieldValue(TYPE_AC, CH0, FLD_PAC); + float inverterEfficiencyFactor = getInverterEfficiency(inverter); + + // 98% of the expected power is good enough + auto expectedAcPowerPerChannel = (currentLimitWatts / dcTotalChnls) * 0.98; + + if (log) { + MessageOutput.printf("[DPL::scalePowerLimit] expected AC power per channel %f W\r\n", + expectedAcPowerPerChannel); + } + + size_t dcShadedChnls = 0; + auto shadedChannelACPowerSum = 0.0; + + for (auto& c : dcChnls) { + auto channelPowerAC = inverter->Statistics()->getChannelFieldValue(TYPE_DC, c, FLD_PDC) * inverterEfficiencyFactor; + + if (channelPowerAC < expectedAcPowerPerChannel) { + dcShadedChnls++; + shadedChannelACPowerSum += channelPowerAC; + } + + if (log) { + MessageOutput.printf("[DPL::scalePowerLimit] ch %d AC power %f W\r\n", + c, channelPowerAC); + } + } + + // no shading or the shaded channels provide more power than what + // we currently need. + if (dcShadedChnls == 0 || shadedChannelACPowerSum >= newLimit) { return newLimit; } + + if (dcShadedChnls == dcTotalChnls) { + // keep the currentLimit when: + // - all channels are shaded + // - currentLimit >= newLimit + // - we get the expected AC power or less and + if (currentLimitWatts >= newLimit && inverterOutputAC <= newLimit) { + if (log) { + MessageOutput.printf("[DPL::scalePowerLimit] all channels are shaded, " + "keeping the current limit of %d W\r\n", currentLimitWatts); + } + + return currentLimitWatts; + + } else { + return newLimit; + } + } + + size_t dcNonShadedChnls = dcTotalChnls - dcShadedChnls; + auto overScaledLimit = static_cast((newLimit - shadedChannelACPowerSum) / dcNonShadedChnls * dcTotalChnls); + + if (overScaledLimit <= newLimit) { return newLimit; } + + if (log) { + MessageOutput.printf("[DPL::scalePowerLimit] %d/%d channels are shaded, " + "scaling %d W\r\n", dcShadedChnls, dcTotalChnls, overScaledLimit); + } + + return overScaledLimit; + } + size_t dcProdChnls = 0; for (auto& c : dcChnls) { if (inverter->Statistics()->getChannelFieldValue(TYPE_DC, c, FLD_PDC) > 2.0) { @@ -767,7 +846,7 @@ bool PowerLimiterClass::setNewPowerLimit(std::shared_ptr inver float currentLimitPercent = inverter->SystemConfigPara()->getLimitPercent(); auto currentLimitAbs = static_cast(currentLimitPercent * maxPower / 100); - effPowerLimit = scalePowerLimit(inverter, effPowerLimit, currentLimitAbs); + effPowerLimit = scalePowerLimit(inverter, effPowerLimit, currentLimitAbs, _verboseLogging); effPowerLimit = std::min(effPowerLimit, maxPower); diff --git a/src/PowerMeter.cpp b/src/PowerMeter.cpp index 56582d005..3787dfe75 100644 --- a/src/PowerMeter.cpp +++ b/src/PowerMeter.cpp @@ -1,17 +1,12 @@ // SPDX-License-Identifier: GPL-2.0-or-later -/* - * Copyright (C) 2022 Thomas Basler and others - */ #include "PowerMeter.h" #include "Configuration.h" -#include "PinMapping.h" -#include "HttpPowerMeter.h" -#include "MqttSettings.h" -#include "NetworkSettings.h" -#include "MessageOutput.h" -#include "SerialPortManager.h" -#include -#include +#include "PowerMeterHttpJson.h" +#include "PowerMeterHttpSml.h" +#include "PowerMeterMqtt.h" +#include "PowerMeterSerialSdm.h" +#include "PowerMeterSerialSml.h" +#include "PowerMeterUdpSmaHomeManager.h" PowerMeterClass PowerMeter; @@ -22,277 +17,75 @@ void PowerMeterClass::init(Scheduler& scheduler) _loopTask.setIterations(TASK_FOREVER); _loopTask.enable(); - _lastPowerMeterCheck = 0; - _lastPowerMeterUpdate = 0; - - for (auto const& s: _mqttSubscriptions) { MqttSettings.unsubscribe(s.first); } - _mqttSubscriptions.clear(); - - CONFIG_T& config = Configuration.get(); - - if (!config.PowerMeter.Enabled) { - return; - } - - const PinMapping_t& pin = PinMapping.get(); - MessageOutput.printf("[PowerMeter] rx = %d, tx = %d, dere = %d\r\n", - pin.powermeter_rx, pin.powermeter_tx, pin.powermeter_dere); - - switch(static_cast(config.PowerMeter.Source)) { - case Source::MQTT: { - auto subscribe = [this](char const* topic, float* target) { - if (strlen(topic) == 0) { return; } - MqttSettings.subscribe(topic, 0, - std::bind(&PowerMeterClass::onMqttMessage, - this, std::placeholders::_1, std::placeholders::_2, - std::placeholders::_3, std::placeholders::_4, - std::placeholders::_5, std::placeholders::_6) - ); - _mqttSubscriptions.try_emplace(topic, target); - }; - - subscribe(config.PowerMeter.MqttTopicPowerMeter1, &_powerMeter1Power); - subscribe(config.PowerMeter.MqttTopicPowerMeter2, &_powerMeter2Power); - subscribe(config.PowerMeter.MqttTopicPowerMeter3, &_powerMeter3Power); - break; - } - - case Source::SDM1PH: - case Source::SDM3PH: { - if (pin.powermeter_rx < 0 || pin.powermeter_tx < 0) { - MessageOutput.println("[PowerMeter] invalid pin config for SDM power meter (RX and TX pins must be defined)"); - return; - } - - auto oHwSerialPort = SerialPortManager.allocatePort(_sdmSerialPortOwner); - if (!oHwSerialPort) { return; } - - _upSdmSerial = std::make_unique(*oHwSerialPort); - _upSdmSerial->end(); // make sure the UART will be re-initialized - _upSdm = std::make_unique(*_upSdmSerial, 9600, pin.powermeter_dere, - SERIAL_8N1, pin.powermeter_rx, pin.powermeter_tx); - _upSdm->begin(); - break; - } - - case Source::HTTP: - HttpPowerMeter.init(); - break; - - case Source::SML: - if (pin.powermeter_rx < 0) { - MessageOutput.println("[PowerMeter] invalid pin config for SML power meter (RX pin must be defined)"); - return; - } - - pinMode(pin.powermeter_rx, INPUT); - _upSmlSerial = std::make_unique(); - _upSmlSerial->begin(9600, SWSERIAL_8N1, pin.powermeter_rx, -1, false, 128, 95); - _upSmlSerial->enableRx(true); - _upSmlSerial->enableTx(false); - _upSmlSerial->flush(); - break; - - case Source::SMAHM2: - SMA_HM.init(scheduler, config.PowerMeter.VerboseLogging); - break; - } + updateSettings(); } -void PowerMeterClass::onMqttMessage(const espMqttClientTypes::MessageProperties& properties, const char* topic, const uint8_t* payload, size_t len, size_t index, size_t total) +void PowerMeterClass::updateSettings() { - for (auto const& subscription: _mqttSubscriptions) { - if (subscription.first != topic) { continue; } - - std::string value(reinterpret_cast(payload), len); - try { - *subscription.second = std::stof(value); - } - catch(std::invalid_argument const& e) { - MessageOutput.printf("PowerMeterClass: cannot parse payload of topic '%s' as float: %s\r\n", - topic, value.c_str()); - return; - } - - if (_verboseLogging) { - MessageOutput.printf("PowerMeterClass: Updated from '%s', TotalPower: %5.2f\r\n", - topic, getPowerTotal()); - } + std::lock_guard l(_mutex); - _lastPowerMeterUpdate = millis(); + if (_upProvider) { _upProvider.reset(); } + + auto const& pmcfg = Configuration.get().PowerMeter; + + if (!pmcfg.Enabled) { return; } + + switch(static_cast(pmcfg.Source)) { + case PowerMeterProvider::Type::MQTT: + _upProvider = std::make_unique(pmcfg.Mqtt); + break; + case PowerMeterProvider::Type::SDM1PH: + _upProvider = std::make_unique( + PowerMeterSerialSdm::Phases::One, pmcfg.SerialSdm); + break; + case PowerMeterProvider::Type::SDM3PH: + _upProvider = std::make_unique( + PowerMeterSerialSdm::Phases::Three, pmcfg.SerialSdm); + break; + case PowerMeterProvider::Type::HTTP_JSON: + _upProvider = std::make_unique(pmcfg.HttpJson); + break; + case PowerMeterProvider::Type::SERIAL_SML: + _upProvider = std::make_unique(); + break; + case PowerMeterProvider::Type::SMAHM2: + _upProvider = std::make_unique(); + break; + case PowerMeterProvider::Type::HTTP_SML: + _upProvider = std::make_unique(pmcfg.HttpSml); + break; } -} -float PowerMeterClass::getPowerTotal(bool forceUpdate) -{ - if (forceUpdate) { - CONFIG_T& config = Configuration.get(); - if (config.PowerMeter.Enabled - && (millis() - _lastPowerMeterUpdate) > (1000)) { - readPowerMeter(); - } + if (!_upProvider->init()) { + _upProvider = nullptr; } - - std::lock_guard l(_mutex); - return _powerMeter1Power + _powerMeter2Power + _powerMeter3Power; } -uint32_t PowerMeterClass::getLastPowerMeterUpdate() +float PowerMeterClass::getPowerTotal() const { std::lock_guard l(_mutex); - return _lastPowerMeterUpdate; + if (!_upProvider) { return 0.0; } + return _upProvider->getPowerTotal(); } -bool PowerMeterClass::isDataValid() +uint32_t PowerMeterClass::getLastUpdate() const { - 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; + if (!_upProvider) { return 0; } + return _upProvider->getLastUpdate(); } -void PowerMeterClass::mqtt() +bool PowerMeterClass::isDataValid() const { - if (!MqttSettings.getConnected()) { return; } - - String topic = "powermeter"; - auto totalPower = getPowerTotal(); - std::lock_guard l(_mutex); - MqttSettings.publish(topic + "/power1", String(_powerMeter1Power)); - MqttSettings.publish(topic + "/power2", String(_powerMeter2Power)); - MqttSettings.publish(topic + "/power3", String(_powerMeter3Power)); - MqttSettings.publish(topic + "/powertotal", String(totalPower)); - MqttSettings.publish(topic + "/voltage1", String(_powerMeter1Voltage)); - MqttSettings.publish(topic + "/voltage2", String(_powerMeter2Voltage)); - MqttSettings.publish(topic + "/voltage3", String(_powerMeter3Voltage)); - MqttSettings.publish(topic + "/import", String(_powerMeterImport)); - MqttSettings.publish(topic + "/export", String(_powerMeterExport)); + if (!_upProvider) { return false; } + return _upProvider->isDataValid(); } void PowerMeterClass::loop() { - CONFIG_T const& config = Configuration.get(); - _verboseLogging = config.PowerMeter.VerboseLogging; - - if (!config.PowerMeter.Enabled) { return; } - - if (static_cast(config.PowerMeter.Source) == Source::SML && - nullptr != _upSmlSerial) { - if (!smlReadLoop()) { return; } - _lastPowerMeterUpdate = millis(); - } - - if ((millis() - _lastPowerMeterCheck) < (config.PowerMeter.Interval * 1000)) { - return; - } - - readPowerMeter(); - - MessageOutput.printf("PowerMeterClass: TotalPower: %5.2f\r\n", getPowerTotal()); - - mqtt(); - - _lastPowerMeterCheck = millis(); -} - -void PowerMeterClass::readPowerMeter() -{ - CONFIG_T& config = Configuration.get(); - - uint8_t _address = config.PowerMeter.SdmAddress; - Source configuredSource = static_cast(config.PowerMeter.Source); - - if (configuredSource == Source::SDM1PH) { - if (!_upSdm) { return; } - - // this takes a "very long" time as each readVal() is a synchronous - // exchange of serial messages. cache the values and write later. - auto phase1Power = _upSdm->readVal(SDM_PHASE_1_POWER, _address); - auto phase1Voltage = _upSdm->readVal(SDM_PHASE_1_VOLTAGE, _address); - auto energyImport = _upSdm->readVal(SDM_IMPORT_ACTIVE_ENERGY, _address); - auto energyExport = _upSdm->readVal(SDM_EXPORT_ACTIVE_ENERGY, _address); - - std::lock_guard l(_mutex); - _powerMeter1Power = static_cast(phase1Power); - _powerMeter2Power = 0; - _powerMeter3Power = 0; - _powerMeter1Voltage = static_cast(phase1Voltage); - _powerMeter2Voltage = 0; - _powerMeter3Voltage = 0; - _powerMeterImport = static_cast(energyImport); - _powerMeterExport = static_cast(energyExport); - _lastPowerMeterUpdate = millis(); - } - else if (configuredSource == Source::SDM3PH) { - if (!_upSdm) { return; } - - // this takes a "very long" time as each readVal() is a synchronous - // exchange of serial messages. cache the values and write later. - auto phase1Power = _upSdm->readVal(SDM_PHASE_1_POWER, _address); - auto phase2Power = _upSdm->readVal(SDM_PHASE_2_POWER, _address); - auto phase3Power = _upSdm->readVal(SDM_PHASE_3_POWER, _address); - auto phase1Voltage = _upSdm->readVal(SDM_PHASE_1_VOLTAGE, _address); - auto phase2Voltage = _upSdm->readVal(SDM_PHASE_2_VOLTAGE, _address); - auto phase3Voltage = _upSdm->readVal(SDM_PHASE_3_VOLTAGE, _address); - auto energyImport = _upSdm->readVal(SDM_IMPORT_ACTIVE_ENERGY, _address); - auto energyExport = _upSdm->readVal(SDM_EXPORT_ACTIVE_ENERGY, _address); - - std::lock_guard l(_mutex); - _powerMeter1Power = static_cast(phase1Power); - _powerMeter2Power = static_cast(phase2Power); - _powerMeter3Power = static_cast(phase3Power); - _powerMeter1Voltage = static_cast(phase1Voltage); - _powerMeter2Voltage = static_cast(phase2Voltage); - _powerMeter3Voltage = static_cast(phase3Voltage); - _powerMeterImport = static_cast(energyImport); - _powerMeterExport = static_cast(energyExport); - _lastPowerMeterUpdate = millis(); - } - else if (configuredSource == Source::HTTP) { - if (HttpPowerMeter.updateValues()) { - std::lock_guard l(_mutex); - _powerMeter1Power = HttpPowerMeter.getPower(1); - _powerMeter2Power = HttpPowerMeter.getPower(2); - _powerMeter3Power = HttpPowerMeter.getPower(3); - _lastPowerMeterUpdate = millis(); - } - } - else if (configuredSource == Source::SMAHM2) { - std::lock_guard l(_mutex); - _powerMeter1Power = SMA_HM.getPowerL1(); - _powerMeter2Power = SMA_HM.getPowerL2(); - _powerMeter3Power = SMA_HM.getPowerL3(); - _lastPowerMeterUpdate = millis(); - } -} - -bool PowerMeterClass::smlReadLoop() -{ - while (_upSmlSerial->available()) { - double readVal = 0; - unsigned char smlCurrentChar = _upSmlSerial->read(); - sml_states_t smlCurrentState = smlState(smlCurrentChar); - if (smlCurrentState == SML_LISTEND) { - for (auto& handler: smlHandlerList) { - if (smlOBISCheck(handler.OBIS)) { - handler.Fn(readVal); - *handler.Arg = readVal; - } - } - } else if (smlCurrentState == SML_FINAL) { - return true; - } - } - - return false; + std::lock_guard lock(_mutex); + if (!_upProvider) { return; } + _upProvider->loop(); + _upProvider->mqttLoop(); } diff --git a/src/PowerMeterHttpJson.cpp b/src/PowerMeterHttpJson.cpp new file mode 100644 index 000000000..3d4431f3d --- /dev/null +++ b/src/PowerMeterHttpJson.cpp @@ -0,0 +1,192 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "Utils.h" +#include "PowerMeterHttpJson.h" +#include "MessageOutput.h" +#include +#include +#include "mbedtls/sha256.h" +#include +#include + +PowerMeterHttpJson::~PowerMeterHttpJson() +{ + _taskDone = false; + + std::unique_lock lock(_pollingMutex); + _stopPolling = true; + lock.unlock(); + + _cv.notify_all(); + + if (_taskHandle != nullptr) { + while (!_taskDone) { delay(10); } + _taskHandle = nullptr; + } +} + +bool PowerMeterHttpJson::init() +{ + for (uint8_t i = 0; i < POWERMETER_HTTP_JSON_MAX_VALUES; i++) { + auto const& valueConfig = _cfg.Values[i]; + + _httpGetters[i] = nullptr; + + if (i == 0 || (_cfg.IndividualRequests && valueConfig.Enabled)) { + _httpGetters[i] = std::make_unique(valueConfig.HttpRequest); + } + + if (!_httpGetters[i]) { continue; } + + if (_httpGetters[i]->init()) { + _httpGetters[i]->addHeader("Content-Type", "application/json"); + _httpGetters[i]->addHeader("Accept", "application/json"); + continue; + } + + MessageOutput.printf("[PowerMeterHttpJson] Initializing HTTP getter for value %d failed:\r\n", i + 1); + MessageOutput.printf("[PowerMeterHttpJson] %s\r\n", _httpGetters[i]->getErrorText()); + return false; + } + + return true; +} + +void PowerMeterHttpJson::loop() +{ + if (_taskHandle != nullptr) { return; } + + std::unique_lock lock(_pollingMutex); + _stopPolling = false; + lock.unlock(); + + uint32_t constexpr stackSize = 3072; + xTaskCreate(PowerMeterHttpJson::pollingLoopHelper, "PM:HTTP+JSON", + stackSize, this, 1/*prio*/, &_taskHandle); +} + +void PowerMeterHttpJson::pollingLoopHelper(void* context) +{ + auto pInstance = static_cast(context); + pInstance->pollingLoop(); + pInstance->_taskDone = true; + vTaskDelete(nullptr); +} + +void PowerMeterHttpJson::pollingLoop() +{ + std::unique_lock lock(_pollingMutex); + + while (!_stopPolling) { + auto elapsedMillis = millis() - _lastPoll; + auto intervalMillis = _cfg.PollingInterval * 1000; + if (_lastPoll > 0 && elapsedMillis < intervalMillis) { + auto sleepMs = intervalMillis - elapsedMillis; + _cv.wait_for(lock, std::chrono::milliseconds(sleepMs), + [this] { return _stopPolling; }); // releases the mutex + continue; + } + + _lastPoll = millis(); + + lock.unlock(); // polling can take quite some time + auto res = poll(); + lock.lock(); + + if (std::holds_alternative(res)) { + MessageOutput.printf("[PowerMeterHttpJson] %s\r\n", std::get(res).c_str()); + continue; + } + + MessageOutput.printf("[PowerMeterHttpJson] New total: %.2f\r\n", getPowerTotal()); + + gotUpdate(); + } +} + +PowerMeterHttpJson::poll_result_t PowerMeterHttpJson::poll() +{ + power_values_t cache; + JsonDocument jsonResponse; + + auto prefixedError = [](uint8_t idx, char const* err) -> String { + String res("Value "); + res.reserve(strlen(err) + 16); + return res + String(idx + 1) + ": " + err; + }; + + for (uint8_t i = 0; i < POWERMETER_HTTP_JSON_MAX_VALUES; i++) { + auto const& cfg = _cfg.Values[i]; + + if (!cfg.Enabled) { + cache[i] = 0.0; + continue; + } + + auto const& upGetter = _httpGetters[i]; + + if (upGetter) { + auto res = upGetter->performGetRequest(); + if (!res) { + return prefixedError(i, upGetter->getErrorText()); + } + + auto pStream = res.getStream(); + if (!pStream) { + return prefixedError(i, "Programmer error: HTTP request yields no stream"); + } + + const DeserializationError error = deserializeJson(jsonResponse, *pStream); + if (error) { + String msg("Unable to parse server response as JSON: "); + return prefixedError(i, String(msg + error.c_str()).c_str()); + } + } + + auto pathResolutionResult = Utils::getJsonValueByPath(jsonResponse, cfg.JsonPath); + if (!pathResolutionResult.second.isEmpty()) { + return prefixedError(i, pathResolutionResult.second.c_str()); + } + + // this value is supposed to be in Watts and positive if energy is consumed + cache[i] = pathResolutionResult.first; + + switch (cfg.PowerUnit) { + case Unit_t::MilliWatts: + cache[i] /= 1000; + break; + case Unit_t::KiloWatts: + cache[i] *= 1000; + break; + default: + break; + } + + if (cfg.SignInverted) { cache[i] *= -1; } + } + + std::unique_lock lock(_valueMutex); + _powerValues = cache; + return cache; +} + +float PowerMeterHttpJson::getPowerTotal() const +{ + float sum = 0.0; + std::unique_lock lock(_valueMutex); + for (auto v: _powerValues) { sum += v; } + return sum; +} + +bool PowerMeterHttpJson::isDataValid() const +{ + uint32_t age = millis() - getLastUpdate(); + return getLastUpdate() > 0 && (age < (3 * _cfg.PollingInterval * 1000)); +} + +void PowerMeterHttpJson::doMqttPublish() const +{ + std::unique_lock lock(_valueMutex); + mqttPublish("power1", _powerValues[0]); + mqttPublish("power2", _powerValues[1]); + mqttPublish("power3", _powerValues[2]); +} diff --git a/src/PowerMeterHttpSml.cpp b/src/PowerMeterHttpSml.cpp new file mode 100644 index 000000000..8cfd14806 --- /dev/null +++ b/src/PowerMeterHttpSml.cpp @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "PowerMeterHttpSml.h" +#include "MessageOutput.h" +#include +#include +#include + +PowerMeterHttpSml::~PowerMeterHttpSml() +{ + _taskDone = false; + + std::unique_lock lock(_pollingMutex); + _stopPolling = true; + lock.unlock(); + + _cv.notify_all(); + + if (_taskHandle != nullptr) { + while (!_taskDone) { delay(10); } + _taskHandle = nullptr; + } +} + +bool PowerMeterHttpSml::init() +{ + _upHttpGetter = std::make_unique(_cfg.HttpRequest); + + if (_upHttpGetter->init()) { return true; } + + MessageOutput.printf("[PowerMeterHttpSml] Initializing HTTP getter failed:\r\n"); + MessageOutput.printf("[PowerMeterHttpSml] %s\r\n", _upHttpGetter->getErrorText()); + + _upHttpGetter = nullptr; + + return false; +} + +void PowerMeterHttpSml::loop() +{ + if (_taskHandle != nullptr) { return; } + + std::unique_lock lock(_pollingMutex); + _stopPolling = false; + lock.unlock(); + + uint32_t constexpr stackSize = 3072; + xTaskCreate(PowerMeterHttpSml::pollingLoopHelper, "PM:HTTP+SML", + stackSize, this, 1/*prio*/, &_taskHandle); +} + +void PowerMeterHttpSml::pollingLoopHelper(void* context) +{ + auto pInstance = static_cast(context); + pInstance->pollingLoop(); + pInstance->_taskDone = true; + vTaskDelete(nullptr); +} + +void PowerMeterHttpSml::pollingLoop() +{ + std::unique_lock lock(_pollingMutex); + + while (!_stopPolling) { + auto elapsedMillis = millis() - _lastPoll; + auto intervalMillis = _cfg.PollingInterval * 1000; + if (_lastPoll > 0 && elapsedMillis < intervalMillis) { + auto sleepMs = intervalMillis - elapsedMillis; + _cv.wait_for(lock, std::chrono::milliseconds(sleepMs), + [this] { return _stopPolling; }); // releases the mutex + continue; + } + + _lastPoll = millis(); + + lock.unlock(); // polling can take quite some time + auto res = poll(); + lock.lock(); + + if (!res.isEmpty()) { + MessageOutput.printf("[PowerMeterHttpSml] %s\r\n", res.c_str()); + continue; + } + + gotUpdate(); + } +} + +bool PowerMeterHttpSml::isDataValid() const +{ + uint32_t age = millis() - getLastUpdate(); + return getLastUpdate() > 0 && (age < (3 * _cfg.PollingInterval * 1000)); +} + +String PowerMeterHttpSml::poll() +{ + if (!_upHttpGetter) { + return "Initialization of HTTP request failed"; + } + + auto res = _upHttpGetter->performGetRequest(); + if (!res) { + return _upHttpGetter->getErrorText(); + } + + auto pStream = res.getStream(); + if (!pStream) { + return "Programmer error: HTTP request yields no stream"; + } + + while (pStream->available()) { + processSmlByte(pStream->read()); + } + + PowerMeterSml::reset(); + + return ""; +} diff --git a/src/PowerMeterMqtt.cpp b/src/PowerMeterMqtt.cpp new file mode 100644 index 000000000..787973702 --- /dev/null +++ b/src/PowerMeterMqtt.cpp @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "PowerMeterMqtt.h" +#include "MqttSettings.h" +#include "MessageOutput.h" +#include "ArduinoJson.h" +#include "Utils.h" + +bool PowerMeterMqtt::init() +{ + auto subscribe = [this](PowerMeterMqttValue const& val, float* targetVariable) { + *targetVariable = 0; + char const* topic = val.Topic; + if (strlen(topic) == 0) { return; } + MqttSettings.subscribe(topic, 0, + std::bind(&PowerMeterMqtt::onMessage, + this, std::placeholders::_1, std::placeholders::_2, + std::placeholders::_3, std::placeholders::_4, + std::placeholders::_5, std::placeholders::_6, + targetVariable, &val) + ); + _mqttSubscriptions.push_back(topic); + }; + + for (size_t i = 0; i < _powerValues.size(); ++i) { + subscribe(_cfg.Values[i], &_powerValues[i]); + } + + return _mqttSubscriptions.size() > 0; +} + +PowerMeterMqtt::~PowerMeterMqtt() +{ + for (auto const& t: _mqttSubscriptions) { MqttSettings.unsubscribe(t); } + _mqttSubscriptions.clear(); +} + +void PowerMeterMqtt::onMessage(PowerMeterMqtt::MsgProperties const& properties, + char const* topic, uint8_t const* payload, size_t len, size_t index, + size_t total, float* targetVariable, PowerMeterMqttValue const* cfg) +{ + auto extracted = Utils::getNumericValueFromMqttPayload("PowerMeterMqtt", + std::string(reinterpret_cast(payload), len), topic, + cfg->JsonPath); + + if (!extracted.has_value()) { return; } + + float newValue = *extracted; + + using Unit_t = PowerMeterMqttValue::Unit; + switch (cfg->PowerUnit) { + case Unit_t::MilliWatts: + newValue /= 1000; + break; + case Unit_t::KiloWatts: + newValue *= 1000; + break; + default: + break; + } + + if (cfg->SignInverted) { newValue *= -1; } + + { + std::lock_guard l(_mutex); + *targetVariable = newValue; + } + + if (_verboseLogging) { + MessageOutput.printf("[PowerMeterMqtt] Topic '%s': new value: %5.2f, " + "total: %5.2f\r\n", topic, newValue, getPowerTotal()); + } + + gotUpdate(); +} + +float PowerMeterMqtt::getPowerTotal() const +{ + float sum = 0.0; + std::unique_lock lock(_mutex); + for (auto v: _powerValues) { sum += v; } + return sum; +} + +void PowerMeterMqtt::doMqttPublish() const +{ + std::lock_guard l(_mutex); + mqttPublish("power1", _powerValues[0]); + mqttPublish("power2", _powerValues[1]); + mqttPublish("power3", _powerValues[2]); +} diff --git a/src/PowerMeterProvider.cpp b/src/PowerMeterProvider.cpp new file mode 100644 index 000000000..d1c7d6282 --- /dev/null +++ b/src/PowerMeterProvider.cpp @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "PowerMeterProvider.h" +#include "MqttSettings.h" + +bool PowerMeterProvider::isDataValid() const +{ + return _lastUpdate > 0 && ((millis() - _lastUpdate) < (30 * 1000)); +} + +void PowerMeterProvider::mqttPublish(String const& topic, float const& value) const +{ + MqttSettings.publish("powermeter/" + topic, String(value)); +} + +void PowerMeterProvider::mqttLoop() const +{ + if (!MqttSettings.getConnected()) { return; } + + if (!isDataValid()) { return; } + + auto constexpr halfOfAllMillis = std::numeric_limits::max() / 2; + if ((_lastUpdate - _lastMqttPublish) > halfOfAllMillis) { return; } + + mqttPublish("powertotal", getPowerTotal()); + + doMqttPublish(); + + _lastMqttPublish = millis(); +} diff --git a/src/PowerMeterSerialSdm.cpp b/src/PowerMeterSerialSdm.cpp new file mode 100644 index 000000000..6c0ce82fc --- /dev/null +++ b/src/PowerMeterSerialSdm.cpp @@ -0,0 +1,203 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "PowerMeterSerialSdm.h" +#include "PinMapping.h" +#include "MessageOutput.h" + +PowerMeterSerialSdm::~PowerMeterSerialSdm() +{ + _taskDone = false; + + std::unique_lock lock(_pollingMutex); + _stopPolling = true; + lock.unlock(); + + _cv.notify_all(); + + if (_taskHandle != nullptr) { + while (!_taskDone) { delay(10); } + _taskHandle = nullptr; + } + + if (_upSdmSerial) { + _upSdmSerial->end(); + _upSdmSerial = nullptr; + } +} + +bool PowerMeterSerialSdm::init() +{ + const PinMapping_t& pin = PinMapping.get(); + + MessageOutput.printf("[PowerMeterSerialSdm] rx = %d, tx = %d, dere = %d\r\n", + pin.powermeter_rx, pin.powermeter_tx, pin.powermeter_dere); + + if (pin.powermeter_rx < 0 || pin.powermeter_tx < 0) { + MessageOutput.println("[PowerMeterSerialSdm] invalid pin config for SDM " + "power meter (RX and TX pins must be defined)"); + return false; + } + + _upSdmSerial = std::make_unique(); + _upSdm = std::make_unique(*_upSdmSerial, 9600, pin.powermeter_dere, + SWSERIAL_8N1, pin.powermeter_rx, pin.powermeter_tx); + _upSdm->begin(); + + return true; +} + +void PowerMeterSerialSdm::loop() +{ + if (_taskHandle != nullptr) { return; } + + std::unique_lock lock(_pollingMutex); + _stopPolling = false; + lock.unlock(); + + uint32_t constexpr stackSize = 3072; + xTaskCreate(PowerMeterSerialSdm::pollingLoopHelper, "PM:SDM", + stackSize, this, 1/*prio*/, &_taskHandle); +} + +float PowerMeterSerialSdm::getPowerTotal() const +{ + std::lock_guard l(_valueMutex); + return _phase1Power + _phase2Power + _phase3Power; +} + +bool PowerMeterSerialSdm::isDataValid() const +{ + uint32_t age = millis() - getLastUpdate(); + return getLastUpdate() > 0 && (age < (3 * _cfg.PollingInterval * 1000)); +} + +void PowerMeterSerialSdm::doMqttPublish() const +{ + std::lock_guard l(_valueMutex); + mqttPublish("power1", _phase1Power); + mqttPublish("voltage1", _phase1Voltage); + mqttPublish("import", _energyImport); + mqttPublish("export", _energyExport); + + if (_phases == Phases::Three) { + mqttPublish("power2", _phase2Power); + mqttPublish("power3", _phase3Power); + mqttPublish("voltage2", _phase2Voltage); + mqttPublish("voltage3", _phase3Voltage); + } +} + +void PowerMeterSerialSdm::pollingLoopHelper(void* context) +{ + auto pInstance = static_cast(context); + pInstance->pollingLoop(); + pInstance->_taskDone = true; + vTaskDelete(nullptr); +} + +bool PowerMeterSerialSdm::readValue(std::unique_lock& lock, uint16_t reg, float& targetVar) +{ + lock.unlock(); // reading values takes too long to keep holding the lock + float val = _upSdm->readVal(reg, _cfg.Address); + lock.lock(); + + // we additionally check in between each transaction whether or not we are + // actually asked to stop polling altogether. otherwise, the destructor of + // this instance might need to wait for a whole while until the task ends. + if (_stopPolling) { return false; } + + auto err = _upSdm->getErrCode(true/*clear error code*/); + + switch (err) { + case SDM_ERR_NO_ERROR: + if (_verboseLogging) { + MessageOutput.printf("[PowerMeterSerialSdm]: read register %d " + "(0x%04x) successfully\r\n", reg, reg); + } + + targetVar = val; + return true; + break; + case SDM_ERR_CRC_ERROR: + MessageOutput.printf("[PowerMeterSerialSdm]: CRC error " + "while reading register %d (0x%04x)\r\n", reg, reg); + break; + case SDM_ERR_WRONG_BYTES: + MessageOutput.printf("[PowerMeterSerialSdm]: unexpected data in " + "message while reading register %d (0x%04x)\r\n", reg, reg); + break; + case SDM_ERR_NOT_ENOUGHT_BYTES: + MessageOutput.printf("[PowerMeterSerialSdm]: unexpected end of " + "message while reading register %d (0x%04x)\r\n", reg, reg); + break; + case SDM_ERR_TIMEOUT: + MessageOutput.printf("[PowerMeterSerialSdm]: timeout occured " + "while reading register %d (0x%04x)\r\n", reg, reg); + break; + default: + MessageOutput.printf("[PowerMeterSerialSdm]: unknown SDM error " + "code after reading register %d (0x%04x)\r\n", reg, reg); + break; + } + + return false; +} + +void PowerMeterSerialSdm::pollingLoop() +{ + std::unique_lock lock(_pollingMutex); + + while (!_stopPolling) { + auto elapsedMillis = millis() - _lastPoll; + auto intervalMillis = _cfg.PollingInterval * 1000; + if (_lastPoll > 0 && elapsedMillis < intervalMillis) { + auto sleepMs = intervalMillis - elapsedMillis; + _cv.wait_for(lock, std::chrono::milliseconds(sleepMs), + [this] { return _stopPolling; }); // releases the mutex + continue; + } + + _lastPoll = millis(); + + // reading takes a "very long" time as each readVal() is a synchronous + // exchange of serial messages. cache the values and write later to + // enforce consistent values. + float phase1Power = 0.0; + float phase2Power = 0.0; + float phase3Power = 0.0; + float phase1Voltage = 0.0; + float phase2Voltage = 0.0; + float phase3Voltage = 0.0; + float energyImport = 0.0; + float energyExport = 0.0; + + bool success = readValue(lock, SDM_PHASE_1_POWER, phase1Power) && + readValue(lock, SDM_PHASE_1_VOLTAGE, phase1Voltage) && + readValue(lock, SDM_IMPORT_ACTIVE_ENERGY, energyImport) && + readValue(lock, SDM_EXPORT_ACTIVE_ENERGY, energyExport); + + if (success && _phases == Phases::Three) { + success = readValue(lock, SDM_PHASE_2_POWER, phase2Power) && + readValue(lock, SDM_PHASE_3_POWER, phase3Power) && + readValue(lock, SDM_PHASE_2_VOLTAGE, phase2Voltage) && + readValue(lock, SDM_PHASE_3_VOLTAGE, phase3Voltage); + } + + if (!success) { continue; } + + { + std::lock_guard l(_valueMutex); + _phase1Power = static_cast(phase1Power); + _phase2Power = static_cast(phase2Power); + _phase3Power = static_cast(phase3Power); + _phase1Voltage = static_cast(phase1Voltage); + _phase2Voltage = static_cast(phase2Voltage); + _phase3Voltage = static_cast(phase3Voltage); + _energyImport = static_cast(energyImport); + _energyExport = static_cast(energyExport); + } + + MessageOutput.printf("[PowerMeterSerialSdm] TotalPower: %5.2f\r\n", getPowerTotal()); + + gotUpdate(); + } +} diff --git a/src/PowerMeterSerialSml.cpp b/src/PowerMeterSerialSml.cpp new file mode 100644 index 000000000..a6af3ccba --- /dev/null +++ b/src/PowerMeterSerialSml.cpp @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "PowerMeterSerialSml.h" +#include "PinMapping.h" +#include "MessageOutput.h" + +bool PowerMeterSerialSml::init() +{ + const PinMapping_t& pin = PinMapping.get(); + + MessageOutput.printf("[PowerMeterSerialSml] rx = %d\r\n", pin.powermeter_rx); + + if (pin.powermeter_rx < 0) { + MessageOutput.println("[PowerMeterSerialSml] invalid pin config " + "for serial SML power meter (RX pin must be defined)"); + return false; + } + + pinMode(pin.powermeter_rx, INPUT); + _upSmlSerial = std::make_unique(); + _upSmlSerial->begin(_baud, SWSERIAL_8N1, pin.powermeter_rx, -1/*tx pin*/, + false/*invert*/, _bufCapacity, _isrCapacity); + _upSmlSerial->enableRx(true); + _upSmlSerial->enableTx(false); + _upSmlSerial->flush(); + + return true; +} + +void PowerMeterSerialSml::loop() +{ + if (_taskHandle != nullptr) { return; } + + std::unique_lock lock(_pollingMutex); + _stopPolling = false; + lock.unlock(); + + uint32_t constexpr stackSize = 3072; + xTaskCreate(PowerMeterSerialSml::pollingLoopHelper, "PM:SML", + stackSize, this, 1/*prio*/, &_taskHandle); +} + +PowerMeterSerialSml::~PowerMeterSerialSml() +{ + _taskDone = false; + + std::unique_lock lock(_pollingMutex); + _stopPolling = true; + lock.unlock(); + + if (_taskHandle != nullptr) { + while (!_taskDone) { delay(10); } + _taskHandle = nullptr; + } + + if (_upSmlSerial) { + _upSmlSerial->end(); + _upSmlSerial = nullptr; + } +} + +void PowerMeterSerialSml::pollingLoopHelper(void* context) +{ + auto pInstance = static_cast(context); + pInstance->pollingLoop(); + pInstance->_taskDone = true; + vTaskDelete(nullptr); +} + +void PowerMeterSerialSml::pollingLoop() +{ + int lastAvailable = 0; + uint32_t gapStartMillis = 0; + std::unique_lock lock(_pollingMutex); + + while (!_stopPolling) { + lock.unlock(); + + // calling available() will decode bytes into the receive buffer and + // hence free data from the ISR buffer, so we need to call this rather + // frequenly. + int nowAvailable = _upSmlSerial->available(); + + if (nowAvailable <= 0) { + // sleep, but at most until the software serial ISR + // buffer is potentially half full with transitions. + uint32_t constexpr delayMs = _isrCapacity * 1000 / _baud / 2; + + delay(delayMs); // this yields so other tasks are scheduled + + lock.lock(); + continue; + } + + // sleep more if new data arrived in the meantime. process data only + // once a SML datagram seems to be complete (no new data arrived while + // we slept). this seems to be important as using read() while more + // data arrives causes trouble (we are missing bytes). + if (nowAvailable > lastAvailable) { + lastAvailable = nowAvailable; + delay(10); + gapStartMillis = millis(); + lock.lock(); + continue; + } + + if ((millis() - gapStartMillis) < _datagramGapMillis) { + delay(10); + lock.lock(); + continue; + } + + while (_upSmlSerial->available() > 0) { + processSmlByte(_upSmlSerial->read()); + } + + lastAvailable = 0; + + PowerMeterSml::reset(); + + lock.lock(); + } +} diff --git a/src/PowerMeterSml.cpp b/src/PowerMeterSml.cpp new file mode 100644 index 000000000..c44662104 --- /dev/null +++ b/src/PowerMeterSml.cpp @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "PowerMeterSml.h" +#include "MessageOutput.h" + +float PowerMeterSml::getPowerTotal() const +{ + std::lock_guard l(_mutex); + if (_values.activePowerTotal.has_value()) { return *_values.activePowerTotal; } + return 0; +} + +void PowerMeterSml::doMqttPublish() const +{ +#define PUB(t, m) \ + if (_values.m.has_value()) { mqttPublish(t, *_values.m); } + + std::lock_guard l(_mutex); + PUB("power1", activePowerL1); + PUB("power2", activePowerL2); + PUB("power3", activePowerL3); + PUB("voltage1", voltageL1); + PUB("voltage2", voltageL2); + PUB("voltage3", voltageL3); + PUB("current1", currentL1); + PUB("current2", currentL2); + PUB("current3", currentL3); + PUB("import", energyImport); + PUB("export", energyExport); + +#undef PUB +} + +void PowerMeterSml::reset() +{ + smlReset(); + _cache = { std::nullopt }; +} + +void PowerMeterSml::processSmlByte(uint8_t byte) +{ + switch (smlState(byte)) { + case SML_LISTEND: + for (auto& handler: smlHandlerList) { + if (!smlOBISCheck(handler.OBIS)) { continue; } + + float helper = 0.0; + handler.decoder(helper); + + if (_verboseLogging) { + MessageOutput.printf("[%s] decoded %s to %.2f\r\n", + _user.c_str(), handler.name, helper); + } + + std::lock_guard l(_mutex); + *handler.target = helper; + } + break; + case SML_FINAL: + gotUpdate(); + _values = _cache; + reset(); + MessageOutput.printf("[%s] TotalPower: %5.2f\r\n", + _user.c_str(), getPowerTotal()); + break; + case SML_CHECKSUM_ERROR: + reset(); + MessageOutput.printf("[%s] checksum verification failed\r\n", + _user.c_str()); + break; + default: + break; + } +} diff --git a/src/SMA_HM.cpp b/src/PowerMeterUdpSmaHomeManager.cpp similarity index 79% rename from src/SMA_HM.cpp rename to src/PowerMeterUdpSmaHomeManager.cpp index 7a3a9fe2e..2baa9c43a 100644 --- a/src/SMA_HM.cpp +++ b/src/PowerMeterUdpSmaHomeManager.cpp @@ -2,51 +2,46 @@ /* * Copyright (C) 2024 Holger-Steffen Stapf */ -#include "SMA_HM.h" +#include "PowerMeterUdpSmaHomeManager.h" #include -#include "Configuration.h" -#include "NetworkSettings.h" #include #include "MessageOutput.h" -unsigned int multicastPort = 9522; // local port to listen on -IPAddress multicastIP(239, 12, 255, 254); -WiFiUDP SMAUdp; +static constexpr unsigned int multicastPort = 9522; // local port to listen on +static const IPAddress multicastIP(239, 12, 255, 254); +static WiFiUDP SMAUdp; constexpr uint32_t interval = 1000; -SMA_HMClass SMA_HM; - -void SMA_HMClass::Soutput(int kanal, int index, int art, int tarif, +void PowerMeterUdpSmaHomeManager::Soutput(int kanal, int index, int art, int tarif, char const* name, float value, uint32_t timestamp) { if (!_verboseLogging) { return; } - MessageOutput.printf("SMA_HM: %s = %.1f (timestamp %d)\r\n", + MessageOutput.printf("[PowerMeterUdpSmaHomeManager] %s = %.1f (timestamp %d)\r\n", name, value, timestamp); } -void SMA_HMClass::init(Scheduler& scheduler, bool verboseLogging) +bool PowerMeterUdpSmaHomeManager::init() { - _verboseLogging = verboseLogging; - scheduler.addTask(_loopTask); - _loopTask.setCallback(std::bind(&SMA_HMClass::loop, this)); - _loopTask.setIterations(TASK_FOREVER); - _loopTask.enable(); SMAUdp.begin(multicastPort); SMAUdp.beginMulticast(multicastIP, multicastPort); + return true; } -void SMA_HMClass::loop() +PowerMeterUdpSmaHomeManager::~PowerMeterUdpSmaHomeManager() { - uint32_t currentMillis = millis(); - if (currentMillis - _previousMillis >= interval) { - _previousMillis = currentMillis; - event1(); - } + SMAUdp.stop(); } -uint8_t* SMA_HMClass::decodeGroup(uint8_t* offset, uint16_t grouplen) +void PowerMeterUdpSmaHomeManager::doMqttPublish() const +{ + mqttPublish("power1", _powerMeterL1); + mqttPublish("power2", _powerMeterL2); + mqttPublish("power3", _powerMeterL3); +} + +uint8_t* PowerMeterUdpSmaHomeManager::decodeGroup(uint8_t* offset, uint16_t grouplen) { float Pbezug = 0; float BezugL1 = 0; @@ -149,7 +144,7 @@ uint8_t* SMA_HMClass::decodeGroup(uint8_t* offset, uint16_t grouplen) continue; } - MessageOutput.printf("SMA_HM: Skipped unknown measurement: %d %d %d %d\r\n", + MessageOutput.printf("[PowerMeterUdpSmaHomeManager] Skipped unknown measurement: %d %d %d %d\r\n", kanal, index, art, tarif); offset += art; } @@ -157,15 +152,20 @@ uint8_t* SMA_HMClass::decodeGroup(uint8_t* offset, uint16_t grouplen) return offset; } -void SMA_HMClass::event1() +void PowerMeterUdpSmaHomeManager::loop() { + uint32_t currentMillis = millis(); + if (currentMillis - _previousMillis < interval) { return; } + + _previousMillis = currentMillis; + int packetSize = SMAUdp.parsePacket(); if (!packetSize) { return; } uint8_t buffer[1024]; int rSize = SMAUdp.read(buffer, 1024); if (buffer[0] != 'S' || buffer[1] != 'M' || buffer[2] != 'A') { - MessageOutput.println("SMA_HM: Not an SMA packet?"); + MessageOutput.println("[PowerMeterUdpSmaHomeManager] Not an SMA packet?"); return; } @@ -196,7 +196,7 @@ void SMA_HMClass::event1() continue; } - MessageOutput.printf("SMA_HM: Unhandled group 0x%04x with length %d\r\n", + MessageOutput.printf("[PowerMeterUdpSmaHomeManager] Unhandled group 0x%04x with length %d\r\n", grouptag, grouplen); offset += grouplen; } while (grouplen > 0 && offset + 4 < buffer + rSize); diff --git a/src/PylontechCanReceiver.cpp b/src/PylontechCanReceiver.cpp index e19cff599..517a6a230 100644 --- a/src/PylontechCanReceiver.cpp +++ b/src/PylontechCanReceiver.cpp @@ -1,127 +1,18 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include "PylontechCanReceiver.h" -#include "Configuration.h" #include "MessageOutput.h" #include "PinMapping.h" #include #include -//#define PYLONTECH_DUMMY - bool PylontechCanReceiver::init(bool verboseLogging) { - _verboseLogging = verboseLogging; - - MessageOutput.println("[Pylontech] Initialize interface..."); - - const PinMapping_t& pin = PinMapping.get(); - MessageOutput.printf("[Pylontech] Interface rx = %d, tx = %d\r\n", - pin.battery_rx, pin.battery_tx); - - if (pin.battery_rx < 0 || pin.battery_tx < 0) { - MessageOutput.println("[Pylontech] Invalid pin config"); - return false; - } - - auto tx = static_cast(pin.battery_tx); - auto rx = static_cast(pin.battery_rx); - twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(tx, rx, TWAI_MODE_NORMAL); - - // Initialize configuration structures using macro initializers - twai_timing_config_t t_config = TWAI_TIMING_CONFIG_500KBITS(); - twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL(); - - // Install TWAI driver - esp_err_t twaiLastResult = twai_driver_install(&g_config, &t_config, &f_config); - switch (twaiLastResult) { - case ESP_OK: - MessageOutput.println("[Pylontech] Twai driver installed"); - break; - case ESP_ERR_INVALID_ARG: - MessageOutput.println("[Pylontech] Twai driver install - invalid arg"); - return false; - break; - case ESP_ERR_NO_MEM: - MessageOutput.println("[Pylontech] Twai driver install - no memory"); - return false; - break; - case ESP_ERR_INVALID_STATE: - MessageOutput.println("[Pylontech] Twai driver install - invalid state"); - return false; - break; - } - - // Start TWAI driver - twaiLastResult = twai_start(); - switch (twaiLastResult) { - case ESP_OK: - MessageOutput.println("[Pylontech] Twai driver started"); - break; - case ESP_ERR_INVALID_STATE: - MessageOutput.println("[Pylontech] Twai driver start - invalid state"); - return false; - break; - } - - return true; + return BatteryCanReceiver::init(verboseLogging, "Pylontech"); } -void PylontechCanReceiver::deinit() -{ - // Stop TWAI driver - esp_err_t twaiLastResult = twai_stop(); - switch (twaiLastResult) { - case ESP_OK: - MessageOutput.println("[Pylontech] Twai driver stopped"); - break; - case ESP_ERR_INVALID_STATE: - MessageOutput.println("[Pylontech] Twai driver stop - invalid state"); - break; - } - - // Uninstall TWAI driver - twaiLastResult = twai_driver_uninstall(); - switch (twaiLastResult) { - case ESP_OK: - MessageOutput.println("[Pylontech] Twai driver uninstalled"); - break; - case ESP_ERR_INVALID_STATE: - MessageOutput.println("[Pylontech] Twai driver uninstall - invalid state"); - break; - } -} -void PylontechCanReceiver::loop() +void PylontechCanReceiver::onMessage(twai_message_t rx_message) { -#ifdef PYLONTECH_DUMMY - return dummyData(); -#endif - - // Check for messages. twai_receive is blocking when there is no data so we return if there are no frames in the buffer - twai_status_info_t status_info; - esp_err_t twaiLastResult = twai_get_status_info(&status_info); - if (twaiLastResult != ESP_OK) { - switch (twaiLastResult) { - case ESP_ERR_INVALID_ARG: - MessageOutput.println("[Pylontech] Twai driver get status - invalid arg"); - break; - case ESP_ERR_INVALID_STATE: - MessageOutput.println("[Pylontech] Twai driver get status - invalid state"); - break; - } - return; - } - if (status_info.msgs_to_rx == 0) { - return; - } - - // Wait for message to be received, function is blocking - twai_message_t rx_message; - if (twai_receive(&rx_message, pdMS_TO_TICKS(100)) != ESP_OK) { - MessageOutput.println("[Pylontech] Failed to receive message"); - return; - } - switch (rx_message.identifier) { case 0x351: { _stats->_chargeVoltage = this->scaleValue(this->readUnsignedInt16(rx_message.data), 0.1); @@ -129,7 +20,7 @@ void PylontechCanReceiver::loop() _stats->_dischargeCurrentLimitation = this->scaleValue(this->readSignedInt16(rx_message.data + 4), 0.1); if (_verboseLogging) { - MessageOutput.printf("[Pylontech] chargeVoltage: %f chargeCurrentLimitation: %f dischargeCurrentLimitation: %f\n", + MessageOutput.printf("[Pylontech] chargeVoltage: %f chargeCurrentLimitation: %f dischargeCurrentLimitation: %f\r\n", _stats->_chargeVoltage, _stats->_chargeCurrentLimitation, _stats->_dischargeCurrentLimitation); } break; @@ -140,7 +31,7 @@ void PylontechCanReceiver::loop() _stats->_stateOfHealth = this->readUnsignedInt16(rx_message.data + 2); if (_verboseLogging) { - MessageOutput.printf("[Pylontech] soc: %d soh: %d\n", + MessageOutput.printf("[Pylontech] soc: %d soh: %d\r\n", _stats->getSoC(), _stats->_stateOfHealth); } break; @@ -148,12 +39,12 @@ void PylontechCanReceiver::loop() case 0x356: { _stats->setVoltage(this->scaleValue(this->readSignedInt16(rx_message.data), 0.01), millis()); - _stats->_current = this->scaleValue(this->readSignedInt16(rx_message.data + 2), 0.1); + _stats->setCurrent(this->scaleValue(this->readSignedInt16(rx_message.data + 2), 0.1), 1/*precision*/, millis()); _stats->_temperature = this->scaleValue(this->readSignedInt16(rx_message.data + 4), 0.1); if (_verboseLogging) { - MessageOutput.printf("[Pylontech] voltage: %f current: %f temperature: %f\n", - _stats->getVoltage(), _stats->_current, _stats->_temperature); + MessageOutput.printf("[Pylontech] voltage: %f current: %f temperature: %f\r\n", + _stats->getVoltage(), _stats->getChargeCurrent(), _stats->_temperature); } break; } @@ -171,7 +62,7 @@ void PylontechCanReceiver::loop() _stats->_alarmOverCurrentCharge = this->getBit(alarmBits, 0); if (_verboseLogging) { - MessageOutput.printf("[Pylontech] Alarms: %d %d %d %d %d %d %d\n", + MessageOutput.printf("[Pylontech] Alarms: %d %d %d %d %d %d %d\r\n", _stats->_alarmOverCurrentDischarge, _stats->_alarmUnderTemperature, _stats->_alarmOverTemperature, @@ -193,7 +84,7 @@ void PylontechCanReceiver::loop() _stats->_warningHighCurrentCharge = this->getBit(warningBits, 0); if (_verboseLogging) { - MessageOutput.printf("[Pylontech] Warnings: %d %d %d %d %d %d %d\n", + MessageOutput.printf("[Pylontech] Warnings: %d %d %d %d %d %d %d\r\n", _stats->_warningHighCurrentDischarge, _stats->_warningLowTemperature, _stats->_warningHighTemperature, @@ -212,7 +103,7 @@ void PylontechCanReceiver::loop() if (manufacturer.isEmpty()) { break; } if (_verboseLogging) { - MessageOutput.printf("[Pylontech] Manufacturer: %s\n", manufacturer.c_str()); + MessageOutput.printf("[Pylontech] Manufacturer: %s\r\n", manufacturer.c_str()); } _stats->setManufacturer(std::move(manufacturer)); @@ -226,7 +117,7 @@ void PylontechCanReceiver::loop() _stats->_chargeImmediately = this->getBit(chargeStatusBits, 5); if (_verboseLogging) { - MessageOutput.printf("[Pylontech] chargeStatusBits: %d %d %d\n", + MessageOutput.printf("[Pylontech] chargeStatusBits: %d %d %d\r\n", _stats->_chargeEnabled, _stats->_dischargeEnabled, _stats->_chargeImmediately); @@ -243,29 +134,7 @@ void PylontechCanReceiver::loop() _stats->setLastUpdate(millis()); } -uint16_t PylontechCanReceiver::readUnsignedInt16(uint8_t *data) -{ - uint8_t bytes[2]; - bytes[0] = *data; - bytes[1] = *(data + 1); - return (bytes[1] << 8) + bytes[0]; -} - -int16_t PylontechCanReceiver::readSignedInt16(uint8_t *data) -{ - return this->readUnsignedInt16(data); -} - -float PylontechCanReceiver::scaleValue(int16_t value, float factor) -{ - return value * factor; -} - -bool PylontechCanReceiver::getBit(uint8_t value, uint8_t bit) -{ - return (value & (1 << bit)) >> bit; -} - +// Currently not called because there is no nice way to integrate it right now #ifdef PYLONTECH_DUMMY void PylontechCanReceiver::dummyData() { @@ -288,7 +157,7 @@ void PylontechCanReceiver::dummyData() _stats->_dischargeCurrentLimitation = dummyFloat(12); _stats->_stateOfHealth = 99; _stats->setVoltage(48.67, millis()); - _stats->_current = dummyFloat(-1); + _stats->setCurrent(dummyFloat(-1), 1/*precision*/, millis()); _stats->_temperature = dummyFloat(20); _stats->_chargeEnabled = true; diff --git a/src/PytesCanReceiver.cpp b/src/PytesCanReceiver.cpp new file mode 100644 index 000000000..81c7c85cc --- /dev/null +++ b/src/PytesCanReceiver.cpp @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "PytesCanReceiver.h" +#include "MessageOutput.h" +#include "PinMapping.h" +#include +#include + +bool PytesCanReceiver::init(bool verboseLogging) +{ + return BatteryCanReceiver::init(verboseLogging, "Pytes"); +} + +void PytesCanReceiver::onMessage(twai_message_t rx_message) +{ + switch (rx_message.identifier) { + case 0x351: { + _stats->_chargeVoltageLimit = this->scaleValue(this->readUnsignedInt16(rx_message.data), 0.1); + _stats->_chargeCurrentLimit = this->scaleValue(this->readUnsignedInt16(rx_message.data + 2), 0.1); + _stats->_dischargeCurrentLimit = this->scaleValue(this->readUnsignedInt16(rx_message.data + 4), 0.1); + _stats->_dischargeVoltageLimit = this->scaleValue(this->readSignedInt16(rx_message.data + 6), 0.1); + + if (_verboseLogging) { + MessageOutput.printf("[Pytes] chargeVoltageLimit: %f chargeCurrentLimit: %f dischargeCurrentLimit: %f dischargeVoltageLimit: %f\r\n", + _stats->_chargeVoltageLimit, _stats->_chargeCurrentLimit, + _stats->_dischargeCurrentLimit, _stats->_dischargeVoltageLimit); + } + break; + } + + case 0x355: { + _stats->setSoC(static_cast(this->readUnsignedInt16(rx_message.data)), 0/*precision*/, millis()); + _stats->_stateOfHealth = this->readUnsignedInt16(rx_message.data + 2); + + if (_verboseLogging) { + MessageOutput.printf("[Pytes] soc: %d soh: %d\r\n", + _stats->getSoC(), _stats->_stateOfHealth); + } + break; + } + + case 0x356: { + _stats->setVoltage(this->scaleValue(this->readSignedInt16(rx_message.data), 0.01), millis()); + _stats->setCurrent(this->scaleValue(this->readSignedInt16(rx_message.data + 2), 0.1), 1/*precision*/, millis()); + _stats->_temperature = this->scaleValue(this->readSignedInt16(rx_message.data + 4), 0.1); + + if (_verboseLogging) { + MessageOutput.printf("[Pytes] voltage: %f current: %f temperature: %f\r\n", + _stats->getVoltage(), _stats->getChargeCurrent(), _stats->_temperature); + } + break; + } + + case 0x35A: { // Alarms and Warnings + uint16_t alarmBits = rx_message.data[0]; + _stats->_alarmOverVoltage = this->getBit(alarmBits, 2); + _stats->_alarmUnderVoltage = this->getBit(alarmBits, 4); + _stats->_alarmOverTemperature = this->getBit(alarmBits, 6); + + alarmBits = rx_message.data[1]; + _stats->_alarmUnderTemperature = this->getBit(alarmBits, 0); + _stats->_alarmOverTemperatureCharge = this->getBit(alarmBits, 2); + _stats->_alarmUnderTemperatureCharge = this->getBit(alarmBits, 4); + _stats->_alarmOverCurrentDischarge = this->getBit(alarmBits, 6); + + alarmBits = rx_message.data[2]; + _stats->_alarmOverCurrentCharge = this->getBit(alarmBits, 0); + _stats->_alarmInternalFailure = this->getBit(alarmBits, 6); + + alarmBits = rx_message.data[3]; + _stats->_alarmCellImbalance = this->getBit(alarmBits, 0); + + if (_verboseLogging) { + MessageOutput.printf("[Pytes] Alarms: %d %d %d %d %d %d %d %d %d %d\r\n", + _stats->_alarmOverVoltage, + _stats->_alarmUnderVoltage, + _stats->_alarmOverTemperature, + _stats->_alarmUnderTemperature, + _stats->_alarmOverTemperatureCharge, + _stats->_alarmUnderTemperatureCharge, + _stats->_alarmOverCurrentDischarge, + _stats->_alarmOverCurrentCharge, + _stats->_alarmInternalFailure, + _stats->_alarmCellImbalance); + } + + uint16_t warningBits = rx_message.data[4]; + _stats->_warningHighVoltage = this->getBit(warningBits, 2); + _stats->_warningLowVoltage = this->getBit(warningBits, 4); + _stats->_warningHighTemperature = this->getBit(warningBits, 6); + + warningBits = rx_message.data[5]; + _stats->_warningLowTemperature = this->getBit(warningBits, 0); + _stats->_warningHighTemperatureCharge = this->getBit(warningBits, 2); + _stats->_warningLowTemperatureCharge = this->getBit(warningBits, 4); + _stats->_warningHighDischargeCurrent = this->getBit(warningBits, 6); + + warningBits = rx_message.data[6]; + _stats->_warningHighChargeCurrent = this->getBit(warningBits, 0); + _stats->_warningInternalFailure = this->getBit(warningBits, 6); + + warningBits = rx_message.data[7]; + _stats->_warningCellImbalance = this->getBit(warningBits, 0); + + if (_verboseLogging) { + MessageOutput.printf("[Pytes] Warnings: %d %d %d %d %d %d %d %d %d %d\r\n", + _stats->_warningHighVoltage, + _stats->_warningLowVoltage, + _stats->_warningHighTemperature, + _stats->_warningLowTemperature, + _stats->_warningHighTemperatureCharge, + _stats->_warningLowTemperatureCharge, + _stats->_warningHighDischargeCurrent, + _stats->_warningHighChargeCurrent, + _stats->_warningInternalFailure, + _stats->_warningCellImbalance); + } + break; + } + + case 0x35E: { + String manufacturer(reinterpret_cast(rx_message.data), + rx_message.data_length_code); + + if (manufacturer.isEmpty()) { break; } + + if (_verboseLogging) { + MessageOutput.printf("[Pytes] Manufacturer: %s\r\n", manufacturer.c_str()); + } + + _stats->setManufacturer(std::move(manufacturer)); + break; + } + + case 0x35F: { // BatteryInfo + auto fwVersionPart1 = String(this->readUnsignedInt8(rx_message.data + 2)); + auto fwVersionPart2 = String(this->readUnsignedInt8(rx_message.data + 3)); + _stats->_fwversion = "v" + fwVersionPart1 + "." + fwVersionPart2; + + _stats->_availableCapacity = this->readUnsignedInt16(rx_message.data + 4); + + if (_verboseLogging) { + MessageOutput.printf("[Pytes] fwversion: %s availableCapacity: %d Ah\r\n", + _stats->_fwversion.c_str(), _stats->_availableCapacity); + } + break; + } + + case 0x372: { // BankInfo + _stats->_moduleCountOnline = this->readUnsignedInt16(rx_message.data); + _stats->_moduleCountBlockingCharge = this->readUnsignedInt16(rx_message.data + 2); + _stats->_moduleCountBlockingDischarge = this->readUnsignedInt16(rx_message.data + 4); + _stats->_moduleCountOffline = this->readUnsignedInt16(rx_message.data + 6); + + if (_verboseLogging) { + MessageOutput.printf("[Pytes] moduleCountOnline: %d moduleCountBlockingCharge: %d moduleCountBlockingDischarge: %d moduleCountOffline: %d\r\n", + _stats->_moduleCountOnline, _stats->_moduleCountBlockingCharge, + _stats->_moduleCountBlockingDischarge, _stats->_moduleCountOffline); + } + break; + } + + case 0x373: { // CellInfo + _stats->_cellMinMilliVolt = this->readUnsignedInt16(rx_message.data); + _stats->_cellMaxMilliVolt = this->readUnsignedInt16(rx_message.data + 2); + _stats->_cellMinTemperature = this->readUnsignedInt16(rx_message.data + 4) - 273; + _stats->_cellMaxTemperature = this->readUnsignedInt16(rx_message.data + 6) - 273; + + if (_verboseLogging) { + MessageOutput.printf("[Pytes] lowestCellMilliVolt: %d highestCellMilliVolt: %d minimumCellTemperature: %f maximumCellTemperature: %f\r\n", + _stats->_cellMinMilliVolt, _stats->_cellMaxMilliVolt, + _stats->_cellMinTemperature, _stats->_cellMaxTemperature); + } + break; + } + + case 0x374: { // Battery/Cell name (string) with "Lowest Cell Voltage" + String cellMinVoltageName(reinterpret_cast(rx_message.data), + rx_message.data_length_code); + + if (cellMinVoltageName.isEmpty()) { break; } + + if (_verboseLogging) { + MessageOutput.printf("[Pytes] cellMinVoltageName: %s\r\n", + cellMinVoltageName.c_str()); + } + + _stats->_cellMinVoltageName = cellMinVoltageName; + break; + } + + case 0x375: { // Battery/Cell name (string) with "Highest Cell Voltage" + String cellMaxVoltageName(reinterpret_cast(rx_message.data), + rx_message.data_length_code); + + if (cellMaxVoltageName.isEmpty()) { break; } + + if (_verboseLogging) { + MessageOutput.printf("[Pytes] cellMaxVoltageName: %s\r\n", + cellMaxVoltageName.c_str()); + } + + _stats->_cellMaxVoltageName = cellMaxVoltageName; + break; + } + + case 0x376: { // Battery/Cell name (string) with "Minimum Cell Temperature" + String cellMinTemperatureName(reinterpret_cast(rx_message.data), + rx_message.data_length_code); + + if (cellMinTemperatureName.isEmpty()) { break; } + + if (_verboseLogging) { + MessageOutput.printf("[Pytes] cellMinTemperatureName: %s\r\n", + cellMinTemperatureName.c_str()); + } + + _stats->_cellMinTemperatureName = cellMinTemperatureName; + break; + } + + case 0x377: { // Battery/Cell name (string) with "Maximum Cell Temperature" + String cellMaxTemperatureName(reinterpret_cast(rx_message.data), + rx_message.data_length_code); + + if (cellMaxTemperatureName.isEmpty()) { break; } + + if (_verboseLogging) { + MessageOutput.printf("[Pytes] cellMaxTemperatureName: %s\r\n", + cellMaxTemperatureName.c_str()); + } + + _stats->_cellMaxTemperatureName = cellMaxTemperatureName; + break; + } + + case 0x378: { // History: Charged / Discharged Energy + _stats->_chargedEnergy = this->scaleValue(this->readUnsignedInt32(rx_message.data), 0.1); + _stats->_dischargedEnergy = this->scaleValue(this->readUnsignedInt32(rx_message.data + 4), 0.1); + + if (_verboseLogging) { + MessageOutput.printf("[Pytes] chargedEnergy: %f dischargedEnergy: %f\r\n", + _stats->_chargedEnergy, _stats->_dischargedEnergy); + } + break; + } + + case 0x379: { // BatterySize: Installed Ah + _stats->_totalCapacity = this->readUnsignedInt16(rx_message.data); + + if (_verboseLogging) { + MessageOutput.printf("[Pytes] totalCapacity: %d Ah\r\n", + _stats->_totalCapacity); + } + break; + } + + case 0x380: { // Serialnumber - part 1 + String snPart1(reinterpret_cast(rx_message.data), + rx_message.data_length_code); + + if (snPart1.isEmpty() || !isgraph(snPart1.charAt(0))) { break; } + + if (_verboseLogging) { + MessageOutput.printf("[Pytes] snPart1: %s\r\n", snPart1.c_str()); + } + + _stats->_serialPart1 = snPart1; + _stats->updateSerial(); + break; + } + + case 0x381: { // Serialnumber - part 2 + String snPart2(reinterpret_cast(rx_message.data), + rx_message.data_length_code); + + if (snPart2.isEmpty() || !isgraph(snPart2.charAt(0))) { break; } + + if (_verboseLogging) { + MessageOutput.printf("[Pytes] snPart2: %s\r\n", snPart2.c_str()); + } + + _stats->_serialPart2 = snPart2; + _stats->updateSerial(); + break; + } + + default: + return; // do not update last update timestamp + break; + } + + _stats->setLastUpdate(millis()); +} diff --git a/src/Utils.cpp b/src/Utils.cpp index 6bedd2cbd..c2e40885b 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -1,12 +1,12 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2022 - 2023 Thomas Basler and others + * Copyright (C) 2022 - 2024 Thomas Basler and others */ + #include "Utils.h" #include "Display_Graphic.h" #include "Led_Single.h" #include "MessageOutput.h" -#include "PinMapping.h" #include #include @@ -92,3 +92,158 @@ void Utils::removeAllFiles() file = root.getNextFileName(); } } + +/* OpenDTU-OnBatter-specific utils go here: */ +template +std::optional getFromString(char const* val); + +template<> +std::optional getFromString(char const* val) +{ + float res = 0; + + try { + res = std::stof(val); + } + catch (std::invalid_argument const& e) { + return std::nullopt; + } + + return res; +} + +template +char const* getTypename(); + +template<> +char const* getTypename() { return "float"; } + +template +std::pair Utils::getJsonValueByPath(JsonDocument const& root, String const& path) +{ + size_t constexpr kErrBufferSize = 256; + char errBuffer[kErrBufferSize]; + constexpr char delimiter = '/'; + int start = 0; + int end = path.indexOf(delimiter); + auto value = root.as(); + + // NOTE: "Because ArduinoJson implements the Null Object Pattern, it is + // always safe to read the object: if the key doesn't exist, it returns an + // empty value." + auto getNext = [&](String const& key) -> bool { + // handle double forward slashes and paths starting or ending with a slash + if (key.isEmpty()) { return true; } + + if (key[0] == '[' && key[key.length() - 1] == ']') { + if (!value.is()) { + snprintf(errBuffer, kErrBufferSize, "Cannot access non-array " + "JSON node using array index '%s' (JSON path '%s', " + "position %i)", key.c_str(), path.c_str(), start); + return false; + } + + auto idx = key.substring(1, key.length() - 1).toInt(); + value = value[idx]; + + if (value.isNull()) { + snprintf(errBuffer, kErrBufferSize, "Unable to access JSON " + "array index %li (JSON path '%s', position %i)", + idx, path.c_str(), start); + return false; + } + + return true; + } + + value = value[key]; + + if (value.isNull()) { + snprintf(errBuffer, kErrBufferSize, "Unable to access JSON key " + "'%s' (JSON path '%s', position %i)", + key.c_str(), path.c_str(), start); + return false; + } + + return true; + }; + + while (end != -1) { + if (!getNext(path.substring(start, end))) { + return { T(), String(errBuffer) }; + } + start = end + 1; + end = path.indexOf(delimiter, start); + } + + if (!getNext(path.substring(start))) { + return { T(), String(errBuffer) }; + } + + if (value.is()) { + return { value.as(), "" }; + } + + if (!value.is()) { + snprintf(errBuffer, kErrBufferSize, "Value '%s' at JSON path '%s' is " + "neither a string nor of type %s", value.as().c_str(), + path.c_str(), getTypename()); + return { T(), String(errBuffer) }; + } + + auto res = getFromString(value.as()); + if (!res.has_value()) { + snprintf(errBuffer, kErrBufferSize, "String '%s' at JSON path '%s' cannot " + "be converted to %s", value.as().c_str(), path.c_str(), + getTypename()); + return { T(), String(errBuffer) }; + } + + return { *res, "" }; +} + +template std::pair Utils::getJsonValueByPath(JsonDocument const& root, String const& path); + +template +std::optional Utils::getNumericValueFromMqttPayload(char const* client, + std::string const& src, char const* topic, char const* jsonPath) +{ + std::string logValue = src.substr(0, 32); + if (src.length() > logValue.length()) { logValue += "..."; } + + auto log = [client,topic](char const* format, auto&&... args) -> std::optional { + MessageOutput.printf("[%s] Topic '%s': ", client, topic); + MessageOutput.printf(format, args...); + MessageOutput.println(); + return std::nullopt; + }; + + if (strlen(jsonPath) == 0) { + auto res = getFromString(src.c_str()); + if (!res.has_value()) { + return log("cannot parse payload '%s' as float", logValue.c_str()); + } + return res; + } + + JsonDocument json; + + const DeserializationError error = deserializeJson(json, src); + if (error) { + return log("cannot parse payload '%s' as JSON", logValue.c_str()); + } + + if (json.overflowed()) { + return log("payload too large to process as JSON"); + } + + auto pathResolutionResult = getJsonValueByPath(json, jsonPath); + if (!pathResolutionResult.second.isEmpty()) { + return log("%s", pathResolutionResult.second.c_str()); + } + + return pathResolutionResult.first; +} + +template std::optional Utils::getNumericValueFromMqttPayload(char const* client, + std::string const& src, char const* topic, char const* jsonPath); diff --git a/src/VictronMppt.cpp b/src/VictronMppt.cpp index bc0122d40..4e084974c 100644 --- a/src/VictronMppt.cpp +++ b/src/VictronMppt.cpp @@ -75,12 +75,16 @@ void VictronMpptClass::loop() } } +/* + * isDataValid() + * return: true = if at least one of the MPPT controllers delivers valid data + */ bool VictronMpptClass::isDataValid() const { std::lock_guard lock(_mutex); for (auto const& upController: _controllers) { - if (!upController->isDataValid()) { return false; } + if (upController->isDataValid()) { return true; } } return !_controllers.empty(); diff --git a/src/WebApi_Huawei.cpp b/src/WebApi_Huawei.cpp index da6b38b39..d2f1d2e10 100644 --- a/src/WebApi_Huawei.cpp +++ b/src/WebApi_Huawei.cpp @@ -1,259 +1,256 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * Copyright (C) 2022-2024 Thomas Basler and others - */ -#include "WebApi_Huawei.h" -#include "Huawei_can.h" -#include "Configuration.h" -#include "MessageOutput.h" -#include "PinMapping.h" -#include "WebApi.h" -#include "WebApi_errors.h" -#include -#include - -void WebApiHuaweiClass::init(AsyncWebServer& server, Scheduler& scheduler) -{ - using std::placeholders::_1; - - _server = &server; - - _server->on("/api/huawei/status", HTTP_GET, std::bind(&WebApiHuaweiClass::onStatus, this, _1)); - _server->on("/api/huawei/config", HTTP_GET, std::bind(&WebApiHuaweiClass::onAdminGet, this, _1)); - _server->on("/api/huawei/config", HTTP_POST, std::bind(&WebApiHuaweiClass::onAdminPost, this, _1)); - _server->on("/api/huawei/limit/config", HTTP_POST, std::bind(&WebApiHuaweiClass::onPost, this, _1)); -} - -void WebApiHuaweiClass::getJsonData(JsonVariant& root) { - const RectifierParameters_t * rp = HuaweiCan.get(); - - root["data_age"] = (millis() - HuaweiCan.getLastUpdate()) / 1000; - root["input_voltage"]["v"] = rp->input_voltage; - root["input_voltage"]["u"] = "V"; - root["input_current"]["v"] = rp->input_current; - root["input_current"]["u"] = "A"; - root["input_power"]["v"] = rp->input_power; - root["input_power"]["u"] = "W"; - root["output_voltage"]["v"] = rp->output_voltage; - root["output_voltage"]["u"] = "V"; - root["output_current"]["v"] = rp->output_current; - root["output_current"]["u"] = "A"; - root["max_output_current"]["v"] = rp->max_output_current; - root["max_output_current"]["u"] = "A"; - root["output_power"]["v"] = rp->output_power; - root["output_power"]["u"] = "W"; - root["input_temp"]["v"] = rp->input_temp; - root["input_temp"]["u"] = "°C"; - root["output_temp"]["v"] = rp->output_temp; - root["output_temp"]["u"] = "°C"; - root["efficiency"]["v"] = rp->efficiency * 100; - root["efficiency"]["u"] = "%"; - -} - -void WebApiHuaweiClass::onStatus(AsyncWebServerRequest* request) -{ - if (!WebApi.checkCredentialsReadonly(request)) { - return; - } - - AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& root = response->getRoot(); - getJsonData(root); - - response->setLength(); - request->send(response); -} - -void WebApiHuaweiClass::onPost(AsyncWebServerRequest* request) -{ - if (!WebApi.checkCredentials(request)) { - return; - } - - AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonDocument root; - if (!WebApi.parseRequestData(request, response, root)) { - return; - } - - float value; - uint8_t online = true; - float minimal_voltage; - - auto& retMsg = response->getRoot(); - - if (root.containsKey("online")) { - online = root["online"].as(); - if (online) { - minimal_voltage = HUAWEI_MINIMAL_ONLINE_VOLTAGE; - } else { - minimal_voltage = HUAWEI_MINIMAL_OFFLINE_VOLTAGE; - } - } else { - retMsg["message"] = "Could not read info if data should be set for online/offline operation!"; - retMsg["code"] = WebApiError::LimitInvalidType; - response->setLength(); - request->send(response); - return; - } - - if (root.containsKey("voltage_valid")) { - if (root["voltage_valid"].as()) { - if (root["voltage"].as() < minimal_voltage || root["voltage"].as() > 58) { - retMsg["message"] = "voltage not in range between 42 (online)/48 (offline and 58V !"; - retMsg["code"] = WebApiError::LimitInvalidLimit; - retMsg["param"]["max"] = 58; - retMsg["param"]["min"] = minimal_voltage; - response->setLength(); - request->send(response); - return; - } else { - value = root["voltage"].as(); - if (online) { - HuaweiCan.setValue(value, HUAWEI_ONLINE_VOLTAGE); - } else { - HuaweiCan.setValue(value, HUAWEI_OFFLINE_VOLTAGE); - } - } - } - } - - if (root.containsKey("current_valid")) { - if (root["current_valid"].as()) { - if (root["current"].as() < 0 || root["current"].as() > 60) { - retMsg["message"] = "current must be in range between 0 and 60!"; - retMsg["code"] = WebApiError::LimitInvalidLimit; - retMsg["param"]["max"] = 60; - retMsg["param"]["min"] = 0; - response->setLength(); - request->send(response); - return; - } else { - value = root["current"].as(); - if (online) { - HuaweiCan.setValue(value, HUAWEI_ONLINE_CURRENT); - } else { - HuaweiCan.setValue(value, HUAWEI_OFFLINE_CURRENT); - } - } - } - } - - WebApi.writeConfig(retMsg); - - WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); -} - - - - -void WebApiHuaweiClass::onAdminGet(AsyncWebServerRequest* request) -{ - if (!WebApi.checkCredentialsReadonly(request)) { - return; - } - - AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& root = response->getRoot(); - 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); -} - -void WebApiHuaweiClass::onAdminPost(AsyncWebServerRequest* request) -{ - if (!WebApi.checkCredentials(request)) { - return; - } - - AsyncJsonResponse* response = new AsyncJsonResponse(); - JsonDocument root; - if (!WebApi.parseRequestData(request, response, root)) { - 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"))) { - retMsg["message"] = "Values are missing!"; - retMsg["code"] = WebApiError::GenericValueMissing; - response->setLength(); - request->send(response); - return; - } - - 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); - - 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 - // changes, the ESP must restart for configuration changes to take effect. - yield(); - delay(1000); - yield(); - ESP.restart(); - - const PinMapping_t& pin = PinMapping.get(); - // Properly turn this on - if (config.Huawei.Enabled) { - MessageOutput.println("Initialize Huawei AC charger interface... "); - if (PinMapping.isValidHuaweiConfig()) { - MessageOutput.printf("Huawei AC-charger miso = %d, mosi = %d, clk = %d, irq = %d, cs = %d, power_pin = %d\r\n", pin.huawei_miso, pin.huawei_mosi, pin.huawei_clk, pin.huawei_irq, pin.huawei_cs, pin.huawei_power); - HuaweiCan.updateSettings(pin.huawei_miso, pin.huawei_mosi, pin.huawei_clk, pin.huawei_irq, pin.huawei_cs, pin.huawei_power); - MessageOutput.println("done"); - } else { - MessageOutput.println("Invalid pin config"); - } - } - - // Properly turn this off - if (!config.Huawei.Enabled) { - HuaweiCan.setValue(0, HUAWEI_ONLINE_CURRENT); - delay(500); - HuaweiCan.setMode(HUAWEI_MODE_OFF); - return; - } - - if (config.Huawei.Auto_Power_Enabled) { - HuaweiCan.setMode(HUAWEI_MODE_AUTO_INT); - return; - } - - HuaweiCan.setMode(HUAWEI_MODE_AUTO_EXT); -} +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022-2024 Thomas Basler and others + */ +#include "WebApi_Huawei.h" +#include "Huawei_can.h" +#include "Configuration.h" +#include "MessageOutput.h" +#include "PinMapping.h" +#include "WebApi.h" +#include "WebApi_errors.h" +#include +#include + +void WebApiHuaweiClass::init(AsyncWebServer& server, Scheduler& scheduler) +{ + using std::placeholders::_1; + + _server = &server; + + _server->on("/api/huawei/status", HTTP_GET, std::bind(&WebApiHuaweiClass::onStatus, this, _1)); + _server->on("/api/huawei/config", HTTP_GET, std::bind(&WebApiHuaweiClass::onAdminGet, this, _1)); + _server->on("/api/huawei/config", HTTP_POST, std::bind(&WebApiHuaweiClass::onAdminPost, this, _1)); + _server->on("/api/huawei/limit/config", HTTP_POST, std::bind(&WebApiHuaweiClass::onPost, this, _1)); +} + +void WebApiHuaweiClass::getJsonData(JsonVariant& root) { + const RectifierParameters_t * rp = HuaweiCan.get(); + + root["data_age"] = (millis() - HuaweiCan.getLastUpdate()) / 1000; + root["input_voltage"]["v"] = rp->input_voltage; + root["input_voltage"]["u"] = "V"; + root["input_current"]["v"] = rp->input_current; + root["input_current"]["u"] = "A"; + root["input_power"]["v"] = rp->input_power; + root["input_power"]["u"] = "W"; + root["output_voltage"]["v"] = rp->output_voltage; + root["output_voltage"]["u"] = "V"; + root["output_current"]["v"] = rp->output_current; + root["output_current"]["u"] = "A"; + root["max_output_current"]["v"] = rp->max_output_current; + root["max_output_current"]["u"] = "A"; + root["output_power"]["v"] = rp->output_power; + root["output_power"]["u"] = "W"; + root["input_temp"]["v"] = rp->input_temp; + root["input_temp"]["u"] = "°C"; + root["output_temp"]["v"] = rp->output_temp; + root["output_temp"]["u"] = "°C"; + root["efficiency"]["v"] = rp->efficiency * 100; + root["efficiency"]["u"] = "%"; + +} + +void WebApiHuaweiClass::onStatus(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentialsReadonly(request)) { + return; + } + + AsyncJsonResponse* response = new AsyncJsonResponse(); + auto& root = response->getRoot(); + getJsonData(root); + + response->setLength(); + request->send(response); +} + +void WebApiHuaweiClass::onPost(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentials(request)) { + return; + } + + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { + return; + } + + float value; + uint8_t online = true; + float minimal_voltage; + + auto& retMsg = response->getRoot(); + + if (root.containsKey("online")) { + online = root["online"].as(); + if (online) { + minimal_voltage = HUAWEI_MINIMAL_ONLINE_VOLTAGE; + } else { + minimal_voltage = HUAWEI_MINIMAL_OFFLINE_VOLTAGE; + } + } else { + retMsg["message"] = "Could not read info if data should be set for online/offline operation!"; + retMsg["code"] = WebApiError::LimitInvalidType; + response->setLength(); + request->send(response); + return; + } + + if (root.containsKey("voltage_valid")) { + if (root["voltage_valid"].as()) { + if (root["voltage"].as() < minimal_voltage || root["voltage"].as() > 58) { + retMsg["message"] = "voltage not in range between 42 (online)/48 (offline and 58V !"; + retMsg["code"] = WebApiError::LimitInvalidLimit; + retMsg["param"]["max"] = 58; + retMsg["param"]["min"] = minimal_voltage; + response->setLength(); + request->send(response); + return; + } else { + value = root["voltage"].as(); + if (online) { + HuaweiCan.setValue(value, HUAWEI_ONLINE_VOLTAGE); + } else { + HuaweiCan.setValue(value, HUAWEI_OFFLINE_VOLTAGE); + } + } + } + } + + if (root.containsKey("current_valid")) { + if (root["current_valid"].as()) { + if (root["current"].as() < 0 || root["current"].as() > 60) { + retMsg["message"] = "current must be in range between 0 and 60!"; + retMsg["code"] = WebApiError::LimitInvalidLimit; + retMsg["param"]["max"] = 60; + retMsg["param"]["min"] = 0; + response->setLength(); + request->send(response); + return; + } else { + value = root["current"].as(); + if (online) { + HuaweiCan.setValue(value, HUAWEI_ONLINE_CURRENT); + } else { + HuaweiCan.setValue(value, HUAWEI_OFFLINE_CURRENT); + } + } + } + } + + WebApi.writeConfig(retMsg); + + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); +} + +void WebApiHuaweiClass::onAdminGet(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentials(request)) { + return; + } + + AsyncJsonResponse* response = new AsyncJsonResponse(); + auto& root = response->getRoot(); + 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); +} + +void WebApiHuaweiClass::onAdminPost(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentials(request)) { + return; + } + + AsyncJsonResponse* response = new AsyncJsonResponse(); + JsonDocument root; + if (!WebApi.parseRequestData(request, response, root)) { + 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"))) { + retMsg["message"] = "Values are missing!"; + retMsg["code"] = WebApiError::GenericValueMissing; + response->setLength(); + request->send(response); + return; + } + + 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); + + 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 + // changes, the ESP must restart for configuration changes to take effect. + yield(); + delay(1000); + yield(); + ESP.restart(); + + const PinMapping_t& pin = PinMapping.get(); + // Properly turn this on + if (config.Huawei.Enabled) { + MessageOutput.println("Initialize Huawei AC charger interface... "); + if (PinMapping.isValidHuaweiConfig()) { + MessageOutput.printf("Huawei AC-charger miso = %d, mosi = %d, clk = %d, irq = %d, cs = %d, power_pin = %d\r\n", pin.huawei_miso, pin.huawei_mosi, pin.huawei_clk, pin.huawei_irq, pin.huawei_cs, pin.huawei_power); + HuaweiCan.updateSettings(pin.huawei_miso, pin.huawei_mosi, pin.huawei_clk, pin.huawei_irq, pin.huawei_cs, pin.huawei_power); + MessageOutput.println("done"); + } else { + MessageOutput.println("Invalid pin config"); + } + } + + // Properly turn this off + if (!config.Huawei.Enabled) { + HuaweiCan.setValue(0, HUAWEI_ONLINE_CURRENT); + delay(500); + HuaweiCan.setMode(HUAWEI_MODE_OFF); + return; + } + + if (config.Huawei.Auto_Power_Enabled) { + HuaweiCan.setMode(HUAWEI_MODE_AUTO_INT); + return; + } + + HuaweiCan.setMode(HUAWEI_MODE_AUTO_EXT); +} diff --git a/src/WebApi_battery.cpp b/src/WebApi_battery.cpp index 798957d3b..aa8040d73 100644 --- a/src/WebApi_battery.cpp +++ b/src/WebApi_battery.cpp @@ -30,7 +30,7 @@ void WebApiBatteryClass::onStatus(AsyncWebServerRequest* request) if (!WebApi.checkCredentialsReadonly(request)) { return; } - + AsyncJsonResponse* response = new AsyncJsonResponse(); auto& root = response->getRoot(); const CONFIG_T& config = Configuration.get(); @@ -41,7 +41,10 @@ void WebApiBatteryClass::onStatus(AsyncWebServerRequest* request) root["jkbms_interface"] = config.Battery.JkBmsInterface; root["jkbms_polling_interval"] = config.Battery.JkBmsPollingInterval; root["mqtt_soc_topic"] = config.Battery.MqttSocTopic; + root["mqtt_soc_json_path"] = config.Battery.MqttSocJsonPath; root["mqtt_voltage_topic"] = config.Battery.MqttVoltageTopic; + root["mqtt_voltage_json_path"] = config.Battery.MqttVoltageJsonPath; + root["mqtt_voltage_unit"] = config.Battery.MqttVoltageUnit; response->setLength(); request->send(response); @@ -49,6 +52,10 @@ void WebApiBatteryClass::onStatus(AsyncWebServerRequest* request) void WebApiBatteryClass::onAdminGet(AsyncWebServerRequest* request) { + if (!WebApi.checkCredentials(request)) { + return; + } + onStatus(request); } @@ -80,7 +87,10 @@ void WebApiBatteryClass::onAdminPost(AsyncWebServerRequest* request) config.Battery.JkBmsInterface = root["jkbms_interface"].as(); config.Battery.JkBmsPollingInterval = root["jkbms_polling_interval"].as(); strlcpy(config.Battery.MqttSocTopic, root["mqtt_soc_topic"].as().c_str(), sizeof(config.Battery.MqttSocTopic)); + strlcpy(config.Battery.MqttSocJsonPath, root["mqtt_soc_json_path"].as().c_str(), sizeof(config.Battery.MqttSocJsonPath)); strlcpy(config.Battery.MqttVoltageTopic, root["mqtt_voltage_topic"].as().c_str(), sizeof(config.Battery.MqttVoltageTopic)); + strlcpy(config.Battery.MqttVoltageJsonPath, root["mqtt_voltage_json_path"].as().c_str(), sizeof(config.Battery.MqttVoltageJsonPath)); + config.Battery.MqttVoltageUnit = static_cast(root["mqtt_voltage_unit"].as()); WebApi.writeConfig(retMsg); diff --git a/src/WebApi_firmware.cpp b/src/WebApi_firmware.cpp index 9491f935d..6988b7fc9 100644 --- a/src/WebApi_firmware.cpp +++ b/src/WebApi_firmware.cpp @@ -9,6 +9,7 @@ #include "WebApi.h" #include "helper.h" #include +#include "esp_partition.h" void WebApiFirmwareClass::init(AsyncWebServer& server, Scheduler& scheduler) { @@ -22,6 +23,15 @@ void WebApiFirmwareClass::init(AsyncWebServer& server, Scheduler& scheduler) server.on("/api/firmware/update", HTTP_POST, std::bind(&WebApiFirmwareClass::onFirmwareUpdateFinish, this, _1), std::bind(&WebApiFirmwareClass::onFirmwareUpdateUpload, this, _1, _2, _3, _4, _5, _6)); + + server.on("/api/firmware/status", HTTP_GET, std::bind(&WebApiFirmwareClass::onFirmwareStatus, this, _1)); +} + +bool WebApiFirmwareClass::otaSupported() const +{ + const esp_partition_t* pOtaPartition = esp_partition_find_first( + ESP_PARTITION_TYPE_APP, ESP_PARTITION_SUBTYPE_APP_OTA_1, NULL); + return (pOtaPartition != nullptr); } void WebApiFirmwareClass::onFirmwareUpdateFinish(AsyncWebServerRequest* request) @@ -46,6 +56,10 @@ void WebApiFirmwareClass::onFirmwareUpdateUpload(AsyncWebServerRequest* request, return; } + if (!otaSupported()) { + return request->send(500, "text/plain", "OTA updates not supported"); + } + // Upload handler chunks in data if (!index) { if (!request->hasParam("MD5", true)) { @@ -78,3 +92,17 @@ void WebApiFirmwareClass::onFirmwareUpdateUpload(AsyncWebServerRequest* request, return; } } + +void WebApiFirmwareClass::onFirmwareStatus(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentialsReadonly(request)) { + return; + } + + AsyncJsonResponse* response = new AsyncJsonResponse(); + auto& root = response->getRoot(); + + root["ota_supported"] = otaSupported(); + + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); +} diff --git a/src/WebApi_inverter.cpp b/src/WebApi_inverter.cpp index 2d9a56344..5a8585f70 100644 --- a/src/WebApi_inverter.cpp +++ b/src/WebApi_inverter.cpp @@ -55,6 +55,7 @@ void WebApiInverterClass::onInverterList(AsyncWebServerRequest* request) obj["reachable_threshold"] = config.Inverter[i].ReachableThreshold; obj["zero_runtime"] = config.Inverter[i].ZeroRuntimeDataIfUnrechable; obj["zero_day"] = config.Inverter[i].ZeroYieldDayOnMidnight; + obj["clear_eventlog"] = config.Inverter[i].ClearEventlogOnMidnight; obj["yieldday_correction"] = config.Inverter[i].YieldDayCorrection; auto inv = Hoymiles.getInverterBySerial(config.Inverter[i].Serial); @@ -213,20 +214,21 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) inverter.Serial = new_serial; strncpy(inverter.Name, root["name"].as().c_str(), INV_MAX_NAME_STRLEN); + inverter.Poll_Enable = root["poll_enable"] | true; + inverter.Poll_Enable_Night = root["poll_enable_night"] | true; + inverter.Command_Enable = root["command_enable"] | true; + inverter.Command_Enable_Night = root["command_enable_night"] | true; + inverter.ReachableThreshold = root["reachable_threshold"] | REACHABLE_THRESHOLD; + inverter.ZeroRuntimeDataIfUnrechable = root["zero_runtime"] | false; + inverter.ZeroYieldDayOnMidnight = root["zero_day"] | false; + inverter.ClearEventlogOnMidnight = root["clear_eventlog"] | false; + inverter.YieldDayCorrection = root["yieldday_correction"] | false; + uint8_t arrayCount = 0; for (JsonVariant channel : channelArray) { inverter.channel[arrayCount].MaxChannelPower = channel["max_power"].as(); inverter.channel[arrayCount].YieldTotalOffset = channel["yield_total_offset"].as(); strncpy(inverter.channel[arrayCount].Name, channel["name"] | "", sizeof(inverter.channel[arrayCount].Name)); - inverter.Poll_Enable = root["poll_enable"] | true; - inverter.Poll_Enable_Night = root["poll_enable_night"] | true; - inverter.Command_Enable = root["command_enable"] | true; - inverter.Command_Enable_Night = root["command_enable_night"] | true; - inverter.ReachableThreshold = root["reachable_threshold"] | REACHABLE_THRESHOLD; - inverter.ZeroRuntimeDataIfUnrechable = root["zero_runtime"] | false; - inverter.ZeroYieldDayOnMidnight = root["zero_day"] | false; - inverter.YieldDayCorrection = root["yieldday_correction"] | false; - arrayCount++; } @@ -254,6 +256,7 @@ void WebApiInverterClass::onInverterEdit(AsyncWebServerRequest* request) inv->setReachableThreshold(inverter.ReachableThreshold); inv->setZeroValuesIfUnreachable(inverter.ZeroRuntimeDataIfUnrechable); inv->setZeroYieldDayOnMidnight(inverter.ZeroYieldDayOnMidnight); + inv->setClearEventlogOnMidnight(inverter.ClearEventlogOnMidnight); inv->Statistics()->setYieldDayCorrection(inverter.YieldDayCorrection); for (uint8_t c = 0; c < INV_MAX_CHAN_COUNT; c++) { inv->Statistics()->setStringMaxPower(c, inverter.channel[c].MaxChannelPower); diff --git a/src/WebApi_limit.cpp b/src/WebApi_limit.cpp index 9a622deae..6a6c90ca4 100644 --- a/src/WebApi_limit.cpp +++ b/src/WebApi_limit.cpp @@ -83,7 +83,7 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) return; } - if (root["limit_value"].as() > MAX_INVERTER_LIMIT) { + if (root["limit_value"].as() > MAX_INVERTER_LIMIT) { retMsg["message"] = "Limit must between 0 and " STR(MAX_INVERTER_LIMIT) "!"; retMsg["code"] = WebApiError::LimitInvalidLimit; retMsg["param"]["max"] = MAX_INVERTER_LIMIT; @@ -102,7 +102,7 @@ void WebApiLimitClass::onLimitPost(AsyncWebServerRequest* request) return; } - uint16_t limit = root["limit_value"].as(); + float limit = root["limit_value"].as(); PowerLimitControlType type = root["limit_type"].as(); auto inv = Hoymiles.getInverterBySerial(serial); diff --git a/src/WebApi_mqtt.cpp b/src/WebApi_mqtt.cpp index a032a34db..4f76256df 100644 --- a/src/WebApi_mqtt.cpp +++ b/src/WebApi_mqtt.cpp @@ -5,6 +5,7 @@ #include "WebApi_mqtt.h" #include "Configuration.h" #include "MqttHandleHass.h" +#include "MqttHandleInverter.h" #include "MqttHandleVedirectHass.h" #include "MqttHandleVedirect.h" #include "MqttSettings.h" @@ -38,6 +39,7 @@ void WebApiMqttClass::onMqttStatus(AsyncWebServerRequest* request) root["mqtt_verbose_logging"] = config.Mqtt.VerboseLogging; root["mqtt_hostname"] = config.Mqtt.Hostname; root["mqtt_port"] = config.Mqtt.Port; + root["mqtt_clientid"] = MqttSettings.getClientId(); root["mqtt_username"] = config.Mqtt.Username; root["mqtt_topic"] = config.Mqtt.Topic; root["mqtt_connected"] = MqttSettings.getConnected(); @@ -72,6 +74,7 @@ void WebApiMqttClass::onMqttAdminGet(AsyncWebServerRequest* request) root["mqtt_verbose_logging"] = config.Mqtt.VerboseLogging; root["mqtt_hostname"] = config.Mqtt.Hostname; root["mqtt_port"] = config.Mqtt.Port; + root["mqtt_clientid"] = config.Mqtt.ClientId; root["mqtt_username"] = config.Mqtt.Username; root["mqtt_password"] = config.Mqtt.Password; root["mqtt_topic"] = config.Mqtt.Topic; @@ -82,7 +85,7 @@ void WebApiMqttClass::onMqttAdminGet(AsyncWebServerRequest* request) root["mqtt_client_cert"] = config.Mqtt.Tls.ClientCert; root["mqtt_client_key"] = config.Mqtt.Tls.ClientKey; root["mqtt_lwt_topic"] = config.Mqtt.Lwt.Topic; - root["mqtt_lwt_online"] = config.Mqtt.Lwt.Value_Online;; + root["mqtt_lwt_online"] = config.Mqtt.Lwt.Value_Online; root["mqtt_lwt_offline"] = config.Mqtt.Lwt.Value_Offline; root["mqtt_lwt_qos"] = config.Mqtt.Lwt.Qos; root["mqtt_publish_interval"] = config.Mqtt.PublishInterval; @@ -114,6 +117,7 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) && root.containsKey("mqtt_verbose_logging") && root.containsKey("mqtt_hostname") && root.containsKey("mqtt_port") + && root.containsKey("mqtt_clientid") && root.containsKey("mqtt_username") && root.containsKey("mqtt_password") && root.containsKey("mqtt_topic") @@ -148,6 +152,13 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) return; } + if (root["mqtt_clientid"].as().length() > MQTT_MAX_CLIENTID_STRLEN) { + retMsg["message"] = "Client ID must not be longer than " STR(MQTT_MAX_CLIENTID_STRLEN) " characters!"; + retMsg["code"] = WebApiError::MqttClientIdLength; + retMsg["param"]["max"] = MQTT_MAX_CLIENTID_STRLEN; + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); + return; + } if (root["mqtt_username"].as().length() > MQTT_MAX_USERNAME_STRLEN) { retMsg["message"] = "Username must not be longer than " STR(MQTT_MAX_USERNAME_STRLEN) " characters!"; retMsg["code"] = WebApiError::MqttUsernameLength; @@ -278,9 +289,9 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) strlcpy(config.Mqtt.Tls.ClientKey, root["mqtt_client_key"].as().c_str(), sizeof(config.Mqtt.Tls.ClientKey)); config.Mqtt.Port = root["mqtt_port"].as(); strlcpy(config.Mqtt.Hostname, root["mqtt_hostname"].as().c_str(), sizeof(config.Mqtt.Hostname)); + strlcpy(config.Mqtt.ClientId, root["mqtt_clientid"].as().c_str(), sizeof(config.Mqtt.ClientId)); strlcpy(config.Mqtt.Username, root["mqtt_username"].as().c_str(), sizeof(config.Mqtt.Username)); strlcpy(config.Mqtt.Password, root["mqtt_password"].as().c_str(), sizeof(config.Mqtt.Password)); - strlcpy(config.Mqtt.Topic, root["mqtt_topic"].as().c_str(), sizeof(config.Mqtt.Topic)); strlcpy(config.Mqtt.Lwt.Topic, root["mqtt_lwt_topic"].as().c_str(), sizeof(config.Mqtt.Lwt.Topic)); strlcpy(config.Mqtt.Lwt.Value_Online, root["mqtt_lwt_online"].as().c_str(), sizeof(config.Mqtt.Lwt.Value_Online)); strlcpy(config.Mqtt.Lwt.Value_Offline, root["mqtt_lwt_offline"].as().c_str(), sizeof(config.Mqtt.Lwt.Value_Offline)); @@ -293,6 +304,13 @@ void WebApiMqttClass::onMqttAdminPost(AsyncWebServerRequest* request) config.Mqtt.Hass.IndividualPanels = root["mqtt_hass_individualpanels"].as(); strlcpy(config.Mqtt.Hass.Topic, root["mqtt_hass_topic"].as().c_str(), sizeof(config.Mqtt.Hass.Topic)); + // Check if base topic was changed + if (strcmp(config.Mqtt.Topic, root["mqtt_topic"].as().c_str())) { + MqttHandleInverter.unsubscribeTopics(); + strlcpy(config.Mqtt.Topic, root["mqtt_topic"].as().c_str(), sizeof(config.Mqtt.Topic)); + MqttHandleInverter.subscribeTopics(); + } + WebApi.writeConfig(retMsg); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); diff --git a/src/WebApi_powerlimiter.cpp b/src/WebApi_powerlimiter.cpp index 114ced773..b28380d3c 100644 --- a/src/WebApi_powerlimiter.cpp +++ b/src/WebApi_powerlimiter.cpp @@ -38,6 +38,7 @@ void WebApiPowerLimiterClass::onStatus(AsyncWebServerRequest* request) root["battery_always_use_at_night"] = config.PowerLimiter.BatteryAlwaysUseAtNight; root["is_inverter_behind_powermeter"] = config.PowerLimiter.IsInverterBehindPowerMeter; root["is_inverter_solar_powered"] = config.PowerLimiter.IsInverterSolarPowered; + root["use_overscaling_to_compensate_shading"] = config.PowerLimiter.UseOverscalingToCompensateShading; root["inverter_serial"] = String(config.PowerLimiter.InverterId); root["inverter_channel_id"] = config.PowerLimiter.InverterChannelId; root["target_power_consumption"] = config.PowerLimiter.TargetPowerConsumption; @@ -159,6 +160,7 @@ void WebApiPowerLimiterClass::onAdminPost(AsyncWebServerRequest* request) config.PowerLimiter.IsInverterBehindPowerMeter = root["is_inverter_behind_powermeter"].as(); config.PowerLimiter.IsInverterSolarPowered = root["is_inverter_solar_powered"].as(); + config.PowerLimiter.UseOverscalingToCompensateShading = root["use_overscaling_to_compensate_shading"].as(); config.PowerLimiter.InverterId = root["inverter_serial"].as(); config.PowerLimiter.InverterChannelId = root["inverter_channel_id"].as(); config.PowerLimiter.TargetPowerConsumption = root["target_power_consumption"].as(); diff --git a/src/WebApi_powermeter.cpp b/src/WebApi_powermeter.cpp index 8ca492b01..59297a0b3 100644 --- a/src/WebApi_powermeter.cpp +++ b/src/WebApi_powermeter.cpp @@ -12,7 +12,8 @@ #include "MqttSettings.h" #include "PowerLimiter.h" #include "PowerMeter.h" -#include "HttpPowerMeter.h" +#include "PowerMeterHttpJson.h" +#include "PowerMeterHttpSml.h" #include "WebApi.h" #include "helper.h" @@ -25,26 +26,16 @@ void WebApiPowerMeterClass::init(AsyncWebServer& server, Scheduler& scheduler) _server->on("/api/powermeter/status", HTTP_GET, std::bind(&WebApiPowerMeterClass::onStatus, this, _1)); _server->on("/api/powermeter/config", HTTP_GET, std::bind(&WebApiPowerMeterClass::onAdminGet, this, _1)); _server->on("/api/powermeter/config", HTTP_POST, std::bind(&WebApiPowerMeterClass::onAdminPost, this, _1)); - _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(); + _server->on("/api/powermeter/testhttpjsonrequest", HTTP_POST, std::bind(&WebApiPowerMeterClass::onTestHttpJsonRequest, this, _1)); + _server->on("/api/powermeter/testhttpsmlrequest", HTTP_POST, std::bind(&WebApiPowerMeterClass::onTestHttpSmlRequest, this, _1)); } void WebApiPowerMeterClass::onStatus(AsyncWebServerRequest* request) { + if (!WebApi.checkCredentialsReadonly(request)) { + return; + } + AsyncJsonResponse* response = new AsyncJsonResponse(); auto& root = response->getRoot(); const CONFIG_T& config = Configuration.get(); @@ -52,32 +43,18 @@ void WebApiPowerMeterClass::onStatus(AsyncWebServerRequest* request) root["enabled"] = config.PowerMeter.Enabled; root["verbose_logging"] = config.PowerMeter.VerboseLogging; root["source"] = config.PowerMeter.Source; - root["interval"] = config.PowerMeter.Interval; - root["mqtt_topic_powermeter_1"] = config.PowerMeter.MqttTopicPowerMeter1; - root["mqtt_topic_powermeter_2"] = config.PowerMeter.MqttTopicPowerMeter2; - root["mqtt_topic_powermeter_3"] = config.PowerMeter.MqttTopicPowerMeter3; - root["sdmbaudrate"] = config.PowerMeter.SdmBaudrate; - root["sdmaddress"] = config.PowerMeter.SdmAddress; - root["http_individual_requests"] = config.PowerMeter.HttpIndividualRequests; - - auto httpPhases = root["http_phases"].to(); - - for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) { - 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); - phaseObject["auth_type"]= config.PowerMeter.Http_Phase[i].AuthType; - phaseObject["username"] = String(config.PowerMeter.Http_Phase[i].Username); - 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["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; - } + + auto mqtt = root["mqtt"].to(); + Configuration.serializePowerMeterMqttConfig(config.PowerMeter.Mqtt, mqtt); + + auto serialSdm = root["serial_sdm"].to(); + Configuration.serializePowerMeterSerialSdmConfig(config.PowerMeter.SerialSdm, serialSdm); + + auto httpJson = root["http_json"].to(); + Configuration.serializePowerMeterHttpJsonConfig(config.PowerMeter.HttpJson, httpJson); + + auto httpSml = root["http_sml"].to(); + Configuration.serializePowerMeterHttpSmlConfig(config.PowerMeter.HttpSml, httpSml); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); } @@ -112,44 +89,53 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request) return; } - if (static_cast(root["source"].as()) == PowerMeterClass::Source::HTTP) { - JsonArray http_phases = root["http_phases"]; - for (uint8_t i = 0; i < http_phases.size(); i++) { - JsonObject phase = http_phases[i].as(); + auto checkHttpConfig = [&](JsonObject const& cfg) -> bool { + if (!cfg.containsKey("url") + || (!cfg["url"].as().startsWith("http://") + && !cfg["url"].as().startsWith("https://"))) { + retMsg["message"] = "URL must either start with http:// or https://!"; + response->setLength(); + request->send(response); + return false; + } - if (i > 0 && !phase["enabled"].as()) { - continue; - } + if ((cfg["auth_type"].as() != HttpRequestConfig::Auth::None) + && (cfg["username"].as().length() == 0 || cfg["password"].as().length() == 0)) { + retMsg["message"] = "Username or password must not be empty!"; + response->setLength(); + request->send(response); + return false; + } - if (i == 0 || phase["http_individual_requests"].as()) { - if (!phase.containsKey("url") - || (!phase["url"].as().startsWith("http://") - && !phase["url"].as().startsWith("https://"))) { - retMsg["message"] = "URL must either start with http:// or https://!"; - response->setLength(); - request->send(response); - return; - } + if (!cfg.containsKey("timeout") + || cfg["timeout"].as() <= 0) { + retMsg["message"] = "Timeout must be greater than 0 ms!"; + response->setLength(); + request->send(response); + return false; + } - 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(); - request->send(response); - return; - } + return true; + }; + + if (static_cast(root["source"].as()) == PowerMeterProvider::Type::HTTP_JSON) { + JsonObject httpJson = root["http_json"]; + JsonArray valueConfigs = httpJson["values"]; + for (uint8_t i = 0; i < valueConfigs.size(); i++) { + JsonObject valueConfig = valueConfigs[i].as(); - if (!phase.containsKey("timeout") - || phase["timeout"].as() <= 0) { - retMsg["message"] = "Timeout must be greater than 0 ms!"; - response->setLength(); - request->send(response); + if (i > 0 && !valueConfig["enabled"].as()) { + continue; + } + + if (i == 0 || httpJson["individual_requests"].as()) { + if (!checkHttpConfig(valueConfig["http_request"].as())) { return; } } - if (!phase.containsKey("json_path") - || phase["json_path"].as().length() == 0) { + if (!valueConfig.containsKey("json_path") + || valueConfig["json_path"].as().length() == 0) { retMsg["message"] = "Json path must not be empty!"; response->setLength(); request->send(response); @@ -158,37 +144,38 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request) } } + if (static_cast(root["source"].as()) == PowerMeterProvider::Type::HTTP_SML) { + JsonObject httpSml = root["http_sml"]; + if (!checkHttpConfig(httpSml["http_request"].as())) { + return; + } + } + CONFIG_T& config = Configuration.get(); config.PowerMeter.Enabled = root["enabled"].as(); config.PowerMeter.VerboseLogging = root["verbose_logging"].as(); config.PowerMeter.Source = root["source"].as(); - config.PowerMeter.Interval = root["interval"].as(); - strlcpy(config.PowerMeter.MqttTopicPowerMeter1, root["mqtt_topic_powermeter_1"].as().c_str(), sizeof(config.PowerMeter.MqttTopicPowerMeter1)); - strlcpy(config.PowerMeter.MqttTopicPowerMeter2, root["mqtt_topic_powermeter_2"].as().c_str(), sizeof(config.PowerMeter.MqttTopicPowerMeter2)); - strlcpy(config.PowerMeter.MqttTopicPowerMeter3, root["mqtt_topic_powermeter_3"].as().c_str(), sizeof(config.PowerMeter.MqttTopicPowerMeter3)); - config.PowerMeter.SdmBaudrate = root["sdmbaudrate"].as(); - config.PowerMeter.SdmAddress = root["sdmaddress"].as(); - config.PowerMeter.HttpIndividualRequests = root["http_individual_requests"].as(); - - JsonArray http_phases = root["http_phases"]; - for (uint8_t i = 0; i < http_phases.size(); i++) { - decodeJsonPhaseConfig(http_phases[i].as(), config.PowerMeter.Http_Phase[i]); - } - config.PowerMeter.Http_Phase[0].Enabled = true; + + Configuration.deserializePowerMeterMqttConfig(root["mqtt"].as(), + config.PowerMeter.Mqtt); + + Configuration.deserializePowerMeterSerialSdmConfig(root["serial_sdm"].as(), + config.PowerMeter.SerialSdm); + + Configuration.deserializePowerMeterHttpJsonConfig(root["http_json"].as(), + config.PowerMeter.HttpJson); + + Configuration.deserializePowerMeterHttpSmlConfig(root["http_sml"].as(), + config.PowerMeter.HttpSml); WebApi.writeConfig(retMsg); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); - - // reboot requiered as per https://github.com/helgeerbe/OpenDTU-OnBattery/issues/565#issuecomment-1872552559 - yield(); - delay(1000); - yield(); - ESP.restart(); + PowerMeter.updateSettings(); } -void WebApiPowerMeterClass::onTestHttpRequest(AsyncWebServerRequest* request) +void WebApiPowerMeterClass::onTestHttpJsonRequest(AsyncWebServerRequest* request) { if (!WebApi.checkCredentials(request)) { return; @@ -202,26 +189,60 @@ void WebApiPowerMeterClass::onTestHttpRequest(AsyncWebServerRequest* request) 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") - || !root.containsKey("timeout") || !root.containsKey("json_path")) { - retMsg["message"] = "Missing fields!"; - asyncJsonResponse->setLength(); - request->send(asyncJsonResponse); + char response[256]; + + auto powerMeterConfig = std::make_unique(); + Configuration.deserializePowerMeterHttpJsonConfig(root["http_json"].as(), + *powerMeterConfig); + auto upMeter = std::make_unique(*powerMeterConfig); + upMeter->init(); + auto res = upMeter->poll(); + using values_t = PowerMeterHttpJson::power_values_t; + if (std::holds_alternative(res)) { + retMsg["type"] = "success"; + auto vals = std::get(res); + auto pos = snprintf(response, sizeof(response), "Result: %5.2fW", vals[0]); + for (size_t i = 1; i < vals.size(); ++i) { + if (!powerMeterConfig->Values[i].Enabled) { continue; } + pos += snprintf(response + pos, sizeof(response) - pos, ", %5.2fW", vals[i]); + } + snprintf(response + pos, sizeof(response) - pos, ", Total: %5.2f", upMeter->getPowerTotal()); + } else { + snprintf(response, sizeof(response), "%s", std::get(res).c_str()); + } + + retMsg["message"] = response; + asyncJsonResponse->setLength(); + request->send(asyncJsonResponse); +} + +void WebApiPowerMeterClass::onTestHttpSmlRequest(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentials(request)) { return; } + AsyncJsonResponse* asyncJsonResponse = new AsyncJsonResponse(); + JsonDocument root; + if (!WebApi.parseRequestData(request, asyncJsonResponse, root)) { + return; + } + + auto& retMsg = asyncJsonResponse->getRoot(); char response[256]; - int phase = 0;//"absuing" index 0 of the float power[3] in HttpPowerMeter to store the result - PowerMeterHttpConfig phaseConfig; - decodeJsonPhaseConfig(root.as(), phaseConfig); - if (HttpPowerMeter.queryPhase(phase, phaseConfig)) { + auto powerMeterConfig = std::make_unique(); + Configuration.deserializePowerMeterHttpSmlConfig(root["http_sml"].as(), + *powerMeterConfig); + auto upMeter = std::make_unique(*powerMeterConfig); + upMeter->init(); + auto res = upMeter->poll(); + if (res.isEmpty()) { retMsg["type"] = "success"; - snprintf_P(response, sizeof(response), "Success! Power: %5.2fW", HttpPowerMeter.getPower(phase + 1)); + snprintf(response, sizeof(response), "Result: %5.2fW", upMeter->getPowerTotal()); } else { - snprintf_P(response, sizeof(response), "%s", HttpPowerMeter.httpPowerMeterError); + snprintf(response, sizeof(response), "%s", res.c_str()); } retMsg["message"] = response; diff --git a/src/WebApi_sysstatus.cpp b/src/WebApi_sysstatus.cpp index 6d07f2fbc..3a6b8f440 100644 --- a/src/WebApi_sysstatus.cpp +++ b/src/WebApi_sysstatus.cpp @@ -7,11 +7,12 @@ #include "NetworkSettings.h" #include "PinMapping.h" #include "WebApi.h" +#include "__compiled_constants.h" #include +#include #include #include #include -#include "__compiled_constants.h" void WebApiSysstatusClass::init(AsyncWebServer& server, Scheduler& scheduler) { @@ -33,6 +34,7 @@ void WebApiSysstatusClass::onSystemStatus(AsyncWebServerRequest* request) root["sdkversion"] = ESP.getSdkVersion(); root["cpufreq"] = ESP.getCpuFreqMHz(); + root["cputemp"] = CpuTemperature.read(); root["heap_total"] = ESP.getHeapSize(); root["heap_used"] = ESP.getHeapSize() - ESP.getFreeHeap(); diff --git a/src/WebApi_webapp.cpp b/src/WebApi_webapp.cpp index b8b813853..b5335b02d 100644 --- a/src/WebApi_webapp.cpp +++ b/src/WebApi_webapp.cpp @@ -42,7 +42,7 @@ void WebApiWebappClass::responseBinaryDataWithETagCache(AsyncWebServerRequest *r if (eTagMatch) { response = request->beginResponse(304); } else { - response = request->beginResponse_P(200, contentType, content, len); + response = request->beginResponse(200, contentType, content, len); if (contentEncoding.length() > 0) { response->addHeader("Content-Encoding", contentEncoding); } diff --git a/src/WebApi_ws_Huawei.cpp b/src/WebApi_ws_Huawei.cpp index e8e23615a..f171a18ab 100644 --- a/src/WebApi_ws_Huawei.cpp +++ b/src/WebApi_ws_Huawei.cpp @@ -1,144 +1,144 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * Copyright (C) 2022-2024 Thomas Basler and others - */ -#include "WebApi_ws_Huawei.h" -#include "AsyncJson.h" -#include "Configuration.h" -#include "Huawei_can.h" -#include "MessageOutput.h" -#include "Utils.h" -#include "WebApi.h" -#include "defaults.h" - -WebApiWsHuaweiLiveClass::WebApiWsHuaweiLiveClass() - : _ws("/huaweilivedata") -{ -} - -void WebApiWsHuaweiLiveClass::init(AsyncWebServer& server, Scheduler& scheduler) -{ - using std::placeholders::_1; - using std::placeholders::_2; - using std::placeholders::_3; - using std::placeholders::_4; - using std::placeholders::_5; - using std::placeholders::_6; - - _server = &server; - _server->on("/api/huaweilivedata/status", HTTP_GET, std::bind(&WebApiWsHuaweiLiveClass::onLivedataStatus, this, _1)); - - _server->addHandler(&_ws); - _ws.onEvent(std::bind(&WebApiWsHuaweiLiveClass::onWebsocketEvent, this, _1, _2, _3, _4, _5, _6)); - - scheduler.addTask(_wsCleanupTask); - _wsCleanupTask.setCallback(std::bind(&WebApiWsHuaweiLiveClass::wsCleanupTaskCb, this)); - _wsCleanupTask.setIterations(TASK_FOREVER); - _wsCleanupTask.setInterval(1 * TASK_SECOND); - _wsCleanupTask.enable(); - - scheduler.addTask(_sendDataTask); - _sendDataTask.setCallback(std::bind(&WebApiWsHuaweiLiveClass::sendDataTaskCb, this)); - _sendDataTask.setIterations(TASK_FOREVER); - _sendDataTask.setInterval(1 * TASK_SECOND); - _sendDataTask.enable(); -} - -void WebApiWsHuaweiLiveClass::wsCleanupTaskCb() -{ - // see: https://github.com/me-no-dev/ESPAsyncWebServer#limiting-the-number-of-web-socket-clients - _ws.cleanupClients(); -} - -void WebApiWsHuaweiLiveClass::sendDataTaskCb() -{ - // do nothing if no WS client is connected - if (_ws.count() == 0) { - return; - } - - try { - std::lock_guard lock(_mutex); - JsonDocument root; - JsonVariant var = root; - - generateCommonJsonResponse(var); - - if (Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { - String buffer; - serializeJson(root, buffer); - - _ws.textAll(buffer); - } - } 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()); - } catch (const std::exception& exc) { - MessageOutput.printf("Unknown exception in /api/huaweilivedata/status. Reason: \"%s\".\r\n", exc.what()); - } -} - -void WebApiWsHuaweiLiveClass::generateCommonJsonResponse(JsonVariant& root) -{ - const RectifierParameters_t * rp = HuaweiCan.get(); - - root["data_age"] = (millis() - HuaweiCan.getLastUpdate()) / 1000; - root["input_voltage"]["v"] = rp->input_voltage; - root["input_voltage"]["u"] = "V"; - root["input_current"]["v"] = rp->input_current; - root["input_current"]["u"] = "A"; - root["input_power"]["v"] = rp->input_power; - root["input_power"]["u"] = "W"; - root["output_voltage"]["v"] = rp->output_voltage; - root["output_voltage"]["u"] = "V"; - root["output_current"]["v"] = rp->output_current; - root["output_current"]["u"] = "A"; - root["max_output_current"]["v"] = rp->max_output_current; - root["max_output_current"]["u"] = "A"; - root["output_power"]["v"] = rp->output_power; - root["output_power"]["u"] = "W"; - root["input_temp"]["v"] = rp->input_temp; - root["input_temp"]["u"] = "°C"; - root["output_temp"]["v"] = rp->output_temp; - root["output_temp"]["u"] = "°C"; - root["efficiency"]["v"] = rp->efficiency * 100; - root["efficiency"]["u"] = "%"; - -} - -void WebApiWsHuaweiLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len) -{ - if (type == WS_EVT_CONNECT) { - char str[64]; - snprintf(str, sizeof(str), "Websocket: [%s][%u] connect", server->url(), client->id()); - Serial.println(str); - MessageOutput.println(str); - } else if (type == WS_EVT_DISCONNECT) { - char str[64]; - snprintf(str, sizeof(str), "Websocket: [%s][%u] disconnect", server->url(), client->id()); - Serial.println(str); - MessageOutput.println(str); - } -} - -void WebApiWsHuaweiLiveClass::onLivedataStatus(AsyncWebServerRequest* request) -{ - if (!WebApi.checkCredentialsReadonly(request)) { - return; - } - try { - std::lock_guard lock(_mutex); - AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& root = response->getRoot(); - - generateCommonJsonResponse(root); - - WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); - - } 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); - } catch (const std::exception& exc) { - MessageOutput.printf("Unknown exception in /api/huaweilivedata/status. Reason: \"%s\".\r\n", exc.what()); - WebApi.sendTooManyRequests(request); - } +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022-2024 Thomas Basler and others + */ +#include "WebApi_ws_Huawei.h" +#include "AsyncJson.h" +#include "Configuration.h" +#include "Huawei_can.h" +#include "MessageOutput.h" +#include "Utils.h" +#include "WebApi.h" +#include "defaults.h" + +WebApiWsHuaweiLiveClass::WebApiWsHuaweiLiveClass() + : _ws("/huaweilivedata") +{ +} + +void WebApiWsHuaweiLiveClass::init(AsyncWebServer& server, Scheduler& scheduler) +{ + using std::placeholders::_1; + using std::placeholders::_2; + using std::placeholders::_3; + using std::placeholders::_4; + using std::placeholders::_5; + using std::placeholders::_6; + + _server = &server; + _server->on("/api/huaweilivedata/status", HTTP_GET, std::bind(&WebApiWsHuaweiLiveClass::onLivedataStatus, this, _1)); + + _server->addHandler(&_ws); + _ws.onEvent(std::bind(&WebApiWsHuaweiLiveClass::onWebsocketEvent, this, _1, _2, _3, _4, _5, _6)); + + scheduler.addTask(_wsCleanupTask); + _wsCleanupTask.setCallback(std::bind(&WebApiWsHuaweiLiveClass::wsCleanupTaskCb, this)); + _wsCleanupTask.setIterations(TASK_FOREVER); + _wsCleanupTask.setInterval(1 * TASK_SECOND); + _wsCleanupTask.enable(); + + scheduler.addTask(_sendDataTask); + _sendDataTask.setCallback(std::bind(&WebApiWsHuaweiLiveClass::sendDataTaskCb, this)); + _sendDataTask.setIterations(TASK_FOREVER); + _sendDataTask.setInterval(1 * TASK_SECOND); + _sendDataTask.enable(); +} + +void WebApiWsHuaweiLiveClass::wsCleanupTaskCb() +{ + // see: https://github.com/me-no-dev/ESPAsyncWebServer#limiting-the-number-of-web-socket-clients + _ws.cleanupClients(); +} + +void WebApiWsHuaweiLiveClass::sendDataTaskCb() +{ + // do nothing if no WS client is connected + if (_ws.count() == 0) { + return; + } + + try { + std::lock_guard lock(_mutex); + JsonDocument root; + JsonVariant var = root; + + generateCommonJsonResponse(var); + + if (Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { + String buffer; + serializeJson(root, buffer); + + _ws.textAll(buffer); + } + } 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()); + } catch (const std::exception& exc) { + MessageOutput.printf("Unknown exception in /api/huaweilivedata/status. Reason: \"%s\".\r\n", exc.what()); + } +} + +void WebApiWsHuaweiLiveClass::generateCommonJsonResponse(JsonVariant& root) +{ + const RectifierParameters_t * rp = HuaweiCan.get(); + + root["data_age"] = (millis() - HuaweiCan.getLastUpdate()) / 1000; + root["input_voltage"]["v"] = rp->input_voltage; + root["input_voltage"]["u"] = "V"; + root["input_current"]["v"] = rp->input_current; + root["input_current"]["u"] = "A"; + root["input_power"]["v"] = rp->input_power; + root["input_power"]["u"] = "W"; + root["output_voltage"]["v"] = rp->output_voltage; + root["output_voltage"]["u"] = "V"; + root["output_current"]["v"] = rp->output_current; + root["output_current"]["u"] = "A"; + root["max_output_current"]["v"] = rp->max_output_current; + root["max_output_current"]["u"] = "A"; + root["output_power"]["v"] = rp->output_power; + root["output_power"]["u"] = "W"; + root["input_temp"]["v"] = rp->input_temp; + root["input_temp"]["u"] = "°C"; + root["output_temp"]["v"] = rp->output_temp; + root["output_temp"]["u"] = "°C"; + root["efficiency"]["v"] = rp->efficiency * 100; + root["efficiency"]["u"] = "%"; + +} + +void WebApiWsHuaweiLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len) +{ + if (type == WS_EVT_CONNECT) { + char str[64]; + snprintf(str, sizeof(str), "Websocket: [%s][%u] connect", server->url(), client->id()); + Serial.println(str); + MessageOutput.println(str); + } else if (type == WS_EVT_DISCONNECT) { + char str[64]; + snprintf(str, sizeof(str), "Websocket: [%s][%u] disconnect", server->url(), client->id()); + Serial.println(str); + MessageOutput.println(str); + } +} + +void WebApiWsHuaweiLiveClass::onLivedataStatus(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentialsReadonly(request)) { + return; + } + try { + std::lock_guard lock(_mutex); + AsyncJsonResponse* response = new AsyncJsonResponse(); + auto& root = response->getRoot(); + + generateCommonJsonResponse(root); + + WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); + + } 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); + } catch (const std::exception& exc) { + MessageOutput.printf("Unknown exception in /api/huaweilivedata/status. Reason: \"%s\".\r\n", exc.what()); + WebApi.sendTooManyRequests(request); + } } \ No newline at end of file diff --git a/src/WebApi_ws_battery.cpp b/src/WebApi_ws_battery.cpp index 466540228..42913abc6 100644 --- a/src/WebApi_ws_battery.cpp +++ b/src/WebApi_ws_battery.cpp @@ -1,126 +1,126 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * Copyright (C) 2022-2024 Thomas Basler and others - */ -#include "WebApi_ws_battery.h" -#include "AsyncJson.h" -#include "Configuration.h" -#include "Battery.h" -#include "MessageOutput.h" -#include "WebApi.h" -#include "defaults.h" -#include "Utils.h" - -WebApiWsBatteryLiveClass::WebApiWsBatteryLiveClass() - : _ws("/batterylivedata") -{ -} - -void WebApiWsBatteryLiveClass::init(AsyncWebServer& server, Scheduler& scheduler) -{ - using std::placeholders::_1; - using std::placeholders::_2; - using std::placeholders::_3; - using std::placeholders::_4; - using std::placeholders::_5; - using std::placeholders::_6; - - _server = &server; - _server->on("/api/batterylivedata/status", HTTP_GET, std::bind(&WebApiWsBatteryLiveClass::onLivedataStatus, this, _1)); - - _server->addHandler(&_ws); - _ws.onEvent(std::bind(&WebApiWsBatteryLiveClass::onWebsocketEvent, this, _1, _2, _3, _4, _5, _6)); - - scheduler.addTask(_wsCleanupTask); - _wsCleanupTask.setCallback(std::bind(&WebApiWsBatteryLiveClass::wsCleanupTaskCb, this)); - _wsCleanupTask.setIterations(TASK_FOREVER); - _wsCleanupTask.setInterval(1 * TASK_SECOND); - _wsCleanupTask.enable(); - - scheduler.addTask(_sendDataTask); - _sendDataTask.setCallback(std::bind(&WebApiWsBatteryLiveClass::sendDataTaskCb, this)); - _sendDataTask.setIterations(TASK_FOREVER); - _sendDataTask.setInterval(1 * TASK_SECOND); - _sendDataTask.enable(); -} - -void WebApiWsBatteryLiveClass::wsCleanupTaskCb() -{ - // see: https://github.com/me-no-dev/ESPAsyncWebServer#limiting-the-number-of-web-socket-clients - _ws.cleanupClients(); -} - -void WebApiWsBatteryLiveClass::sendDataTaskCb() -{ - // do nothing if no WS client is connected - if (_ws.count() == 0) { - return; - } - - if (!Battery.getStats()->updateAvailable(_lastUpdateCheck)) { return; } - _lastUpdateCheck = millis(); - - try { - std::lock_guard lock(_mutex); - JsonDocument root; - JsonVariant var = root; - - generateCommonJsonResponse(var); - - if (Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { - - // battery provider does not generate a card, e.g., MQTT provider - if (root.isNull()) { return; } - - 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) { - MessageOutput.printf("Calling /api/batterylivedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what()); - } catch (const std::exception& exc) { - MessageOutput.printf("Unknown exception in /api/batterylivedata/status. Reason: \"%s\".\r\n", exc.what()); - } -} - -void WebApiWsBatteryLiveClass::generateCommonJsonResponse(JsonVariant& root) -{ - Battery.getStats()->getLiveViewData(root); -} - -void WebApiWsBatteryLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len) -{ - if (type == WS_EVT_CONNECT) { - MessageOutput.printf("Websocket: [%s][%u] connect\r\n", server->url(), client->id()); - } else if (type == WS_EVT_DISCONNECT) { - MessageOutput.printf("Websocket: [%s][%u] disconnect\r\n", server->url(), client->id()); - } -} - -void WebApiWsBatteryLiveClass::onLivedataStatus(AsyncWebServerRequest* request) -{ - if (!WebApi.checkCredentialsReadonly(request)) { - return; - } - try { - std::lock_guard lock(_mutex); - AsyncJsonResponse* response = new AsyncJsonResponse(); - auto& root = response->getRoot(); - generateCommonJsonResponse(root); - - 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); - } catch (const std::exception& exc) { - MessageOutput.printf("Unknown exception in /api/batterylivedata/status. Reason: \"%s\".\r\n", exc.what()); - WebApi.sendTooManyRequests(request); - } +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022-2024 Thomas Basler and others + */ +#include "WebApi_ws_battery.h" +#include "AsyncJson.h" +#include "Configuration.h" +#include "Battery.h" +#include "MessageOutput.h" +#include "WebApi.h" +#include "defaults.h" +#include "Utils.h" + +WebApiWsBatteryLiveClass::WebApiWsBatteryLiveClass() + : _ws("/batterylivedata") +{ +} + +void WebApiWsBatteryLiveClass::init(AsyncWebServer& server, Scheduler& scheduler) +{ + using std::placeholders::_1; + using std::placeholders::_2; + using std::placeholders::_3; + using std::placeholders::_4; + using std::placeholders::_5; + using std::placeholders::_6; + + _server = &server; + _server->on("/api/batterylivedata/status", HTTP_GET, std::bind(&WebApiWsBatteryLiveClass::onLivedataStatus, this, _1)); + + _server->addHandler(&_ws); + _ws.onEvent(std::bind(&WebApiWsBatteryLiveClass::onWebsocketEvent, this, _1, _2, _3, _4, _5, _6)); + + scheduler.addTask(_wsCleanupTask); + _wsCleanupTask.setCallback(std::bind(&WebApiWsBatteryLiveClass::wsCleanupTaskCb, this)); + _wsCleanupTask.setIterations(TASK_FOREVER); + _wsCleanupTask.setInterval(1 * TASK_SECOND); + _wsCleanupTask.enable(); + + scheduler.addTask(_sendDataTask); + _sendDataTask.setCallback(std::bind(&WebApiWsBatteryLiveClass::sendDataTaskCb, this)); + _sendDataTask.setIterations(TASK_FOREVER); + _sendDataTask.setInterval(1 * TASK_SECOND); + _sendDataTask.enable(); +} + +void WebApiWsBatteryLiveClass::wsCleanupTaskCb() +{ + // see: https://github.com/me-no-dev/ESPAsyncWebServer#limiting-the-number-of-web-socket-clients + _ws.cleanupClients(); +} + +void WebApiWsBatteryLiveClass::sendDataTaskCb() +{ + // do nothing if no WS client is connected + if (_ws.count() == 0) { + return; + } + + if (!Battery.getStats()->updateAvailable(_lastUpdateCheck)) { return; } + _lastUpdateCheck = millis(); + + try { + std::lock_guard lock(_mutex); + JsonDocument root; + JsonVariant var = root; + + generateCommonJsonResponse(var); + + if (Utils::checkJsonAlloc(root, __FUNCTION__, __LINE__)) { + + // battery provider does not generate a card, e.g., MQTT provider + if (root.isNull()) { return; } + + 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) { + MessageOutput.printf("Calling /api/batterylivedata/status has temporarily run out of resources. Reason: \"%s\".\r\n", bad_alloc.what()); + } catch (const std::exception& exc) { + MessageOutput.printf("Unknown exception in /api/batterylivedata/status. Reason: \"%s\".\r\n", exc.what()); + } +} + +void WebApiWsBatteryLiveClass::generateCommonJsonResponse(JsonVariant& root) +{ + Battery.getStats()->getLiveViewData(root); +} + +void WebApiWsBatteryLiveClass::onWebsocketEvent(AsyncWebSocket* server, AsyncWebSocketClient* client, AwsEventType type, void* arg, uint8_t* data, size_t len) +{ + if (type == WS_EVT_CONNECT) { + MessageOutput.printf("Websocket: [%s][%u] connect\r\n", server->url(), client->id()); + } else if (type == WS_EVT_DISCONNECT) { + MessageOutput.printf("Websocket: [%s][%u] disconnect\r\n", server->url(), client->id()); + } +} + +void WebApiWsBatteryLiveClass::onLivedataStatus(AsyncWebServerRequest* request) +{ + if (!WebApi.checkCredentialsReadonly(request)) { + return; + } + try { + std::lock_guard lock(_mutex); + AsyncJsonResponse* response = new AsyncJsonResponse(); + auto& root = response->getRoot(); + generateCommonJsonResponse(root); + + 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); + } catch (const std::exception& exc) { + MessageOutput.printf("Unknown exception in /api/batterylivedata/status. Reason: \"%s\".\r\n", exc.what()); + WebApi.sendTooManyRequests(request); + } } \ No newline at end of file diff --git a/src/WebApi_ws_live.cpp b/src/WebApi_ws_live.cpp index ab54d479f..cda20e72f 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -92,18 +92,32 @@ void WebApiWsLiveClass::generateOnBatteryJsonResponse(JsonVariant& root, bool al batteryObj["enabled"] = config.Battery.Enabled; if (config.Battery.Enabled) { - addTotalField(batteryObj, "soc", spStats->getSoC(), "%", 0); + if (spStats->isSoCValid()) { + addTotalField(batteryObj, "soc", spStats->getSoC(), "%", spStats->getSoCPrecision()); + } + + if (spStats->isVoltageValid()) { + addTotalField(batteryObj, "voltage", spStats->getVoltage(), "V", 2); + } + + if (spStats->isCurrentValid()) { + addTotalField(batteryObj, "current", spStats->getChargeCurrent(), "A", spStats->getChargeCurrentPrecision()); + } + + if (spStats->isVoltageValid() && spStats->isCurrentValid()) { + addTotalField(batteryObj, "power", spStats->getVoltage() * spStats->getChargeCurrent(), "W", 1); + } } if (!all) { _lastPublishBattery = millis(); } } - if (all || (PowerMeter.getLastPowerMeterUpdate() - _lastPublishPowerMeter) < halfOfAllMillis) { + if (all || (PowerMeter.getLastUpdate() - _lastPublishPowerMeter) < halfOfAllMillis) { auto powerMeterObj = root["power_meter"].to(); powerMeterObj["enabled"] = config.PowerMeter.Enabled; if (config.PowerMeter.Enabled) { - addTotalField(powerMeterObj, "Power", PowerMeter.getPowerTotal(false), "W", 1); + addTotalField(powerMeterObj, "Power", PowerMeter.getPowerTotal(), "W", 1); } if (!all) { _lastPublishPowerMeter = millis(); } @@ -332,9 +346,7 @@ void WebApiWsLiveClass::onLivedataStatus(AsyncWebServerRequest* request) generateOnBatteryJsonResponse(root, true); - generateOnBatteryJsonResponse(root, true); - - WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); + 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()); diff --git a/src/main.cpp b/src/main.cpp index c23087aae..7489a27d8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,203 +1,203 @@ -// SPDX-License-Identifier: GPL-2.0-or-later -/* - * Copyright (C) 2022-2024 Thomas Basler and others - */ -#include "Configuration.h" -#include "Datastore.h" -#include "Display_Graphic.h" -#include "InverterSettings.h" -#include "Led_Single.h" -#include "MessageOutput.h" -#include "SerialPortManager.h" -#include "VictronMppt.h" -#include "Battery.h" -#include "Huawei_can.h" -#include "MqttHandleDtu.h" -#include "MqttHandleHass.h" -#include "MqttHandleVedirectHass.h" -#include "MqttHandleBatteryHass.h" -#include "MqttHandleInverter.h" -#include "MqttHandleInverterTotal.h" -#include "MqttHandleVedirect.h" -#include "MqttHandleHuawei.h" -#include "MqttHandlePowerLimiter.h" -#include "MqttHandlePowerLimiterHass.h" -#include "MqttSettings.h" -#include "NetworkSettings.h" -#include "NtpSettings.h" -#include "PinMapping.h" -#include "Scheduler.h" -#include "SunPosition.h" -#include "Utils.h" -#include "WebApi.h" -#include "PowerMeter.h" -#include "PowerLimiter.h" -#include "defaults.h" -#include -#include -#include -#include - -void setup() -{ - // Move all dynamic allocations >512byte to psram (if available) - heap_caps_malloc_extmem_enable(512); - - // Initialize serial output - Serial.begin(SERIAL_BAUDRATE); -#if ARDUINO_USB_CDC_ON_BOOT - Serial.setTxTimeoutMs(0); - delay(100); -#else - while (!Serial) - yield(); -#endif - MessageOutput.init(scheduler); - MessageOutput.println(); - MessageOutput.println("Starting OpenDTU"); - - // Initialize file system - MessageOutput.print("Initialize FS... "); - if (!LittleFS.begin(false)) { // Do not format if mount failed - MessageOutput.print("failed... trying to format..."); - if (!LittleFS.begin(true)) { - MessageOutput.print("success"); - } else { - MessageOutput.print("failed"); - } - } else { - MessageOutput.println("done"); - } - - // Read configuration values - MessageOutput.print("Reading configuration... "); - if (!Configuration.read()) { - MessageOutput.print("initializing... "); - Configuration.init(); - if (Configuration.write()) { - MessageOutput.print("written... "); - } else { - MessageOutput.print("failed... "); - } - } - if (Configuration.get().Cfg.Version != CONFIG_VERSION) { - MessageOutput.print("migrated... "); - Configuration.migrate(); - } - auto& config = Configuration.get(); - MessageOutput.println("done"); - - // Load PinMapping - MessageOutput.print("Reading PinMapping... "); - if (PinMapping.init(String(Configuration.get().Dev_PinMapping))) { - MessageOutput.print("found valid mapping "); - } else { - MessageOutput.print("using default config "); - } - const auto& pin = PinMapping.get(); - MessageOutput.println("done"); - - SerialPortManager.init(); - - // Initialize WiFi - MessageOutput.print("Initialize Network... "); - NetworkSettings.init(scheduler); - MessageOutput.println("done"); - NetworkSettings.applyConfig(); - - // Initialize NTP - MessageOutput.print("Initialize NTP... "); - NtpSettings.init(); - MessageOutput.println("done"); - - // Initialize SunPosition - MessageOutput.print("Initialize SunPosition... "); - SunPosition.init(scheduler); - MessageOutput.println("done"); - - // Initialize MqTT - MessageOutput.print("Initialize MqTT... "); - MqttSettings.init(); - MqttHandleDtu.init(scheduler); - MqttHandleInverter.init(scheduler); - MqttHandleInverterTotal.init(scheduler); - MqttHandleVedirect.init(scheduler); - MqttHandleHass.init(scheduler); - MqttHandleVedirectHass.init(scheduler); - MqttHandleBatteryHass.init(scheduler); - MqttHandleHuawei.init(scheduler); - MqttHandlePowerLimiter.init(scheduler); - MqttHandlePowerLimiterHass.init(scheduler); - MessageOutput.println("done"); - - // Initialize WebApi - MessageOutput.print("Initialize WebApi... "); - WebApi.init(scheduler); - MessageOutput.println("done"); - - // Initialize Display - MessageOutput.print("Initialize Display... "); - Display.init( - scheduler, - static_cast(pin.display_type), - pin.display_data, - pin.display_clk, - pin.display_cs, - pin.display_reset); - Display.setDiagramMode(static_cast(config.Display.Diagram.Mode)); - Display.setOrientation(config.Display.Rotation); - Display.enablePowerSafe = config.Display.PowerSafe; - Display.enableScreensaver = config.Display.ScreenSaver; - Display.setContrast(config.Display.Contrast); - Display.setLanguage(config.Display.Language); - Display.setStartupDisplay(); - MessageOutput.println("done"); - - // Initialize Single LEDs - MessageOutput.print("Initialize LEDs... "); - LedSingle.init(scheduler); - MessageOutput.println("done"); - - // Check for default DTU serial - MessageOutput.print("Check for default DTU serial... "); - if (config.Dtu.Serial == DTU_SERIAL) { - MessageOutput.print("generate serial based on ESP chip id: "); - const uint64_t dtuId = Utils::generateDtuSerial(); - MessageOutput.printf("%0x%08x... ", - ((uint32_t)((dtuId >> 32) & 0xFFFFFFFF)), - ((uint32_t)(dtuId & 0xFFFFFFFF))); - config.Dtu.Serial = dtuId; - Configuration.write(); - } - MessageOutput.println("done"); - MessageOutput.println("done"); - - InverterSettings.init(scheduler); - - Datastore.init(scheduler); - - VictronMppt.init(scheduler); - - // Power meter - PowerMeter.init(scheduler); - - // Dynamic power limiter - PowerLimiter.init(scheduler); - - // Initialize Huawei AC-charger PSU / CAN bus - MessageOutput.println("Initialize Huawei AC charger interface... "); - if (PinMapping.isValidHuaweiConfig()) { - MessageOutput.printf("Huawei AC-charger miso = %d, mosi = %d, clk = %d, irq = %d, cs = %d, power_pin = %d\r\n", pin.huawei_miso, pin.huawei_mosi, pin.huawei_clk, pin.huawei_irq, pin.huawei_cs, pin.huawei_power); - HuaweiCan.init(scheduler, pin.huawei_miso, pin.huawei_mosi, pin.huawei_clk, pin.huawei_irq, pin.huawei_cs, pin.huawei_power); - MessageOutput.println("done"); - } else { - MessageOutput.println("Invalid pin config"); - } - - Battery.init(scheduler); -} - -void loop() -{ - scheduler.execute(); -} +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2022-2024 Thomas Basler and others + */ +#include "Configuration.h" +#include "Datastore.h" +#include "Display_Graphic.h" +#include "InverterSettings.h" +#include "Led_Single.h" +#include "MessageOutput.h" +#include "SerialPortManager.h" +#include "VictronMppt.h" +#include "Battery.h" +#include "Huawei_can.h" +#include "MqttHandleDtu.h" +#include "MqttHandleHass.h" +#include "MqttHandleVedirectHass.h" +#include "MqttHandleBatteryHass.h" +#include "MqttHandleInverter.h" +#include "MqttHandleInverterTotal.h" +#include "MqttHandleVedirect.h" +#include "MqttHandleHuawei.h" +#include "MqttHandlePowerLimiter.h" +#include "MqttHandlePowerLimiterHass.h" +#include "MqttSettings.h" +#include "NetworkSettings.h" +#include "NtpSettings.h" +#include "PinMapping.h" +#include "Scheduler.h" +#include "SunPosition.h" +#include "Utils.h" +#include "WebApi.h" +#include "PowerMeter.h" +#include "PowerLimiter.h" +#include "defaults.h" +#include +#include +#include +#include + +void setup() +{ + // Move all dynamic allocations >512byte to psram (if available) + heap_caps_malloc_extmem_enable(512); + + // Initialize serial output + Serial.begin(SERIAL_BAUDRATE); +#if ARDUINO_USB_CDC_ON_BOOT + Serial.setTxTimeoutMs(0); + delay(100); +#else + while (!Serial) + yield(); +#endif + MessageOutput.init(scheduler); + MessageOutput.println(); + MessageOutput.println("Starting OpenDTU"); + + // Initialize file system + MessageOutput.print("Initialize FS... "); + if (!LittleFS.begin(false)) { // Do not format if mount failed + MessageOutput.print("failed... trying to format..."); + if (!LittleFS.begin(true)) { + MessageOutput.print("success"); + } else { + MessageOutput.print("failed"); + } + } else { + MessageOutput.println("done"); + } + + // Read configuration values + MessageOutput.print("Reading configuration... "); + if (!Configuration.read()) { + MessageOutput.print("initializing... "); + Configuration.init(); + if (Configuration.write()) { + MessageOutput.print("written... "); + } else { + MessageOutput.print("failed... "); + } + } + if (Configuration.get().Cfg.Version != CONFIG_VERSION) { + MessageOutput.print("migrated... "); + Configuration.migrate(); + } + auto& config = Configuration.get(); + MessageOutput.println("done"); + + // Load PinMapping + MessageOutput.print("Reading PinMapping... "); + if (PinMapping.init(String(Configuration.get().Dev_PinMapping))) { + MessageOutput.print("found valid mapping "); + } else { + MessageOutput.print("using default config "); + } + const auto& pin = PinMapping.get(); + MessageOutput.println("done"); + + SerialPortManager.init(); + + // Initialize WiFi + MessageOutput.print("Initialize Network... "); + NetworkSettings.init(scheduler); + MessageOutput.println("done"); + NetworkSettings.applyConfig(); + + // Initialize NTP + MessageOutput.print("Initialize NTP... "); + NtpSettings.init(); + MessageOutput.println("done"); + + // Initialize SunPosition + MessageOutput.print("Initialize SunPosition... "); + SunPosition.init(scheduler); + MessageOutput.println("done"); + + // Initialize MqTT + MessageOutput.print("Initialize MqTT... "); + MqttSettings.init(); + MqttHandleDtu.init(scheduler); + MqttHandleInverter.init(scheduler); + MqttHandleInverterTotal.init(scheduler); + MqttHandleVedirect.init(scheduler); + MqttHandleHass.init(scheduler); + MqttHandleVedirectHass.init(scheduler); + MqttHandleBatteryHass.init(scheduler); + MqttHandleHuawei.init(scheduler); + MqttHandlePowerLimiter.init(scheduler); + MqttHandlePowerLimiterHass.init(scheduler); + MessageOutput.println("done"); + + // Initialize WebApi + MessageOutput.print("Initialize WebApi... "); + WebApi.init(scheduler); + MessageOutput.println("done"); + + // Initialize Display + MessageOutput.print("Initialize Display... "); + Display.init( + scheduler, + static_cast(pin.display_type), + pin.display_data, + pin.display_clk, + pin.display_cs, + pin.display_reset); + Display.setDiagramMode(static_cast(config.Display.Diagram.Mode)); + Display.setOrientation(config.Display.Rotation); + Display.enablePowerSafe = config.Display.PowerSafe; + Display.enableScreensaver = config.Display.ScreenSaver; + Display.setContrast(config.Display.Contrast); + Display.setLanguage(config.Display.Language); + Display.setStartupDisplay(); + MessageOutput.println("done"); + + // Initialize Single LEDs + MessageOutput.print("Initialize LEDs... "); + LedSingle.init(scheduler); + MessageOutput.println("done"); + + // Check for default DTU serial + MessageOutput.print("Check for default DTU serial... "); + if (config.Dtu.Serial == DTU_SERIAL) { + MessageOutput.print("generate serial based on ESP chip id: "); + const uint64_t dtuId = Utils::generateDtuSerial(); + MessageOutput.printf("%0x%08x... ", + ((uint32_t)((dtuId >> 32) & 0xFFFFFFFF)), + ((uint32_t)(dtuId & 0xFFFFFFFF))); + config.Dtu.Serial = dtuId; + Configuration.write(); + } + MessageOutput.println("done"); + MessageOutput.println("done"); + + InverterSettings.init(scheduler); + + Datastore.init(scheduler); + + VictronMppt.init(scheduler); + + // Power meter + PowerMeter.init(scheduler); + + // Dynamic power limiter + PowerLimiter.init(scheduler); + + // Initialize Huawei AC-charger PSU / CAN bus + MessageOutput.println("Initialize Huawei AC charger interface... "); + if (PinMapping.isValidHuaweiConfig()) { + MessageOutput.printf("Huawei AC-charger miso = %d, mosi = %d, clk = %d, irq = %d, cs = %d, power_pin = %d\r\n", pin.huawei_miso, pin.huawei_mosi, pin.huawei_clk, pin.huawei_irq, pin.huawei_cs, pin.huawei_power); + HuaweiCan.init(scheduler, pin.huawei_miso, pin.huawei_mosi, pin.huawei_clk, pin.huawei_irq, pin.huawei_cs, pin.huawei_power); + MessageOutput.println("done"); + } else { + MessageOutput.println("Invalid pin config"); + } + + Battery.init(scheduler); +} + +void loop() +{ + scheduler.execute(); +} diff --git a/webapp/.prettierrc.json b/webapp/.prettierrc.json new file mode 100644 index 000000000..678c251b9 --- /dev/null +++ b/webapp/.prettierrc.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/prettierrc", + "semi": true, + "tabWidth": 4, + "singleQuote": true, + "printWidth": 120, + "trailingComma": "es5" +} diff --git a/webapp/README.md b/webapp/README.md index 1729f905b..d342f47d7 100644 --- a/webapp/README.md +++ b/webapp/README.md @@ -1,6 +1,6 @@ # OpenDTU web frontend -You can run the webapp locally with `yarn dev`. If you enter the IP of your ESP in the `vite.config.ts` beforehand, all api requests will even be proxied to the real ESP. Then you can develop the webapp as if it were running directly on the ESP. The `yarn dev` also supports hot reload, i.e. as soon as you save a vue file, it is automatically reloaded in the browser. +You can run the webapp locally with `yarn dev`. If you enter the IP of your ESP in the `vite.user.ts` beforehand (template can be found in `vite.config.ts`), all api requests will even be proxied to the real ESP. Then you can develop the webapp as if it were running directly on the ESP. The `yarn dev` also supports hot reload, i.e. as soon as you save a vue file, it is automatically reloaded in the browser. ## Project Setup @@ -24,4 +24,4 @@ yarn build ```sh yarn lint -``` \ No newline at end of file +``` diff --git a/webapp/env.d.ts b/webapp/env.d.ts index 11f02fe2a..038f29277 100644 --- a/webapp/env.d.ts +++ b/webapp/env.d.ts @@ -1 +1,9 @@ /// + +import { Router, Route } from 'vue-router' +declare module '@vue/runtime-core' { + interface ComponentCustomProperties { + $router: Router + $route: Route + } +} diff --git a/webapp/eslint.config.js b/webapp/eslint.config.js index 91657b987..9a2aaecb7 100644 --- a/webapp/eslint.config.js +++ b/webapp/eslint.config.js @@ -30,7 +30,7 @@ export default [ "**/*.mts", ], languageOptions: { - ecmaVersion: 'latest' + ecmaVersion: 2022 }, } ] diff --git a/webapp/package.json b/webapp/package.json index e758594da..a427ce2c7 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -9,7 +9,8 @@ "preview": "vite preview --port 4173", "build-only": "vite build", "type-check": "vue-tsc --noEmit", - "lint": "eslint ." + "lint": "eslint .", + "format": "prettier --write src/" }, "dependencies": { "@popperjs/core": "^2.11.8", @@ -18,31 +19,32 @@ "mitt": "^3.0.1", "sortablejs": "^1.15.2", "spark-md5": "^3.0.2", - "vue": "^3.4.26", + "vue": "^3.4.35", "vue-i18n": "^9.13.1", - "vue-router": "^4.3.2" + "vue-router": "^4.4.2" }, "devDependencies": { "@intlify/unplugin-vue-i18n": "^4.0.0", "@tsconfig/node18": "^18.2.4", "@types/bootstrap": "^5.2.10", - "@types/node": "^20.12.10", + "@types/node": "^22.1.0", "@types/pulltorefreshjs": "^0.1.7", "@types/sortablejs": "^1.15.8", "@types/spark-md5": "^3.0.4", - "@vitejs/plugin-vue": "^5.0.4", + "@vitejs/plugin-vue": "^5.1.2", "@vue/eslint-config-typescript": "^13.0.0", "@vue/tsconfig": "^0.5.1", - "eslint": "^9.2.0", - "eslint-plugin-vue": "^9.25.0", + "eslint": "^9.8.0", + "eslint-plugin-vue": "^9.27.0", "npm-run-all": "^4.1.5", + "prettier": "^3.3.3", "pulltorefreshjs": "^0.1.22", - "sass": "^1.76.0", - "terser": "^5.31.0", - "typescript": "^5.4.5", - "vite": "^5.2.11", + "sass": "^1.77.6", + "terser": "^5.31.3", + "typescript": "^5.5.4", + "vite": "^5.3.5", "vite-plugin-compression": "^0.5.1", "vite-plugin-css-injected-by-js": "^3.5.1", - "vue-tsc": "^2.0.16" + "vue-tsc": "^2.0.29" } } diff --git a/webapp/src/App.vue b/webapp/src/App.vue index 6fa6eeaa5..200ce6695 100644 --- a/webapp/src/App.vue +++ b/webapp/src/App.vue @@ -5,10 +5,10 @@ \ No newline at end of file + + + diff --git a/webapp/src/components/BootstrapAlert.vue b/webapp/src/components/BootstrapAlert.vue index a629863db..e5e23e559 100644 --- a/webapp/src/components/BootstrapAlert.vue +++ b/webapp/src/components/BootstrapAlert.vue @@ -1,44 +1,50 @@ diff --git a/webapp/src/components/DevInfo.vue b/webapp/src/components/DevInfo.vue index 024566ad3..7a998e1c0 100644 --- a/webapp/src/components/DevInfo.vue +++ b/webapp/src/components/DevInfo.vue @@ -1,8 +1,7 @@