diff --git a/include/Configuration.h b/include/Configuration.h index cd810d84b..f7a8e082b 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 @@ -30,14 +31,15 @@ #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 struct CHANNEL_CONFIG_T { uint16_t MaxChannelPower; @@ -61,22 +63,66 @@ struct INVERTER_CONFIG_T { 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 PowerMeterHttpConfig = struct POWERMETER_HTTP_PHASE_CONFIG_T; +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 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; struct CONFIG_T { struct { @@ -187,19 +233,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 { @@ -272,6 +313,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/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/Utils.h b/include/Utils.h index f81e73180..38905b289 100644 --- a/include/Utils.h +++ b/include/Utils.h @@ -3,6 +3,7 @@ #include #include +#include class Utils { public: @@ -12,4 +13,8 @@ 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); }; 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/defaults.h b/include/defaults.h index 32d1e54cd..c480f4d99 100644 --- a/include/defaults.h +++ b/include/defaults.h @@ -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 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/platformio.ini b/platformio.ini index 73e5d23c4..94fc53701 100644 --- a/platformio.ini +++ b/platformio.ini @@ -46,8 +46,7 @@ lib_deps = 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 diff --git a/src/Configuration.cpp b/src/Configuration.cpp index 9adda28d0..97f95bfae 100644 --- a/src/Configuration.cpp +++ b/src/Configuration.cpp @@ -6,7 +6,6 @@ #include "MessageOutput.h" #include "Utils.h" #include "defaults.h" -#include #include #include @@ -17,6 +16,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"); @@ -150,31 +206,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; @@ -241,6 +285,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); @@ -411,30 +518,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"]; 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 5f378602e..aef57a198 100644 --- a/src/Huawei_can.cpp +++ b/src/Huawei_can.cpp @@ -371,12 +371,12 @@ void HuaweiCanClass::loop() } } - if (PowerMeter.getLastPowerMeterUpdate() > _lastPowerMeterUpdateReceivedMillis && + 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.getLastPowerMeterUpdate(); + _lastPowerMeterUpdateReceivedMillis = PowerMeter.getLastUpdate(); // Calculate new power limit float newPowerLimit = -1 * round(PowerMeter.getPowerTotal()); diff --git a/src/PowerLimiter.cpp b/src/PowerLimiter.cpp index d9e280488..5851f3278 100644 --- a/src/PowerLimiter.cpp +++ b/src/PowerLimiter.cpp @@ -201,7 +201,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); } 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..fea0fea15 --- /dev/null +++ b/src/PowerMeterMqtt.cpp @@ -0,0 +1,114 @@ +// 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) { + 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) +{ + std::string value(reinterpret_cast(payload), len); + std::string logValue = value.substr(0, 32); + if (value.length() > logValue.length()) { logValue += "..."; } + + auto log= [topic](char const* format, auto&&... args) -> void { + MessageOutput.printf("[PowerMeterMqtt] Topic '%s': ", topic); + MessageOutput.printf(format, args...); + MessageOutput.println(); + }; + + if (strlen(cfg->JsonPath) == 0) { + try { + std::lock_guard l(_mutex); + *targetVariable = std::stof(value); + } + catch (std::invalid_argument const& e) { + return log("cannot parse payload '%s' as float", logValue.c_str()); + } + } + else { + JsonDocument json; + + const DeserializationError error = deserializeJson(json, value); + 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 = Utils::getJsonValueByPath(json, cfg->JsonPath); + if (!pathResolutionResult.second.isEmpty()) { + return log("%s", pathResolutionResult.second.c_str()); + } + + *targetVariable = pathResolutionResult.first; + } + + using Unit_t = PowerMeterMqttValue::Unit; + switch (cfg->PowerUnit) { + case Unit_t::MilliWatts: + *targetVariable /= 1000; + break; + case Unit_t::KiloWatts: + *targetVariable *= 1000; + break; + default: + break; + } + + if (cfg->SignInverted) { *targetVariable *= -1; } + + if (_verboseLogging) { + log("new value: %5.2f, total: %5.2f", *targetVariable, 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..40514427a --- /dev/null +++ b/src/PowerMeterSerialSdm.cpp @@ -0,0 +1,204 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#include "PowerMeterSerialSdm.h" +#include "PinMapping.h" +#include "MessageOutput.h" +#include "SerialPortManager.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/Utils.cpp b/src/Utils.cpp index 6abe4dd19..b7b42f567 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -92,3 +92,77 @@ void Utils::removeAllFiles() file = root.getNextFileName(); } } + +/* OpenDTU-OnBatter-specific utils go here: */ +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()) { + snprintf(errBuffer, kErrBufferSize, "Value '%s' at JSON path '%s' is not " + "of the expected type", value.as().c_str(), path.c_str()); + return { T(), String(errBuffer) }; + } + + return { value.as(), "" }; +} + +template std::pair Utils::getJsonValueByPath(JsonDocument const& root, String const& path); diff --git a/src/WebApi_powermeter.cpp b/src/WebApi_powermeter.cpp index 8ca492b01..b3276d4fd 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,22 +26,8 @@ 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) @@ -52,32 +39,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 +85,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 (!phase.containsKey("timeout") - || phase["timeout"].as() <= 0) { - retMsg["message"] = "Timeout must be greater than 0 ms!"; - response->setLength(); - request->send(response); + 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 (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 +140,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 +185,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_ws_live.cpp b/src/WebApi_ws_live.cpp index ab54d479f..85781bce4 100644 --- a/src/WebApi_ws_live.cpp +++ b/src/WebApi_ws_live.cpp @@ -98,12 +98,12 @@ void WebApiWsLiveClass::generateOnBatteryJsonResponse(JsonVariant& root, bool al 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(); } diff --git a/webapp/src/components/HttpRequestSettings.vue b/webapp/src/components/HttpRequestSettings.vue new file mode 100644 index 000000000..d3a69c2db --- /dev/null +++ b/webapp/src/components/HttpRequestSettings.vue @@ -0,0 +1,94 @@ + + + diff --git a/webapp/src/locales/de.json b/webapp/src/locales/de.json index 2a03539b4..6ffafcb0d 100644 --- a/webapp/src/locales/de.json +++ b/webapp/src/locales/de.json @@ -554,40 +554,53 @@ "PowerMeterConfiguration": "Stromzähler Konfiguration", "PowerMeterEnable": "Aktiviere Stromzähler", "VerboseLogging": "@:base.VerboseLogging", - "PowerMeterParameter": "Power Meter Parameter", "PowerMeterSource": "Stromzählertyp", - "MQTT": "MQTT Konfiguration", + "pollingInterval": "Abfrageintervall", + "seconds": "@:base.Seconds", "typeMQTT": "MQTT", - "typeSDM1ph": "SDM 1 phase (SDM120/220/230)", - "typeSDM3ph": "SDM 3 phase (SDM72/630)", - "typeHTTP": "HTTP(S) + JSON", - "typeSML": "SML (OBIS 16.7.0)", + "typeSDM1ph": "SDM mit 1 Phase (SDM120/220/230)", + "typeSDM3ph": "SDM mit 3 Phasen (SDM72/630)", + "typeHTTP_JSON": "HTTP(S) + JSON", + "typeSML": "SML/OBIS via serieller Verbindung (z.B. Hichi TTL)", "typeSMAHM2": "SMA Homemanager 2.0", - "MqttTopicPowerMeter1": "MQTT topic - Stromzähler #1", - "MqttTopicPowerMeter2": "MQTT topic - Stromzähler #2 (Optional)", - "MqttTopicPowerMeter3": "MQTT topic - Stromzähler #3 (Optional)", + "typeHTTP_SML": "HTTP(S) + SML (z.B. Tibber Pulse via Tibber Bridge)", + "MqttValue": "Konfiguration Wert {valueNumber}", + "MqttTopic": "MQTT Topic", + "mqttJsonPath": "Optional: JSON-Pfad", "SDM": "SDM-Stromzähler Konfiguration", - "sdmbaudrate": "Baudrate", "sdmaddress": "Modbus Adresse", - "HTTP": "HTTP(S) + JSON - Allgemeine Konfiguration", - "httpIndividualRequests": "Individuelle HTTP requests pro Phase", + "HTTP_JSON": "HTTP(S) + JSON - Allgemeine Konfiguration", + "httpIndividualRequests": "Individuelle HTTP(S) Anfragen pro Wert", "urlExamplesHeading": "Beispiele für URLs", - "jsonPathExamplesHeading": "Beispiele für JSON Pfade", + "jsonPathExamplesHeading": "Beispiele für JSON-Pfade", "jsonPathExamplesExplanation": "Die folgenden Pfade finden jeweils den Wert '123.4' im jeweiligen Beispiel-JSON.", - "httpUrlDescription": "Die URL muss mit http:// oder https:// beginnen. Manche Zeichen wie Leerzeichen und = müssen mit URL-Kodierung kodiert werden (%xx). Achtung: Ein Überprüfung von SSL Server Zertifikaten ist nicht implementiert (MITM-Attacken sind möglich)!.", - "httpPhase": "HTTP(S) + JSON Konfiguration - Phase {phaseNumber}", - "httpEnabled": "Phase aktiviert", - "httpUrl": "URL", - "httpHeaderKey": "Optional: HTTP request header - Key", - "httpHeaderKeyDescription": "Ein individueller HTTP request header kann hier definiert werden. Das kann z.B. verwendet werden um einen eigenen Authorization header mitzugeben.", - "httpHeaderValue": "Optional: HTTP request header - Wert", - "httpJsonPath": "JSON Pfad", - "httpJsonPathDescription": "Anwendungsspezifischer JSON-Pfad um den Leistungswert in the HTTP(S) Antwort zu finden, z.B. 'power/total/watts' oder nur 'total'.", - "httpUnit": "Einheit", - "httpSignInverted": "Vorzeichen umkehren", - "httpSignInvertedHint": "Positive Werte werden als Leistungsabnahme aus dem Netz interpretiert. Diese Option muss aktiviert werden, wenn das Vorzeichen des Wertes die gegenteilige Bedeutung hat.", - "httpTimeout": "Timeout", - "testHttpRequest": "Testen" + "httpValue": "Konfiguration Wert {valueNumber}", + "httpEnabled": "Wert aktiviert", + "valueJsonPath": "JSON-Pfad", + "valueJsonPathDescription": "Anwendungsspezifischer JSON-Pfad um den Leistungswert in den JSON Nutzdatzen zu finden, z.B. 'power/total/watts' oder nur 'total'.", + "valueUnit": "Einheit", + "valueSignInverted": "Vorzeichen umkehren", + "valueSignInvertedHint": "Positive Werte werden als Leistungsabnahme aus dem Netz interpretiert. Diese Option muss aktiviert werden, wenn das Vorzeichen des Wertes die gegenteilige Bedeutung hat.", + "testHttpJsonHeader": "Konfiguration testen", + "testHttpJsonRequest": "HTTP(S)-Anfrage(n) senden und Antwort(en) verarbeiten", + "testHttpSmlHeader": "Konfiguration testen", + "testHttpSmlRequest": "HTTP(S)-Anfrage senden und Antwort verarbeiten", + "HTTP_SML": "HTTP(S) + SML - Konfiguration" + }, + "httprequestsettings": { + "url": "URL", + "urlDescription": "Die URL muss mit 'http://' oder 'https://' beginnen. Zeichen wie Leerzeichen und = müssen mit URL-kodiert werden (%xx). Achtung: Eine Überprüfung von SSL-Server-Zertifikaten ist nicht implementiert (MITM-Attacken sind möglich)!.", + "authorization": "Authentifizierungsverfahren", + "authTypeNone": "Ohne", + "authTypeBasic": "Basic", + "authTypeDigest": "Digest", + "username": "Benutzername", + "password": "Passwort", + "headerKey": "HTTP Header - Name", + "headerKeyDescription": "Optional. Ein benutzerdefinierter HTTP header kann definiert werden. Nützlich um z.B. ein (zusätzlichen) Authentifizierungstoken zu übermitteln.", + "headerValue": "HTTP Header - Wert", + "timeout": "Zeitüberschreitung", + "milliSeconds": "Millisekunden" }, "powerlimiteradmin": { "PowerLimiterSettings": "Dynamic Power Limiter Einstellungen", diff --git a/webapp/src/locales/en.json b/webapp/src/locales/en.json index 68a29e791..e84ffb2a0 100644 --- a/webapp/src/locales/en.json +++ b/webapp/src/locales/en.json @@ -556,44 +556,53 @@ "PowerMeterConfiguration": "Power Meter Configuration", "PowerMeterEnable": "Enable Power Meter", "VerboseLogging": "@:base.VerboseLogging", - "PowerMeterParameter": "Power Meter Parameter", - "PowerMeterSource": "Power Meter type", - "MQTT": "MQTT Parameter", + "PowerMeterSource": "Power Meter Type", + "pollingInterval": "Polling Interval", + "seconds": "@:base.Seconds", "typeMQTT": "MQTT", - "typeSDM1ph": "SDM 1 phase (SDM120/220/230)", - "typeSDM3ph": "SDM 3 phase (SDM72/630)", - "typeHTTP": "HTTP(s) + JSON", - "typeSML": "SML (OBIS 16.7.0)", + "typeSDM1ph": "SDM for 1 phase (SDM120/220/230)", + "typeSDM3ph": "SDM for 3 phases (SDM72/630)", + "typeHTTP_JSON": "HTTP(S) + JSON", + "typeSML": "SML/OBIS via serial connection (e.g. Hichi TTL)", "typeSMAHM2": "SMA Homemanager 2.0", - "MqttTopicPowerMeter1": "MQTT topic - Power meter #1", - "MqttTopicPowerMeter2": "MQTT topic - Power meter #2", - "MqttTopicPowerMeter3": "MQTT topic - Power meter #3", + "typeHTTP_SML": "HTTP(S) + SML (e.g. Tibber Pulse via Tibber Bridge)", + "MqttValue": "Value {valueNumber} Configuration", + "mqttJsonPath": "Optional: JSON Path", + "MqttTopic": "MQTT Topic", "SDM": "SDM-Power Meter Parameter", - "sdmbaudrate": "Baudrate", "sdmaddress": "Modbus Address", - "HTTP": "HTTP(S) + Json - General configuration", - "httpIndividualRequests": "Individual HTTP requests per phase", + "HTTP": "HTTP(S) + JSON - General configuration", + "httpIndividualRequests": "Individual HTTP(S) requests per value", "urlExamplesHeading": "URL Examples", "jsonPathExamplesHeading": "JSON Path Examples", "jsonPathExamplesExplanation": "The following paths each find the value '123.4' in the respective example JSON.", - "httpPhase": "HTTP(S) + Json configuration - Phase {phaseNumber}", - "httpEnabled": "Phase enabled", - "httpUrl": "URL", - "httpUrlDescription": "URL must start with http:// or https://. Some characters like spaces and = have to be encoded with URL encoding (%xx). Warning: SSL server certificate check is not implemented (MITM attacks are possible)!", - "httpAuthorization": "Authorization Type", - "httpUsername": "Username", - "httpPassword": "Password", - "httpHeaderKey": "Optional: HTTP request header - Key", - "httpHeaderKeyDescription": "A custom HTTP request header can be defined. Might be useful if you have to send something like a custom Authorization header.", - "httpHeaderValue": "Optional: HTTP request header - Value", - "httpJsonPath": "JSON path", - "httpJsonPathDescription": "Application specific JSON path to find the power value in the HTTP(S) response, e.g., 'power/total/watts' or simply 'total'.", - "httpUnit": "Unit", - "httpSignInverted": "Change Sign", - "httpSignInvertedHint": "Is is expected that positive values denote power usage from the grid. Check this option if the sign of this value has the opposite meaning.", - "httpTimeout": "Timeout", - "testHttpRequest": "Run test", - "milliSeconds": "ms" + "httpValue": "Value {valueNumber} Configuration", + "httpEnabled": "Value Enabled", + "valueJsonPath": "JSON Path", + "valueJsonPathDescription": "Application specific JSON path to find the power value in the JSON payload, e.g., 'power/total/watts' or simply 'total'.", + "valueUnit": "Unit", + "valueSignInverted": "Change Sign", + "valueSignInvertedHint": "Is is expected that positive values denote power usage from the grid. Check this option if the sign of this value has the opposite meaning.", + "testHttpJsonHeader": "Test Configuration", + "testHttpJsonRequest": "Send HTTP(S) request(s) and process response(s)", + "testHttpSmlHeader": "Test Configuration", + "testHttpSmlRequest": "Send HTTP(S) request and process response", + "HTTP_SML": "Configuration" + }, + "httprequestsettings": { + "url": "URL", + "urlDescription": "URL must start with 'http://' or 'https://'. Characters like spaces and '=' have to be URL-encoded (%xx). Warning: SSL server certificate check is not implemented (MITM attacks are possible)!", + "authorization": "Authorization Type", + "authTypeNone": "None", + "authTypeBasic": "Basic", + "authTypeDigest": "Digest", + "username": "Username", + "password": "Password", + "headerKey": "HTTP Header - Key", + "headerKeyDescription": "Optional. A custom HTTP header key-value pair can be defined. Useful, e.g., to send an (additional) authentication token.", + "headerValue": "HTTP Header - Value", + "timeout": "Timeout", + "milliSeconds": "Milliseconds" }, "powerlimiteradmin": { "PowerLimiterSettings": "Dynamic Power Limiter Settings", diff --git a/webapp/src/types/HttpRequestConfig.ts b/webapp/src/types/HttpRequestConfig.ts new file mode 100644 index 000000000..465924578 --- /dev/null +++ b/webapp/src/types/HttpRequestConfig.ts @@ -0,0 +1,9 @@ +export interface HttpRequestConfig { + url: string; + auth_type: number; + username: string; + password: string; + header_key: string; + header_value: string; + timeout: number; +} diff --git a/webapp/src/types/PowerMeterConfig.ts b/webapp/src/types/PowerMeterConfig.ts index a8ceb4f78..e1f6892ec 100644 --- a/webapp/src/types/PowerMeterConfig.ts +++ b/webapp/src/types/PowerMeterConfig.ts @@ -1,28 +1,47 @@ -export interface PowerMeterHttpPhaseConfig { - index: number; +import type { HttpRequestConfig } from '@/types/HttpRequestConfig'; + +export interface PowerMeterMqttValue { + topic: string; + json_path: string; + unit: number; + sign_inverted: boolean; +} + +export interface PowerMeterMqttConfig { + values: Array; +} + +export interface PowerMeterSerialSdmConfig { + polling_interval: number; + address: number; +}; + +export interface PowerMeterHttpJsonValue { + http_request: HttpRequestConfig; enabled: boolean; - url: string; - auth_type: number; - username: string; - password: string; - header_key: string; - header_value: string; json_path: string; - timeout: number; unit: number; sign_inverted: boolean; } +export interface PowerMeterHttpJsonConfig { + polling_interval: number; + individual_requests: boolean; + values: Array; +} + +export interface PowerMeterHttpSmlConfig { + polling_interval: number; + http_request: HttpRequestConfig; +} + export interface PowerMeterConfig { enabled: boolean; verbose_logging: boolean; source: number; interval: number; - mqtt_topic_powermeter_1: string; - mqtt_topic_powermeter_2: string; - mqtt_topic_powermeter_3: string; - sdmbaudrate: number; - sdmaddress: number; - http_individual_requests: boolean; - http_phases: Array; + mqtt: PowerMeterMqttConfig; + serial_sdm: PowerMeterSerialSdmConfig; + http_json: PowerMeterHttpJsonConfig; + http_sml: PowerMeterHttpSmlConfig; } diff --git a/webapp/src/views/PowerMeterAdminView.vue b/webapp/src/views/PowerMeterAdminView.vue index ffe0868ce..f819463ef 100644 --- a/webapp/src/views/PowerMeterAdminView.vue +++ b/webapp/src/views/PowerMeterAdminView.vue @@ -7,25 +7,20 @@
-
- -
-
- -
-
-
+ + + type="checkbox" wide />
- -
- @@ -35,189 +30,200 @@
- -
- -
-
- -
-
+
+ +
-
- -
-
- -
-
-
+ +
+ + + -
- -
-
- + + +
+ +
+
-
- + + + +
-
- -
-
- -
-
-
-
- -
-
- -
-
-
+ + +
+ + - - - + + -
-
- - -
- -
- -
-
-
- - - -
+
- - - - - -
+ - + :tooltip="$t('powermeteradmin.valueJsonPathDescription')" + wide />
-
- -
-
+ + + + +
+ +
+ + + {{ testHttpJsonRequestAlert.message }} + +
+
+ +
+ + + + + + + + + +
+
- - {{ testHttpRequestAlert[index].message }} + + {{ testHttpSmlRequestAlert.message }} -
@@ -235,8 +241,9 @@ import BootstrapAlert from "@/components/BootstrapAlert.vue"; import CardElement from '@/components/CardElement.vue'; import FormFooter from '@/components/FormFooter.vue'; import InputElement from '@/components/InputElement.vue'; +import HttpRequestSettings from '@/components/HttpRequestSettings.vue'; import { handleResponse, authHeader } from '@/utils/authentication'; -import type { PowerMeterHttpPhaseConfig, PowerMeterConfig } from "@/types/PowerMeterConfig"; +import type { PowerMeterConfig } from "@/types/PowerMeterConfig"; export default defineComponent({ components: { @@ -244,6 +251,7 @@ export default defineComponent({ BootstrapAlert, CardElement, FormFooter, + HttpRequestSettings, InputElement }, data() { @@ -254,19 +262,21 @@ export default defineComponent({ { key: 0, value: this.$t('powermeteradmin.typeMQTT') }, { key: 1, value: this.$t('powermeteradmin.typeSDM1ph') }, { key: 2, value: this.$t('powermeteradmin.typeSDM3ph') }, - { key: 3, value: this.$t('powermeteradmin.typeHTTP') }, + { key: 3, value: this.$t('powermeteradmin.typeHTTP_JSON') }, { key: 4, value: this.$t('powermeteradmin.typeSML') }, { key: 5, value: this.$t('powermeteradmin.typeSMAHM2') }, + { key: 6, value: this.$t('powermeteradmin.typeHTTP_SML') }, ], - powerMeterAuthList: [ - { key: 0, value: "None" }, - { key: 1, value: "Basic" }, - { key: 2, value: "Digest" }, + unitTypeList: [ + { key: 1, value: "mW" }, + { key: 0, value: "W" }, + { key: 2, value: "kW" }, ], alertMessage: "", alertType: "info", showAlert: false, - testHttpRequestAlert: [{message: "", type: "", show: false}] as { message: string; type: string; show: boolean; }[] + testHttpJsonRequestAlert: {message: "", type: "", show: false} as { message: string; type: string; show: boolean; }, + testHttpSmlRequestAlert: {message: "", type: "", show: false} as { message: string; type: string; show: boolean; } }; }, created() { @@ -280,14 +290,6 @@ export default defineComponent({ .then((data) => { this.powerMeterConfigList = data; this.dataLoading = false; - - for (let i = 0; i < this.powerMeterConfigList.http_phases.length; i++) { - this.testHttpRequestAlert.push({ - message: "", - type: "", - show: false, - }); - } }); }, savePowerMeterConfig(e: Event) { @@ -311,27 +313,43 @@ export default defineComponent({ } ); }, - testHttpRequest(index: number) { - let phaseConfig:PowerMeterHttpPhaseConfig; - - if (this.powerMeterConfigList.http_individual_requests) { - phaseConfig = this.powerMeterConfigList.http_phases[index]; - } else { - phaseConfig = { ...this.powerMeterConfigList.http_phases[0] }; - phaseConfig.index = this.powerMeterConfigList.http_phases[index].index; - phaseConfig.json_path = this.powerMeterConfigList.http_phases[index].json_path; - } - - this.testHttpRequestAlert[index] = { - message: "Sending HTTP request...", + testHttpJsonRequest() { + this.testHttpJsonRequestAlert = { + message: "Triggering HTTP request...", type: "info", show: true, }; const formData = new FormData(); - formData.append("data", JSON.stringify(phaseConfig)); + formData.append("data", JSON.stringify(this.powerMeterConfigList)); + + fetch("/api/powermeter/testhttpjsonrequest", { + method: "POST", + headers: authHeader(), + body: formData, + }) + .then((response) => handleResponse(response, this.$emitter, this.$router)) + .then( + (response) => { + this.testHttpJsonRequestAlert = { + message: response.message, + type: response.type, + show: true, + }; + } + ) + }, + testHttpSmlRequest() { + this.testHttpSmlRequestAlert = { + message: "Triggering HTTP request...", + type: "info", + show: true, + }; + + const formData = new FormData(); + formData.append("data", JSON.stringify(this.powerMeterConfigList)); - fetch("/api/powermeter/testhttprequest", { + fetch("/api/powermeter/testhttpsmlrequest", { method: "POST", headers: authHeader(), body: formData, @@ -339,7 +357,7 @@ export default defineComponent({ .then((response) => handleResponse(response, this.$emitter, this.$router)) .then( (response) => { - this.testHttpRequestAlert[index] = { + this.testHttpSmlRequestAlert = { message: response.message, type: response.type, show: true,