Merge PR #1077 from helgeerbe/powermeter-refactoring

this PowerMeter refactoring tackles many issues and prepares to solve many more.
This commit is contained in:
Bernhard Kirchen 2024-07-10 21:20:39 +02:00 committed by GitHub
commit e358513495
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
45 changed files with 2894 additions and 1331 deletions

View File

@ -3,6 +3,7 @@
#include "PinMapping.h" #include "PinMapping.h"
#include <cstdint> #include <cstdint>
#include <ArduinoJson.h>
#define CONFIG_FILENAME "/config.json" #define CONFIG_FILENAME "/config.json"
#define CONFIG_VERSION 0x00011c00 // 0.1.28 // make sure to clean all after change #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 DEV_MAX_MAPPING_NAME_STRLEN 63
#define POWERMETER_MAX_PHASES 3 #define HTTP_REQUEST_MAX_URL_STRLEN 1024
#define POWERMETER_MAX_HTTP_URL_STRLEN 1024 #define HTTP_REQUEST_MAX_USERNAME_STRLEN 64
#define POWERMETER_MAX_USERNAME_STRLEN 64 #define HTTP_REQUEST_MAX_PASSWORD_STRLEN 64
#define POWERMETER_MAX_PASSWORD_STRLEN 64 #define HTTP_REQUEST_MAX_HEADER_KEY_STRLEN 64
#define POWERMETER_MAX_HTTP_HEADER_KEY_STRLEN 64 #define HTTP_REQUEST_MAX_HEADER_VALUE_STRLEN 256
#define POWERMETER_MAX_HTTP_HEADER_VALUE_STRLEN 256
#define POWERMETER_MAX_HTTP_JSON_PATH_STRLEN 256 #define POWERMETER_MQTT_MAX_VALUES 3
#define POWERMETER_HTTP_TIMEOUT 1000 #define POWERMETER_HTTP_JSON_MAX_VALUES 3
#define POWERMETER_HTTP_JSON_MAX_PATH_STRLEN 256
struct CHANNEL_CONFIG_T { struct CHANNEL_CONFIG_T {
uint16_t MaxChannelPower; uint16_t MaxChannelPower;
@ -61,22 +63,66 @@ struct INVERTER_CONFIG_T {
CHANNEL_CONFIG_T channel[INV_MAX_CHAN_COUNT]; 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 Auth { None, Basic, Digest };
enum Unit { Watts = 0, MilliWatts = 1, KiloWatts = 2 };
bool Enabled;
char Url[POWERMETER_MAX_HTTP_URL_STRLEN + 1];
Auth AuthType; Auth AuthType;
char Username[POWERMETER_MAX_USERNAME_STRLEN +1];
char Password[POWERMETER_MAX_USERNAME_STRLEN +1]; char Username[HTTP_REQUEST_MAX_USERNAME_STRLEN + 1];
char HeaderKey[POWERMETER_MAX_HTTP_HEADER_KEY_STRLEN + 1]; char Password[HTTP_REQUEST_MAX_PASSWORD_STRLEN + 1];
char HeaderValue[POWERMETER_MAX_HTTP_HEADER_VALUE_STRLEN + 1]; char HeaderKey[HTTP_REQUEST_MAX_HEADER_KEY_STRLEN + 1];
char HeaderValue[HTTP_REQUEST_MAX_HEADER_VALUE_STRLEN + 1];
uint16_t Timeout; 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; Unit PowerUnit;
bool SignInverted; 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 CONFIG_T {
struct { struct {
@ -187,19 +233,14 @@ struct CONFIG_T {
bool UpdatesOnly; bool UpdatesOnly;
} Vedirect; } Vedirect;
struct { struct PowerMeterConfig {
bool Enabled; bool Enabled;
bool VerboseLogging; bool VerboseLogging;
uint32_t Interval;
uint32_t Source; uint32_t Source;
char MqttTopicPowerMeter1[MQTT_MAX_TOPIC_STRLEN + 1]; PowerMeterMqttConfig Mqtt;
char MqttTopicPowerMeter2[MQTT_MAX_TOPIC_STRLEN + 1]; PowerMeterSerialSdmConfig SerialSdm;
char MqttTopicPowerMeter3[MQTT_MAX_TOPIC_STRLEN + 1]; PowerMeterHttpJsonConfig HttpJson;
uint32_t SdmBaudrate; PowerMeterHttpSmlConfig HttpSml;
uint32_t SdmAddress;
uint32_t HttpInterval;
bool HttpIndividualRequests;
PowerMeterHttpConfig Http_Phase[POWERMETER_MAX_PHASES];
} PowerMeter; } PowerMeter;
struct { struct {
@ -272,6 +313,18 @@ public:
INVERTER_CONFIG_T* getFreeInverterSlot(); INVERTER_CONFIG_T* getFreeInverterSlot();
INVERTER_CONFIG_T* getInverterConfig(const uint64_t serial); INVERTER_CONFIG_T* getInverterConfig(const uint64_t serial);
void deleteInverterById(const uint8_t id); 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; extern ConfigurationClass Configuration;

77
include/HttpGetter.h Normal file
View File

@ -0,0 +1,77 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "Configuration.h"
#include <memory>
#include <vector>
#include <utility>
#include <string>
#include <HTTPClient.h>
#include <WiFiClient.h>
using up_http_client_t = std::unique_ptr<HTTPClient>;
using sp_wifi_client_t = std::shared_ptr<WiFiClient>;
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<typename... Args>
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<std::pair<std::string, std::string>> _additionalHeaders;
};

View File

@ -1,34 +0,0 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <stdint.h>
#include <Arduino.h>
#include <HTTPClient.h>
#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;

View File

@ -1,78 +1,27 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
#pragma once #pragma once
#include "Configuration.h" #include "PowerMeterProvider.h"
#include <espMqttClient.h>
#include <Arduino.h>
#include <map>
#include <list>
#include <mutex>
#include "SDM.h"
#include "sml.h"
#include <TaskSchedulerDeclarations.h> #include <TaskSchedulerDeclarations.h>
#include <SoftwareSerial.h> #include <memory>
#include <mutex>
typedef struct {
const unsigned char OBIS[6];
void (*Fn)(double&);
float* Arg;
} OBISHandler;
class PowerMeterClass { class PowerMeterClass {
public: public:
enum class Source : unsigned {
MQTT = 0,
SDM1PH = 1,
SDM3PH = 2,
HTTP = 3,
SML = 4,
SMAHM2 = 5
};
void init(Scheduler& scheduler); void init(Scheduler& scheduler);
float getPowerTotal(bool forceUpdate = true);
uint32_t getLastPowerMeterUpdate(); void updateSettings();
bool isDataValid();
float getPowerTotal() const;
uint32_t getLastUpdate() const;
bool isDataValid() const;
private: private:
void loop(); 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; 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<String, float*> _mqttSubscriptions;
mutable std::mutex _mutex; mutable std::mutex _mutex;
std::unique_ptr<PowerMeterProvider> _upProvider = nullptr;
static char constexpr _sdmSerialPortOwner[] = "SDM power meter";
std::unique_ptr<HardwareSerial> _upSdmSerial = nullptr;
std::unique_ptr<SDM> _upSdm = nullptr;
std::unique_ptr<SoftwareSerial> _upSmlSerial = nullptr;
void readPowerMeter();
bool smlReadLoop();
const std::list<OBISHandler> 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}
};
}; };
extern PowerMeterClass PowerMeter; extern PowerMeterClass PowerMeter;

View File

@ -0,0 +1,53 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <atomic>
#include <array>
#include <variant>
#include <memory>
#include <condition_variable>
#include <mutex>
#include <stdint.h>
#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<float, POWERMETER_HTTP_JSON_MAX_VALUES>;
using poll_result_t = std::variant<power_values_t, String>;
poll_result_t poll();
private:
static void pollingLoopHelper(void* context);
std::atomic<bool> _taskDone;
void pollingLoop();
PowerMeterHttpJsonConfig const _cfg;
uint32_t _lastPoll = 0;
mutable std::mutex _valueMutex;
power_values_t _powerValues;
std::array<std::unique_ptr<HttpGetter>, POWERMETER_HTTP_JSON_MAX_VALUES> _httpGetters;
TaskHandle_t _taskHandle = nullptr;
bool _stopPolling;
mutable std::mutex _pollingMutex;
std::condition_variable _cv;
};

View File

@ -0,0 +1,45 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <atomic>
#include <memory>
#include <condition_variable>
#include <mutex>
#include <stdint.h>
#include <Arduino.h>
#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<bool> _taskDone;
void pollingLoop();
PowerMeterHttpSmlConfig const _cfg;
uint32_t _lastPoll = 0;
std::unique_ptr<HttpGetter> _upHttpGetter;
TaskHandle_t _taskHandle = nullptr;
bool _stopPolling;
mutable std::mutex _pollingMutex;
std::condition_variable _cv;
};

37
include/PowerMeterMqtt.h Normal file
View File

@ -0,0 +1,37 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "Configuration.h"
#include "PowerMeterProvider.h"
#include <espMqttClient.h>
#include <vector>
#include <mutex>
#include <array>
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<float, POWERMETER_MQTT_MAX_VALUES>;
power_values_t _powerValues;
std::vector<String> _mqttSubscriptions;
mutable std::mutex _mutex;
};

View File

@ -0,0 +1,51 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <atomic>
#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<uint32_t> _lastUpdate = 0;
mutable uint32_t _lastMqttPublish = 0;
};

View File

@ -0,0 +1,60 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <atomic>
#include <mutex>
#include <condition_variable>
#include <SoftwareSerial.h>
#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<std::mutex>& lock, uint16_t reg, float& targetVar);
std::atomic<bool> _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<SoftwareSerial> _upSdmSerial = nullptr;
std::unique_ptr<SDM> _upSdm = nullptr;
TaskHandle_t _taskHandle = nullptr;
bool _stopPolling;
mutable std::mutex _pollingMutex;
std::condition_variable _cv;
};

View File

@ -0,0 +1,44 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include "PowerMeterSml.h"
#include <SoftwareSerial.h>
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<bool> _taskDone;
void pollingLoop();
TaskHandle_t _taskHandle = nullptr;
bool _stopPolling;
mutable std::mutex _pollingMutex;
std::unique_ptr<SoftwareSerial> _upSmlSerial = nullptr;
};

69
include/PowerMeterSml.h Normal file
View File

@ -0,0 +1,69 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <list>
#include <mutex>
#include <optional>
#include <stdint.h>
#include <Arduino.h>
#include <HTTPClient.h>
#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<float> activePowerTotal = std::nullopt;
std::optional<float> activePowerL1 = std::nullopt;
std::optional<float> activePowerL2 = std::nullopt;
std::optional<float> activePowerL3 = std::nullopt;
std::optional<float> voltageL1 = std::nullopt;
std::optional<float> voltageL2 = std::nullopt;
std::optional<float> voltageL3 = std::nullopt;
std::optional<float> currentL1 = std::nullopt;
std::optional<float> currentL2 = std::nullopt;
std::optional<float> currentL3 = std::nullopt;
std::optional<float> energyImport = std::nullopt;
std::optional<float> energyExport = std::nullopt;
};
values_t _values;
values_t _cache;
using OBISHandler = struct {
uint8_t const OBIS[6];
void (*decoder)(float&);
std::optional<float>* target;
char const* name;
};
const std::list<OBISHandler> 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"}
};
};

View File

@ -5,17 +5,16 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <TaskSchedulerDeclarations.h> #include "PowerMeterProvider.h"
class SMA_HMClass { class PowerMeterUdpSmaHomeManager : public PowerMeterProvider {
public: public:
void init(Scheduler& scheduler, bool verboseLogging); ~PowerMeterUdpSmaHomeManager();
void loop();
void event1(); bool init() final;
float getPowerTotal() const { return _powerMeterPower; } void loop() final;
float getPowerL1() const { return _powerMeterL1; } float getPowerTotal() const final { return _powerMeterPower; }
float getPowerL2() const { return _powerMeterL2; } void doMqttPublish() const final;
float getPowerL3() const { return _powerMeterL3; }
private: private:
void Soutput(int kanal, int index, int art, int tarif, void Soutput(int kanal, int index, int art, int tarif,
@ -23,14 +22,10 @@ private:
uint8_t* decodeGroup(uint8_t* offset, uint16_t grouplen); uint8_t* decodeGroup(uint8_t* offset, uint16_t grouplen);
bool _verboseLogging = false;
float _powerMeterPower = 0.0; float _powerMeterPower = 0.0;
float _powerMeterL1 = 0.0; float _powerMeterL1 = 0.0;
float _powerMeterL2 = 0.0; float _powerMeterL2 = 0.0;
float _powerMeterL3 = 0.0; float _powerMeterL3 = 0.0;
uint32_t _previousMillis = 0; uint32_t _previousMillis = 0;
uint32_t _serial = 0; uint32_t _serial = 0;
Task _loopTask;
}; };
extern SMA_HMClass SMA_HM;

View File

@ -3,6 +3,7 @@
#include <ArduinoJson.h> #include <ArduinoJson.h>
#include <cstdint> #include <cstdint>
#include <utility>
class Utils { class Utils {
public: public:
@ -12,4 +13,8 @@ public:
static void restartDtu(); static void restartDtu();
static bool checkJsonAlloc(const JsonDocument& doc, const char* function, const uint16_t line); static bool checkJsonAlloc(const JsonDocument& doc, const char* function, const uint16_t line);
static void removeAllFiles(); static void removeAllFiles();
/* OpenDTU-OnBatter-specific utils go here: */
template<typename T>
static std::pair<T, String> getJsonValueByPath(JsonDocument const& root, String const& path);
}; };

View File

@ -14,8 +14,8 @@ private:
void onStatus(AsyncWebServerRequest* request); void onStatus(AsyncWebServerRequest* request);
void onAdminGet(AsyncWebServerRequest* request); void onAdminGet(AsyncWebServerRequest* request);
void onAdminPost(AsyncWebServerRequest* request); void onAdminPost(AsyncWebServerRequest* request);
void decodeJsonPhaseConfig(JsonObject const& json, PowerMeterHttpConfig& config) const; void onTestHttpJsonRequest(AsyncWebServerRequest* request);
void onTestHttpRequest(AsyncWebServerRequest* request); void onTestHttpSmlRequest(AsyncWebServerRequest* request);
AsyncWebServer* _server; AsyncWebServer* _server;
}; };

View File

@ -115,11 +115,12 @@
#define VEDIRECT_UPDATESONLY true #define VEDIRECT_UPDATESONLY true
#define POWERMETER_ENABLED false #define POWERMETER_ENABLED false
#define POWERMETER_INTERVAL 10 #define POWERMETER_POLLING_INTERVAL 10
#define POWERMETER_SOURCE 2 #define POWERMETER_SOURCE 0
#define POWERMETER_SDMBAUDRATE 9600
#define POWERMETER_SDMADDRESS 1 #define POWERMETER_SDMADDRESS 1
#define HTTP_REQUEST_TIMEOUT_MS 1000
#define POWERLIMITER_ENABLED false #define POWERLIMITER_ENABLED false
#define POWERLIMITER_SOLAR_PASSTHROUGH_ENABLED true #define POWERLIMITER_SOLAR_PASSTHROUGH_ENABLED true
#define POWERLIMITER_SOLAR_PASSTHROUGH_LOSSES 3 #define POWERLIMITER_SOLAR_PASSTHROUGH_LOSSES 3

View File

@ -80,7 +80,7 @@ void pushListBuffer(unsigned char byte)
void reduceList() void reduceList()
{ {
if (currentLevel <= MAX_TREE_SIZE && nodes[currentLevel] > 0) if (currentLevel < MAX_TREE_SIZE && nodes[currentLevel] > 0)
nodes[currentLevel]--; nodes[currentLevel]--;
} }
@ -171,7 +171,7 @@ void checkMagicByte(unsigned char &byte)
// Datatype Octet String // Datatype Octet String
setState(SML_HDATA, (byte & 0x0F) << 4); setState(SML_HDATA, (byte & 0x0F) << 4);
} }
else if (byte >= 0xF0 /*&& byte <= 0xFF*/) { else if (byte >= 0xF0) {
/* Datatype List of ...*/ /* Datatype List of ...*/
setState(SML_LISTEXTENDED, (byte & 0x0F) << 4); setState(SML_LISTEXTENDED, (byte & 0x0F) << 4);
} }
@ -189,7 +189,13 @@ void checkMagicByte(unsigned char &byte)
} }
} }
sml_states_t smlState(unsigned char &currentByte) void smlReset(void)
{
len = 4; // expect start sequence
currentState = SML_START;
}
sml_states_t smlState(unsigned char currentByte)
{ {
unsigned char size; unsigned char size;
if (len > 0) 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) { if (scaler < 0) {
while (scaler++) { 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; long long int val;
smlOBISByUnit(val, sc, SML_WATT_HOUR); smlOBISByUnit(val, sc, SML_WATT_HOUR);
@ -380,7 +386,7 @@ void smlOBISWh(double &wh)
smlPow(wh, sc); smlPow(wh, sc);
} }
void smlOBISW(double &w) void smlOBISW(float &w)
{ {
long long int val; long long int val;
smlOBISByUnit(val, sc, SML_WATT); smlOBISByUnit(val, sc, SML_WATT);
@ -388,7 +394,7 @@ void smlOBISW(double &w)
smlPow(w, sc); smlPow(w, sc);
} }
void smlOBISVolt(double &v) void smlOBISVolt(float &v)
{ {
long long int val; long long int val;
smlOBISByUnit(val, sc, SML_VOLT); smlOBISByUnit(val, sc, SML_VOLT);
@ -396,10 +402,26 @@ void smlOBISVolt(double &v)
smlPow(v, sc); smlPow(v, sc);
} }
void smlOBISAmpere(double &a) void smlOBISAmpere(float &a)
{ {
long long int val; long long int val;
smlOBISByUnit(val, sc, SML_AMPERE); smlOBISByUnit(val, sc, SML_AMPERE);
a = val; a = val;
smlPow(a, sc); 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);
}

View File

@ -92,15 +92,17 @@ typedef enum {
SML_COUNT = 255 SML_COUNT = 255
} sml_units_t; } 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); bool smlOBISCheck(const unsigned char *obis);
void smlOBISManufacturer(unsigned char *str, int maxSize); void smlOBISManufacturer(unsigned char *str, int maxSize);
void smlOBISByUnit(long long int &wh, signed char &scaler, sml_units_t unit); 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(float &wh);
void smlOBISWh(double &wh); void smlOBISW(float &w);
void smlOBISW(double &w); void smlOBISVolt(float &v);
void smlOBISVolt(double &v); void smlOBISAmpere(float &a);
void smlOBISAmpere(double &a); void smlOBISHertz(float &h);
void smlOBISDegree(float &d);
#endif #endif

View File

@ -1,6 +1,6 @@
/* Library for reading SDM 72/120/220/230/630 Modbus Energy meters. /* Library for reading SDM 72/120/220/230/630 Modbus Energy meters.
* Reading via Hardware or Software Serial library & rs232<->rs485 converter * 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/) * crc calculation by Jaime García (https://github.com/peninquen/Modbus-Energy-Monitor-Arduino/)
*/ */
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@ -60,7 +60,7 @@ void SDM::begin(void) {
#endif #endif
#else #else
#if defined ( ESP8266 ) || defined ( ESP32 ) #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 #else
sdmSer.begin(_baud); sdmSer.begin(_baud);
#endif #endif
@ -77,44 +77,67 @@ void SDM::begin(void) {
} }
float SDM::readVal(uint16_t reg, uint8_t node) { float SDM::readVal(uint16_t reg, uint8_t node) {
uint16_t temp; startReadVal(reg, node);
unsigned long resptime;
uint8_t sdmarr[FRAMESIZE] = {node, SDM_B_02, 0, 0, SDM_B_05, SDM_B_06, 0, 0, 0}; uint16_t readErr = SDM_ERR_STILL_WAITING;
float res = NAN;
while (readErr == SDM_ERR_STILL_WAITING) {
readErr = readValReady(node);
delay(1);
}
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);
}
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);
}
uint16_t SDM::readValReady(uint8_t node, uint8_t functionCode) {
uint16_t readErr = SDM_ERR_NO_ERROR; uint16_t readErr = SDM_ERR_NO_ERROR;
if (sdmSer.available() < FRAMESIZE && ((millis() - resptime) < msturnaround))
sdmarr[2] = highByte(reg); {
sdmarr[3] = lowByte(reg); return SDM_ERR_STILL_WAITING;
}
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
flush(); //read serial if any old data is available
dereSet(HIGH); //transmit to SDM -> DE Enable, /RE Disable (for control MAX485)
delay(2); //fix for issue (nan reading) by sjfaustino: https://github.com/reaper7/SDM_Energy_Meter/issues/7#issuecomment-272111524
sdmSer.write(sdmarr, FRAMESIZE - 1); //send 8 bytes
sdmSer.flush(); //clear out tx buffer
dereSet(LOW); //receive from SDM -> DE Disable, /RE Enable (for control MAX485)
resptime = millis();
while (sdmSer.available() < FRAMESIZE) { while (sdmSer.available() < FRAMESIZE) {
if (millis() - resptime > msturnaround) { if ((millis() - resptime) > msturnaround) {
readErr = SDM_ERR_TIMEOUT; //err debug (4) 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; break;
} }
yield(); delay(1);
} }
if (readErr == SDM_ERR_NO_ERROR) { //if no timeout... 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(); sdmarr[n] = sdmSer.read();
} }
if (sdmarr[0] == node && sdmarr[1] == SDM_B_02 && sdmarr[2] == SDM_REPLY_BYTE_COUNT) { if (sdmarr[0] == node &&
sdmarr[1] == functionCode &&
if ((calculateCRC(sdmarr, FRAMESIZE - 2)) == ((sdmarr[8] << 8) | sdmarr[7])) { //calculate crc from first 7 bytes and compare with received crc (bytes 7 & 8) sdmarr[2] == SDM_REPLY_BYTE_COUNT) {
((uint8_t*)&res)[3]= sdmarr[3]; if (!validChecksum(sdmarr, FRAMESIZE)) {
((uint8_t*)&res)[2]= sdmarr[4];
((uint8_t*)&res)[1]= sdmarr[5];
((uint8_t*)&res)[0]= sdmarr[6];
} else {
readErr = SDM_ERR_CRC_ERROR; //err debug (1) 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 ) #if !defined ( USE_HARDWARESERIAL )
sdmSer.stopListening(); //disable softserial rx interrupt // sdmSer.stopListening(); //disable softserial rx interrupt
#endif #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); 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 SDM::getErrCode(bool _clear) {
uint16_t _tmp = readingerrcode; uint16_t _tmp = readingerrcode;
if (_clear == true) if (_clear == true)
@ -224,7 +326,7 @@ uint16_t SDM::getMsTimeout() {
return (mstimeout); 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; uint16_t _crc, _flag;
_crc = 0xFFFF; _crc = 0xFFFF;
for (uint8_t i = 0; i < len; i++) { 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) { void SDM::flush(unsigned long _flushtime) {
unsigned long flushstart = millis(); unsigned long flushstart = millis();
while (sdmSer.available() || (millis() - flushstart < _flushtime)) { sdmSer.flush();
if (sdmSer.available()) //read serial if any old data is available 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(); sdmSer.read();
}
delay(1); delay(1);
available = sdmSer.available();
} }
} }
@ -252,3 +361,58 @@ void SDM::dereSet(bool _state) {
if (_dere_pin != NOT_A_PIN) if (_dere_pin != NOT_A_PIN)
digitalWrite(_dere_pin, _state); //receive from SDM -> DE Disable, /RE Enable (for control MAX485) 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();
}

View File

@ -1,6 +1,6 @@
/* Library for reading SDM 72/120/220/230/630 Modbus Energy meters. /* Library for reading SDM 72/120/220/230/630 Modbus Energy meters.
* Reading via Hardware or Software Serial library & rs232<->rs485 converter * 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/) * crc calculation by Jaime García (https://github.com/peninquen/Modbus-Energy-Monitor-Arduino/)
*/ */
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@ -66,36 +66,47 @@
#endif #endif
#if !defined ( WAITING_TURNAROUND_DELAY ) #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 #endif
#if !defined ( RESPONSE_TIMEOUT ) #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 #endif
#if !defined ( SDM_MIN_DELAY ) #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 #endif
#if !defined ( SDM_MAX_DELAY ) #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 #endif
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
#define SDM_ERR_NO_ERROR 0 // no error #define SDM_ERR_NO_ERROR 0 // no error
#define SDM_ERR_CRC_ERROR 1 // crc error #define SDM_ERR_ILLEGAL_FUNCTION 1
#define SDM_ERR_WRONG_BYTES 2 // bytes b0,b1 or b2 wrong #define SDM_ERR_ILLEGAL_DATA_ADDRESS 2
#define SDM_ERR_NOT_ENOUGHT_BYTES 3 // not enough bytes from sdm #define SDM_ERR_ILLEGAL_DATA_VALUE 3
#define SDM_ERR_TIMEOUT 4 // timeout #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 FRAMESIZE 9 // size of out/in array
#define SDM_REPLY_BYTE_COUNT 0x04 // number of bytes with data #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_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_05 0x00 // BYTE 5
#define SDM_B_06 0x02 // BYTE 6 #define SDM_B_06 0x02 // BYTE 6
// BYTES 3 & 4 (BELOW) // BYTES 3 & 4 (BELOW)
@ -151,6 +162,8 @@
#define SDM_MAXIMUM_TOTAL_SYSTEM_VA_DEMAND 0x0066 // VA | 1 | | | | | | | #define SDM_MAXIMUM_TOTAL_SYSTEM_VA_DEMAND 0x0066 // VA | 1 | | | | | | |
#define SDM_NEUTRAL_CURRENT_DEMAND 0x0068 // A | 1 | | | | | | | #define SDM_NEUTRAL_CURRENT_DEMAND 0x0068 // A | 1 | | | | | | |
#define SDM_MAXIMUM_NEUTRAL_CURRENT 0x006A // 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_1_TO_LINE_2_VOLTS 0x00C8 // V | 1 | | | | | | 1 |
#define SDM_LINE_2_TO_LINE_3_VOLTS 0x00CA // 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 | #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_TOTAL_REACTIVE_ENERGY 0x0182 // kVArh | | 1 | | | | | |
#define SDM_CURRENT_RESETTABLE_IMPORT_ENERGY 0x0184 // kWh | | | | | | 1 | 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_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_KWH 0x018C // kWh | | | | | | | 1 |
#define SDM_NET_KVARH 0x018E // kVArh | | | | | | | 1 |
#define SDM_IMPORT_POWER 0x0500 // W | | | | | | 1 | 1 | #define SDM_IMPORT_POWER 0x0500 // W | | | | | | 1 | 1 |
#define SDM_EXPORT_POWER 0x0502 // W | | | | | | 1 | 1 | #define SDM_EXPORT_POWER 0x0502 // W | | | | | | 1 | 1 |
//--------------------------------------------------------------------------------------------------------------------------------------------------------------------- //---------------------------------------------------------------------------------------------------------------------------------------------------------------------
@ -229,6 +245,78 @@
//#define DEVNAME_POWER 0x0004 // W | 1 | //#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 { class SDM {
@ -252,6 +340,16 @@ class SDM {
void begin(void); void begin(void);
float readVal(uint16_t reg, uint8_t node = SDM_B_01); // read value from register = reg and from deviceId = node 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) 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 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) 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) uint16_t getMsTimeout(); // get current value of RESPONSE_TIMEOUT (ms)
private: private:
bool validChecksum(const uint8_t* data, size_t messageLength) const;
void modbusWrite(uint8_t* data, size_t messageLength);
#if defined ( USE_HARDWARESERIAL ) #if defined ( USE_HARDWARESERIAL )
HardwareSerial& sdmSer; HardwareSerial& sdmSer;
#else #else
@ -292,7 +395,9 @@ class SDM {
uint16_t mstimeout = RESPONSE_TIMEOUT; uint16_t mstimeout = RESPONSE_TIMEOUT;
uint32_t readingerrcount = 0; // total errors counter uint32_t readingerrcount = 0; // total errors counter
uint32_t readingsuccesscount = 0; // total success 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 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 void dereSet(bool _state = LOW); // for control MAX485 DE/RE pins, LOW receive from SDM, HIGH transmit to SDM
}; };

View File

@ -1,6 +1,6 @@
/* Library for reading SDM 72/120/220/230/630 Modbus Energy meters. /* Library for reading SDM 72/120/220/230/630 Modbus Energy meters.
* Reading via Hardware or Software Serial library & rs232<->rs485 converter * 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/) * 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) * define or undefine USE_HARDWARESERIAL (uncomment only one or none)
*/ */
//#undef USE_HARDWARESERIAL //#undef USE_HARDWARESERIAL
#define USE_HARDWARESERIAL //#define USE_HARDWARESERIAL
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@ -32,7 +32,7 @@
#if defined ( USE_HARDWARESERIAL ) #if defined ( USE_HARDWARESERIAL )
#if defined ( ESP32 ) #if defined ( ESP32 )
#define SDM_RX_PIN 13 #define SDM_RX_PIN 13
#define SDM_TX_PIN 32 #define SDM_TX_PIN 15
#endif #endif
#else #else
#if defined ( ESP8266 ) || defined ( ESP32 ) #if defined ( ESP8266 ) || defined ( ESP32 )

View File

@ -46,8 +46,7 @@ lib_deps =
buelowp/sunset @ 1.1.7 buelowp/sunset @ 1.1.7
https://github.com/arkhipenko/TaskScheduler#testing https://github.com/arkhipenko/TaskScheduler#testing
https://github.com/coryjfowler/MCP_CAN_lib https://github.com/coryjfowler/MCP_CAN_lib
plerup/EspSoftwareSerial @ ^8.0.1 plerup/EspSoftwareSerial @ ^8.2.0
https://github.com/dok-net/ghostl @ ^1.0.1
extra_scripts = extra_scripts =
pre:pio-scripts/auto_firmware_version.py pre:pio-scripts/auto_firmware_version.py

View File

@ -6,7 +6,6 @@
#include "MessageOutput.h" #include "MessageOutput.h"
#include "Utils.h" #include "Utils.h"
#include "defaults.h" #include "defaults.h"
#include <ArduinoJson.h>
#include <LittleFS.h> #include <LittleFS.h>
#include <nvs_flash.h> #include <nvs_flash.h>
@ -17,6 +16,63 @@ void ConfigurationClass::init()
memset(&config, 0x0, sizeof(config)); memset(&config, 0x0, sizeof(config));
} }
void ConfigurationClass::serializeHttpRequestConfig(HttpRequestConfig const& source, JsonObject& target)
{
JsonObject target_http_config = target["http_request"].to<JsonObject>();
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<JsonArray>();
for (size_t i = 0; i < POWERMETER_MQTT_MAX_VALUES; ++i) {
JsonObject t = values.add<JsonObject>();
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<JsonArray>();
for (size_t i = 0; i < POWERMETER_HTTP_JSON_MAX_VALUES; ++i) {
JsonObject t = values.add<JsonObject>();
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() bool ConfigurationClass::write()
{ {
File f = LittleFS.open(CONFIG_FILENAME, "w"); File f = LittleFS.open(CONFIG_FILENAME, "w");
@ -150,31 +206,19 @@ bool ConfigurationClass::write()
JsonObject powermeter = doc["powermeter"].to<JsonObject>(); JsonObject powermeter = doc["powermeter"].to<JsonObject>();
powermeter["enabled"] = config.PowerMeter.Enabled; powermeter["enabled"] = config.PowerMeter.Enabled;
powermeter["verbose_logging"] = config.PowerMeter.VerboseLogging; powermeter["verbose_logging"] = config.PowerMeter.VerboseLogging;
powermeter["interval"] = config.PowerMeter.Interval;
powermeter["source"] = config.PowerMeter.Source; 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<JsonArray>(); JsonObject powermeter_mqtt = powermeter["mqtt"].to<JsonObject>();
for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) { serializePowerMeterMqttConfig(config.PowerMeter.Mqtt, powermeter_mqtt);
JsonObject powermeter_phase = powermeter_http_phases.add<JsonObject>();
powermeter_phase["enabled"] = config.PowerMeter.Http_Phase[i].Enabled; JsonObject powermeter_serial_sdm = powermeter["serial_sdm"].to<JsonObject>();
powermeter_phase["url"] = config.PowerMeter.Http_Phase[i].Url; serializePowerMeterSerialSdmConfig(config.PowerMeter.SerialSdm, powermeter_serial_sdm);
powermeter_phase["auth_type"] = config.PowerMeter.Http_Phase[i].AuthType;
powermeter_phase["username"] = config.PowerMeter.Http_Phase[i].Username; JsonObject powermeter_http_json = powermeter["http_json"].to<JsonObject>();
powermeter_phase["password"] = config.PowerMeter.Http_Phase[i].Password; serializePowerMeterHttpJsonConfig(config.PowerMeter.HttpJson, powermeter_http_json);
powermeter_phase["header_key"] = config.PowerMeter.Http_Phase[i].HeaderKey;
powermeter_phase["header_value"] = config.PowerMeter.Http_Phase[i].HeaderValue; JsonObject powermeter_http_sml = powermeter["http_sml"].to<JsonObject>();
powermeter_phase["timeout"] = config.PowerMeter.Http_Phase[i].Timeout; serializePowerMeterHttpSmlConfig(config.PowerMeter.HttpSml, powermeter_http_sml);
powermeter_phase["json_path"] = config.PowerMeter.Http_Phase[i].JsonPath;
powermeter_phase["unit"] = config.PowerMeter.Http_Phase[i].PowerUnit;
powermeter_phase["sign_inverted"] = config.PowerMeter.Http_Phase[i].SignInverted;
}
JsonObject powerlimiter = doc["powerlimiter"].to<JsonObject>(); JsonObject powerlimiter = doc["powerlimiter"].to<JsonObject>();
powerlimiter["enabled"] = config.PowerLimiter.Enabled; powerlimiter["enabled"] = config.PowerLimiter.Enabled;
@ -241,6 +285,69 @@ bool ConfigurationClass::write()
return true; 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<JsonArray>();
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() bool ConfigurationClass::read()
{ {
File f = LittleFS.open(CONFIG_FILENAME, "r", false); File f = LittleFS.open(CONFIG_FILENAME, "r", false);
@ -411,30 +518,51 @@ bool ConfigurationClass::read()
JsonObject powermeter = doc["powermeter"]; JsonObject powermeter = doc["powermeter"];
config.PowerMeter.Enabled = powermeter["enabled"] | POWERMETER_ENABLED; config.PowerMeter.Enabled = powermeter["enabled"] | POWERMETER_ENABLED;
config.PowerMeter.VerboseLogging = powermeter["verbose_logging"] | VERBOSE_LOGGING; config.PowerMeter.VerboseLogging = powermeter["verbose_logging"] | VERBOSE_LOGGING;
config.PowerMeter.Interval = powermeter["interval"] | POWERMETER_INTERVAL;
config.PowerMeter.Source = powermeter["source"] | POWERMETER_SOURCE; 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"]; deserializePowerMeterMqttConfig(powermeter["mqtt"], config.PowerMeter.Mqtt);
for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) {
JsonObject powermeter_phase = powermeter_http_phases[i].as<JsonObject>();
config.PowerMeter.Http_Phase[i].Enabled = powermeter_phase["enabled"] | (i == 0); // process settings from legacy config if they are present
strlcpy(config.PowerMeter.Http_Phase[i].Url, powermeter_phase["url"] | "", sizeof(config.PowerMeter.Http_Phase[i].Url)); // TODO(schlimmchen): remove in early 2025.
config.PowerMeter.Http_Phase[i].AuthType = powermeter_phase["auth_type"] | PowerMeterHttpConfig::Auth::None; if (!powermeter["mqtt_topic_powermeter_1"].isNull()) {
strlcpy(config.PowerMeter.Http_Phase[i].Username, powermeter_phase["username"] | "", sizeof(config.PowerMeter.Http_Phase[i].Username)); auto& values = config.PowerMeter.Mqtt.Values;
strlcpy(config.PowerMeter.Http_Phase[i].Password, powermeter_phase["password"] | "", sizeof(config.PowerMeter.Http_Phase[i].Password)); strlcpy(values[0].Topic, powermeter["mqtt_topic_powermeter_1"], sizeof(values[0].Topic));
strlcpy(config.PowerMeter.Http_Phase[i].HeaderKey, powermeter_phase["header_key"] | "", sizeof(config.PowerMeter.Http_Phase[i].HeaderKey)); strlcpy(values[1].Topic, powermeter["mqtt_topic_powermeter_2"], sizeof(values[1].Topic));
strlcpy(config.PowerMeter.Http_Phase[i].HeaderValue, powermeter_phase["header_value"] | "", sizeof(config.PowerMeter.Http_Phase[i].HeaderValue)); strlcpy(values[2].Topic, powermeter["mqtt_topic_powermeter_3"], sizeof(values[2].Topic));
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; deserializePowerMeterSerialSdmConfig(powermeter["serial_sdm"], config.PowerMeter.SerialSdm);
config.PowerMeter.Http_Phase[i].SignInverted = powermeter_phase["sign_inverted"] | false;
// 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"]; JsonObject powerlimiter = doc["powerlimiter"];

View File

@ -294,7 +294,7 @@ void DisplayGraphicClass::loop()
_display->drawBox(0, y, _display->getDisplayWidth(), lineHeight); _display->drawBox(0, y, _display->getDisplayWidth(), lineHeight);
_display->setDrawColor(1); _display->setDrawColor(1);
auto acPower = PowerMeter.getPowerTotal(false); auto acPower = PowerMeter.getPowerTotal();
if (acPower > 999) { if (acPower > 999) {
snprintf(_fmtText, sizeof(_fmtText), i18n_meter_power_kw[_display_language], (acPower / 1000)); snprintf(_fmtText, sizeof(_fmtText), i18n_meter_power_kw[_display_language], (acPower / 1000));
} else { } else {

236
src/HttpGetter.cpp Normal file
View File

@ -0,0 +1,236 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "HttpGetter.h"
#include <WiFiClientSecure.h>
#include "mbedtls/sha256.h"
#include <base64.h>
#include <ESPmDNS.h>
template<typename... Args>
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<WiFiClientSecure>();
secureWifiClient->setInsecure();
_spWiFiClient = std::move(secureWifiClient);
} else {
_spWiFiClient = std::make_shared<WiFiClient>();
}
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<HTTPClient>();
// 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<const unsigned char*>(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 });
}

View File

@ -1,395 +0,0 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "Configuration.h"
#include "HttpPowerMeter.h"
#include "MessageOutput.h"
#include <WiFiClientSecure.h>
#include <ArduinoJson.h>
#include "mbedtls/sha256.h"
#include <base64.h>
#include <memory>
#include <ESPmDNS.h>
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> wifiClient;
bool https = protocol == "https";
if (https) {
auto secureWifiClient = std::make_unique<WiFiClientSecure>();
secureWifiClient->setInsecure();
wifiClient = std::move(secureWifiClient);
} else {
wifiClient = std::make_unique<WiFiClient>();
}
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<JsonVariantConst>();
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<JsonArrayConst>()) {
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<float>()) {
snprintf_P(httpPowerMeterError, sizeof(httpPowerMeterError),
PSTR("[HttpPowerMeter] not a float: '%s'"),
value.as<String>().c_str());
return false;
}
// this value is supposed to be in Watts and positive if energy is consumed.
power[phase] = value.as<float>();
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<const unsigned char*>(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;

View File

@ -371,12 +371,12 @@ void HuaweiCanClass::loop()
} }
} }
if (PowerMeter.getLastPowerMeterUpdate() > _lastPowerMeterUpdateReceivedMillis && if (PowerMeter.getLastUpdate() > _lastPowerMeterUpdateReceivedMillis &&
_autoPowerEnabledCounter > 0) { _autoPowerEnabledCounter > 0) {
// We have received a new PowerMeter value. Also we're _autoPowerEnabled // We have received a new PowerMeter value. Also we're _autoPowerEnabled
// So we're good to calculate a new limit // So we're good to calculate a new limit
_lastPowerMeterUpdateReceivedMillis = PowerMeter.getLastPowerMeterUpdate(); _lastPowerMeterUpdateReceivedMillis = PowerMeter.getLastUpdate();
// Calculate new power limit // Calculate new power limit
float newPowerLimit = -1 * round(PowerMeter.getPowerTotal()); float newPowerLimit = -1 * round(PowerMeter.getPowerTotal());

View File

@ -201,7 +201,7 @@ void PowerLimiterClass::loop()
// arrives. this can be the case for readings provided by networked meter // arrives. this can be the case for readings provided by networked meter
// readers, where a packet needs to travel through the network for some // readers, where a packet needs to travel through the network for some
// time after the actual measurement was done by the reader. // 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); return announceStatus(Status::PowerMeterPending);
} }

View File

@ -1,17 +1,12 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
/*
* Copyright (C) 2022 Thomas Basler and others
*/
#include "PowerMeter.h" #include "PowerMeter.h"
#include "Configuration.h" #include "Configuration.h"
#include "PinMapping.h" #include "PowerMeterHttpJson.h"
#include "HttpPowerMeter.h" #include "PowerMeterHttpSml.h"
#include "MqttSettings.h" #include "PowerMeterMqtt.h"
#include "NetworkSettings.h" #include "PowerMeterSerialSdm.h"
#include "MessageOutput.h" #include "PowerMeterSerialSml.h"
#include "SerialPortManager.h" #include "PowerMeterUdpSmaHomeManager.h"
#include <ctime>
#include <SMA_HM.h>
PowerMeterClass PowerMeter; PowerMeterClass PowerMeter;
@ -22,277 +17,75 @@ void PowerMeterClass::init(Scheduler& scheduler)
_loopTask.setIterations(TASK_FOREVER); _loopTask.setIterations(TASK_FOREVER);
_loopTask.enable(); _loopTask.enable();
_lastPowerMeterCheck = 0; updateSettings();
_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<Source>(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<HardwareSerial>(*oHwSerialPort);
_upSdmSerial->end(); // make sure the UART will be re-initialized
_upSdm = std::make_unique<SDM>(*_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<SoftwareSerial>();
_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;
}
} }
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<const char*>(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());
}
_lastPowerMeterUpdate = millis();
}
}
float PowerMeterClass::getPowerTotal(bool forceUpdate)
{
if (forceUpdate) {
CONFIG_T& config = Configuration.get();
if (config.PowerMeter.Enabled
&& (millis() - _lastPowerMeterUpdate) > (1000)) {
readPowerMeter();
}
}
std::lock_guard<std::mutex> l(_mutex);
return _powerMeter1Power + _powerMeter2Power + _powerMeter3Power;
}
uint32_t PowerMeterClass::getLastPowerMeterUpdate()
{ {
std::lock_guard<std::mutex> l(_mutex); std::lock_guard<std::mutex> l(_mutex);
return _lastPowerMeterUpdate;
if (_upProvider) { _upProvider.reset(); }
auto const& pmcfg = Configuration.get().PowerMeter;
if (!pmcfg.Enabled) { return; }
switch(static_cast<PowerMeterProvider::Type>(pmcfg.Source)) {
case PowerMeterProvider::Type::MQTT:
_upProvider = std::make_unique<PowerMeterMqtt>(pmcfg.Mqtt);
break;
case PowerMeterProvider::Type::SDM1PH:
_upProvider = std::make_unique<PowerMeterSerialSdm>(
PowerMeterSerialSdm::Phases::One, pmcfg.SerialSdm);
break;
case PowerMeterProvider::Type::SDM3PH:
_upProvider = std::make_unique<PowerMeterSerialSdm>(
PowerMeterSerialSdm::Phases::Three, pmcfg.SerialSdm);
break;
case PowerMeterProvider::Type::HTTP_JSON:
_upProvider = std::make_unique<PowerMeterHttpJson>(pmcfg.HttpJson);
break;
case PowerMeterProvider::Type::SERIAL_SML:
_upProvider = std::make_unique<PowerMeterSerialSml>();
break;
case PowerMeterProvider::Type::SMAHM2:
_upProvider = std::make_unique<PowerMeterUdpSmaHomeManager>();
break;
case PowerMeterProvider::Type::HTTP_SML:
_upProvider = std::make_unique<PowerMeterHttpSml>(pmcfg.HttpSml);
break;
}
if (!_upProvider->init()) {
_upProvider = nullptr;
}
} }
bool PowerMeterClass::isDataValid() float PowerMeterClass::getPowerTotal() const
{ {
auto const& config = Configuration.get();
std::lock_guard<std::mutex> l(_mutex); std::lock_guard<std::mutex> l(_mutex);
if (!_upProvider) { return 0.0; }
bool valid = config.PowerMeter.Enabled && return _upProvider->getPowerTotal();
_lastPowerMeterUpdate > 0 &&
((millis() - _lastPowerMeterUpdate) < (30 * 1000));
// reset if timed out to avoid glitch once
// (millis() - _lastPowerMeterUpdate) overflows
if (!valid) { _lastPowerMeterUpdate = 0; }
return valid;
} }
void PowerMeterClass::mqtt() uint32_t PowerMeterClass::getLastUpdate() const
{ {
if (!MqttSettings.getConnected()) { return; }
String topic = "powermeter";
auto totalPower = getPowerTotal();
std::lock_guard<std::mutex> l(_mutex); std::lock_guard<std::mutex> l(_mutex);
MqttSettings.publish(topic + "/power1", String(_powerMeter1Power)); if (!_upProvider) { return 0; }
MqttSettings.publish(topic + "/power2", String(_powerMeter2Power)); return _upProvider->getLastUpdate();
MqttSettings.publish(topic + "/power3", String(_powerMeter3Power)); }
MqttSettings.publish(topic + "/powertotal", String(totalPower));
MqttSettings.publish(topic + "/voltage1", String(_powerMeter1Voltage)); bool PowerMeterClass::isDataValid() const
MqttSettings.publish(topic + "/voltage2", String(_powerMeter2Voltage)); {
MqttSettings.publish(topic + "/voltage3", String(_powerMeter3Voltage)); std::lock_guard<std::mutex> l(_mutex);
MqttSettings.publish(topic + "/import", String(_powerMeterImport)); if (!_upProvider) { return false; }
MqttSettings.publish(topic + "/export", String(_powerMeterExport)); return _upProvider->isDataValid();
} }
void PowerMeterClass::loop() void PowerMeterClass::loop()
{ {
CONFIG_T const& config = Configuration.get(); std::lock_guard<std::mutex> lock(_mutex);
_verboseLogging = config.PowerMeter.VerboseLogging; if (!_upProvider) { return; }
_upProvider->loop();
if (!config.PowerMeter.Enabled) { return; } _upProvider->mqttLoop();
if (static_cast<Source>(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<Source>(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<std::mutex> l(_mutex);
_powerMeter1Power = static_cast<float>(phase1Power);
_powerMeter2Power = 0;
_powerMeter3Power = 0;
_powerMeter1Voltage = static_cast<float>(phase1Voltage);
_powerMeter2Voltage = 0;
_powerMeter3Voltage = 0;
_powerMeterImport = static_cast<float>(energyImport);
_powerMeterExport = static_cast<float>(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<std::mutex> l(_mutex);
_powerMeter1Power = static_cast<float>(phase1Power);
_powerMeter2Power = static_cast<float>(phase2Power);
_powerMeter3Power = static_cast<float>(phase3Power);
_powerMeter1Voltage = static_cast<float>(phase1Voltage);
_powerMeter2Voltage = static_cast<float>(phase2Voltage);
_powerMeter3Voltage = static_cast<float>(phase3Voltage);
_powerMeterImport = static_cast<float>(energyImport);
_powerMeterExport = static_cast<float>(energyExport);
_lastPowerMeterUpdate = millis();
}
else if (configuredSource == Source::HTTP) {
if (HttpPowerMeter.updateValues()) {
std::lock_guard<std::mutex> l(_mutex);
_powerMeter1Power = HttpPowerMeter.getPower(1);
_powerMeter2Power = HttpPowerMeter.getPower(2);
_powerMeter3Power = HttpPowerMeter.getPower(3);
_lastPowerMeterUpdate = millis();
}
}
else if (configuredSource == Source::SMAHM2) {
std::lock_guard<std::mutex> 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;
} }

192
src/PowerMeterHttpJson.cpp Normal file
View File

@ -0,0 +1,192 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "Utils.h"
#include "PowerMeterHttpJson.h"
#include "MessageOutput.h"
#include <WiFiClientSecure.h>
#include <ArduinoJson.h>
#include "mbedtls/sha256.h"
#include <base64.h>
#include <ESPmDNS.h>
PowerMeterHttpJson::~PowerMeterHttpJson()
{
_taskDone = false;
std::unique_lock<std::mutex> 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<HttpGetter>(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<std::mutex> 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<PowerMeterHttpJson*>(context);
pInstance->pollingLoop();
pInstance->_taskDone = true;
vTaskDelete(nullptr);
}
void PowerMeterHttpJson::pollingLoop()
{
std::unique_lock<std::mutex> 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<String>(res)) {
MessageOutput.printf("[PowerMeterHttpJson] %s\r\n", std::get<String>(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<float>(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<std::mutex> lock(_valueMutex);
_powerValues = cache;
return cache;
}
float PowerMeterHttpJson::getPowerTotal() const
{
float sum = 0.0;
std::unique_lock<std::mutex> 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<std::mutex> lock(_valueMutex);
mqttPublish("power1", _powerValues[0]);
mqttPublish("power2", _powerValues[1]);
mqttPublish("power3", _powerValues[2]);
}

117
src/PowerMeterHttpSml.cpp Normal file
View File

@ -0,0 +1,117 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include "PowerMeterHttpSml.h"
#include "MessageOutput.h"
#include <WiFiClientSecure.h>
#include <base64.h>
#include <ESPmDNS.h>
PowerMeterHttpSml::~PowerMeterHttpSml()
{
_taskDone = false;
std::unique_lock<std::mutex> 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<HttpGetter>(_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<std::mutex> 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<PowerMeterHttpSml*>(context);
pInstance->pollingLoop();
pInstance->_taskDone = true;
vTaskDelete(nullptr);
}
void PowerMeterHttpSml::pollingLoop()
{
std::unique_lock<std::mutex> 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 "";
}

114
src/PowerMeterMqtt.cpp Normal file
View File

@ -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<char const*>(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<std::mutex> 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<float>(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<std::mutex> lock(_mutex);
for (auto v: _powerValues) { sum += v; }
return sum;
}
void PowerMeterMqtt::doMqttPublish() const
{
std::lock_guard<std::mutex> l(_mutex);
mqttPublish("power1", _powerValues[0]);
mqttPublish("power2", _powerValues[1]);
mqttPublish("power3", _powerValues[2]);
}

View File

@ -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<uint32_t>::max() / 2;
if ((_lastUpdate - _lastMqttPublish) > halfOfAllMillis) { return; }
mqttPublish("powertotal", getPowerTotal());
doMqttPublish();
_lastMqttPublish = millis();
}

204
src/PowerMeterSerialSdm.cpp Normal file
View File

@ -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<std::mutex> 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<SoftwareSerial>();
_upSdm = std::make_unique<SDM>(*_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<std::mutex> 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<std::mutex> 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<std::mutex> 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<PowerMeterSerialSdm*>(context);
pInstance->pollingLoop();
pInstance->_taskDone = true;
vTaskDelete(nullptr);
}
bool PowerMeterSerialSdm::readValue(std::unique_lock<std::mutex>& 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<std::mutex> 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<std::mutex> l(_valueMutex);
_phase1Power = static_cast<float>(phase1Power);
_phase2Power = static_cast<float>(phase2Power);
_phase3Power = static_cast<float>(phase3Power);
_phase1Voltage = static_cast<float>(phase1Voltage);
_phase2Voltage = static_cast<float>(phase2Voltage);
_phase3Voltage = static_cast<float>(phase3Voltage);
_energyImport = static_cast<float>(energyImport);
_energyExport = static_cast<float>(energyExport);
}
MessageOutput.printf("[PowerMeterSerialSdm] TotalPower: %5.2f\r\n", getPowerTotal());
gotUpdate();
}
}

122
src/PowerMeterSerialSml.cpp Normal file
View File

@ -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<SoftwareSerial>();
_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<std::mutex> 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<std::mutex> 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<PowerMeterSerialSml*>(context);
pInstance->pollingLoop();
pInstance->_taskDone = true;
vTaskDelete(nullptr);
}
void PowerMeterSerialSml::pollingLoop()
{
int lastAvailable = 0;
uint32_t gapStartMillis = 0;
std::unique_lock<std::mutex> 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();
}
}

73
src/PowerMeterSml.cpp Normal file
View File

@ -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<std::mutex> 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<std::mutex> 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<std::mutex> 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;
}
}

View File

@ -2,51 +2,46 @@
/* /*
* Copyright (C) 2024 Holger-Steffen Stapf * Copyright (C) 2024 Holger-Steffen Stapf
*/ */
#include "SMA_HM.h" #include "PowerMeterUdpSmaHomeManager.h"
#include <Arduino.h> #include <Arduino.h>
#include "Configuration.h"
#include "NetworkSettings.h"
#include <WiFiUdp.h> #include <WiFiUdp.h>
#include "MessageOutput.h" #include "MessageOutput.h"
unsigned int multicastPort = 9522; // local port to listen on static constexpr unsigned int multicastPort = 9522; // local port to listen on
IPAddress multicastIP(239, 12, 255, 254); static const IPAddress multicastIP(239, 12, 255, 254);
WiFiUDP SMAUdp; static WiFiUDP SMAUdp;
constexpr uint32_t interval = 1000; constexpr uint32_t interval = 1000;
SMA_HMClass SMA_HM; void PowerMeterUdpSmaHomeManager::Soutput(int kanal, int index, int art, int tarif,
void SMA_HMClass::Soutput(int kanal, int index, int art, int tarif,
char const* name, float value, uint32_t timestamp) char const* name, float value, uint32_t timestamp)
{ {
if (!_verboseLogging) { return; } 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); 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.begin(multicastPort);
SMAUdp.beginMulticast(multicastIP, multicastPort); SMAUdp.beginMulticast(multicastIP, multicastPort);
return true;
} }
void SMA_HMClass::loop() PowerMeterUdpSmaHomeManager::~PowerMeterUdpSmaHomeManager()
{ {
uint32_t currentMillis = millis(); SMAUdp.stop();
if (currentMillis - _previousMillis >= interval) {
_previousMillis = currentMillis;
event1();
}
} }
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 Pbezug = 0;
float BezugL1 = 0; float BezugL1 = 0;
@ -149,7 +144,7 @@ uint8_t* SMA_HMClass::decodeGroup(uint8_t* offset, uint16_t grouplen)
continue; 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); kanal, index, art, tarif);
offset += art; offset += art;
} }
@ -157,15 +152,20 @@ uint8_t* SMA_HMClass::decodeGroup(uint8_t* offset, uint16_t grouplen)
return offset; return offset;
} }
void SMA_HMClass::event1() void PowerMeterUdpSmaHomeManager::loop()
{ {
uint32_t currentMillis = millis();
if (currentMillis - _previousMillis < interval) { return; }
_previousMillis = currentMillis;
int packetSize = SMAUdp.parsePacket(); int packetSize = SMAUdp.parsePacket();
if (!packetSize) { return; } if (!packetSize) { return; }
uint8_t buffer[1024]; uint8_t buffer[1024];
int rSize = SMAUdp.read(buffer, 1024); int rSize = SMAUdp.read(buffer, 1024);
if (buffer[0] != 'S' || buffer[1] != 'M' || buffer[2] != 'A') { 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; return;
} }
@ -196,7 +196,7 @@ void SMA_HMClass::event1()
continue; 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); grouptag, grouplen);
offset += grouplen; offset += grouplen;
} while (grouplen > 0 && offset + 4 < buffer + rSize); } while (grouplen > 0 && offset + 4 < buffer + rSize);

View File

@ -92,3 +92,77 @@ void Utils::removeAllFiles()
file = root.getNextFileName(); file = root.getNextFileName();
} }
} }
/* OpenDTU-OnBatter-specific utils go here: */
template<typename T>
std::pair<T, String> 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<JsonVariantConst>();
// 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<JsonArrayConst>()) {
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<T>()) {
snprintf(errBuffer, kErrBufferSize, "Value '%s' at JSON path '%s' is not "
"of the expected type", value.as<String>().c_str(), path.c_str());
return { T(), String(errBuffer) };
}
return { value.as<T>(), "" };
}
template std::pair<float, String> Utils::getJsonValueByPath(JsonDocument const& root, String const& path);

View File

@ -12,7 +12,8 @@
#include "MqttSettings.h" #include "MqttSettings.h"
#include "PowerLimiter.h" #include "PowerLimiter.h"
#include "PowerMeter.h" #include "PowerMeter.h"
#include "HttpPowerMeter.h" #include "PowerMeterHttpJson.h"
#include "PowerMeterHttpSml.h"
#include "WebApi.h" #include "WebApi.h"
#include "helper.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/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_GET, std::bind(&WebApiPowerMeterClass::onAdminGet, this, _1));
_server->on("/api/powermeter/config", HTTP_POST, std::bind(&WebApiPowerMeterClass::onAdminPost, 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)); _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::decodeJsonPhaseConfig(JsonObject const& json, PowerMeterHttpConfig& config) const
{
config.Enabled = json["enabled"].as<bool>();
strlcpy(config.Url, json["url"].as<String>().c_str(), sizeof(config.Url));
config.AuthType = json["auth_type"].as<PowerMeterHttpConfig::Auth>();
strlcpy(config.Username, json["username"].as<String>().c_str(), sizeof(config.Username));
strlcpy(config.Password, json["password"].as<String>().c_str(), sizeof(config.Password));
strlcpy(config.HeaderKey, json["header_key"].as<String>().c_str(), sizeof(config.HeaderKey));
strlcpy(config.HeaderValue, json["header_value"].as<String>().c_str(), sizeof(config.HeaderValue));
config.Timeout = json["timeout"].as<uint16_t>();
strlcpy(config.JsonPath, json["json_path"].as<String>().c_str(), sizeof(config.JsonPath));
config.PowerUnit = json["unit"].as<PowerMeterHttpConfig::Unit>();
config.SignInverted = json["sign_inverted"].as<bool>();
} }
void WebApiPowerMeterClass::onStatus(AsyncWebServerRequest* request) void WebApiPowerMeterClass::onStatus(AsyncWebServerRequest* request)
@ -52,32 +39,18 @@ void WebApiPowerMeterClass::onStatus(AsyncWebServerRequest* request)
root["enabled"] = config.PowerMeter.Enabled; root["enabled"] = config.PowerMeter.Enabled;
root["verbose_logging"] = config.PowerMeter.VerboseLogging; root["verbose_logging"] = config.PowerMeter.VerboseLogging;
root["source"] = config.PowerMeter.Source; 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<JsonArray>(); auto mqtt = root["mqtt"].to<JsonObject>();
Configuration.serializePowerMeterMqttConfig(config.PowerMeter.Mqtt, mqtt);
for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) { auto serialSdm = root["serial_sdm"].to<JsonObject>();
auto phaseObject = httpPhases.add<JsonObject>(); Configuration.serializePowerMeterSerialSdmConfig(config.PowerMeter.SerialSdm, serialSdm);
phaseObject["index"] = i + 1; auto httpJson = root["http_json"].to<JsonObject>();
phaseObject["enabled"] = config.PowerMeter.Http_Phase[i].Enabled; Configuration.serializePowerMeterHttpJsonConfig(config.PowerMeter.HttpJson, httpJson);
phaseObject["url"] = String(config.PowerMeter.Http_Phase[i].Url);
phaseObject["auth_type"]= config.PowerMeter.Http_Phase[i].AuthType; auto httpSml = root["http_sml"].to<JsonObject>();
phaseObject["username"] = String(config.PowerMeter.Http_Phase[i].Username); Configuration.serializePowerMeterHttpSmlConfig(config.PowerMeter.HttpSml, httpSml);
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;
}
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
} }
@ -112,44 +85,53 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request)
return; return;
} }
if (static_cast<PowerMeterClass::Source>(root["source"].as<uint8_t>()) == PowerMeterClass::Source::HTTP) { auto checkHttpConfig = [&](JsonObject const& cfg) -> bool {
JsonArray http_phases = root["http_phases"]; if (!cfg.containsKey("url")
for (uint8_t i = 0; i < http_phases.size(); i++) { || (!cfg["url"].as<String>().startsWith("http://")
JsonObject phase = http_phases[i].as<JsonObject>(); && !cfg["url"].as<String>().startsWith("https://"))) {
if (i > 0 && !phase["enabled"].as<bool>()) {
continue;
}
if (i == 0 || phase["http_individual_requests"].as<bool>()) {
if (!phase.containsKey("url")
|| (!phase["url"].as<String>().startsWith("http://")
&& !phase["url"].as<String>().startsWith("https://"))) {
retMsg["message"] = "URL must either start with http:// or https://!"; retMsg["message"] = "URL must either start with http:// or https://!";
response->setLength(); response->setLength();
request->send(response); request->send(response);
return; return false;
} }
if ((phase["auth_type"].as<uint8_t>() != PowerMeterHttpConfig::Auth::None) if ((cfg["auth_type"].as<uint8_t>() != HttpRequestConfig::Auth::None)
&& ( phase["username"].as<String>().length() == 0 || phase["password"].as<String>().length() == 0)) { && (cfg["username"].as<String>().length() == 0 || cfg["password"].as<String>().length() == 0)) {
retMsg["message"] = "Username or password must not be empty!"; retMsg["message"] = "Username or password must not be empty!";
response->setLength(); response->setLength();
request->send(response); request->send(response);
return; return false;
} }
if (!phase.containsKey("timeout") if (!cfg.containsKey("timeout")
|| phase["timeout"].as<uint16_t>() <= 0) { || cfg["timeout"].as<uint16_t>() <= 0) {
retMsg["message"] = "Timeout must be greater than 0 ms!"; retMsg["message"] = "Timeout must be greater than 0 ms!";
response->setLength(); response->setLength();
request->send(response); request->send(response);
return false;
}
return true;
};
if (static_cast<PowerMeterProvider::Type>(root["source"].as<uint8_t>()) == 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<JsonObject>();
if (i > 0 && !valueConfig["enabled"].as<bool>()) {
continue;
}
if (i == 0 || httpJson["individual_requests"].as<bool>()) {
if (!checkHttpConfig(valueConfig["http_request"].as<JsonObject>())) {
return; return;
} }
} }
if (!phase.containsKey("json_path") if (!valueConfig.containsKey("json_path")
|| phase["json_path"].as<String>().length() == 0) { || valueConfig["json_path"].as<String>().length() == 0) {
retMsg["message"] = "Json path must not be empty!"; retMsg["message"] = "Json path must not be empty!";
response->setLength(); response->setLength();
request->send(response); request->send(response);
@ -158,37 +140,38 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request)
} }
} }
if (static_cast<PowerMeterProvider::Type>(root["source"].as<uint8_t>()) == PowerMeterProvider::Type::HTTP_SML) {
JsonObject httpSml = root["http_sml"];
if (!checkHttpConfig(httpSml["http_request"].as<JsonObject>())) {
return;
}
}
CONFIG_T& config = Configuration.get(); CONFIG_T& config = Configuration.get();
config.PowerMeter.Enabled = root["enabled"].as<bool>(); config.PowerMeter.Enabled = root["enabled"].as<bool>();
config.PowerMeter.VerboseLogging = root["verbose_logging"].as<bool>(); config.PowerMeter.VerboseLogging = root["verbose_logging"].as<bool>();
config.PowerMeter.Source = root["source"].as<uint8_t>(); config.PowerMeter.Source = root["source"].as<uint8_t>();
config.PowerMeter.Interval = root["interval"].as<uint32_t>();
strlcpy(config.PowerMeter.MqttTopicPowerMeter1, root["mqtt_topic_powermeter_1"].as<String>().c_str(), sizeof(config.PowerMeter.MqttTopicPowerMeter1));
strlcpy(config.PowerMeter.MqttTopicPowerMeter2, root["mqtt_topic_powermeter_2"].as<String>().c_str(), sizeof(config.PowerMeter.MqttTopicPowerMeter2));
strlcpy(config.PowerMeter.MqttTopicPowerMeter3, root["mqtt_topic_powermeter_3"].as<String>().c_str(), sizeof(config.PowerMeter.MqttTopicPowerMeter3));
config.PowerMeter.SdmBaudrate = root["sdmbaudrate"].as<uint32_t>();
config.PowerMeter.SdmAddress = root["sdmaddress"].as<uint8_t>();
config.PowerMeter.HttpIndividualRequests = root["http_individual_requests"].as<bool>();
JsonArray http_phases = root["http_phases"]; Configuration.deserializePowerMeterMqttConfig(root["mqtt"].as<JsonObject>(),
for (uint8_t i = 0; i < http_phases.size(); i++) { config.PowerMeter.Mqtt);
decodeJsonPhaseConfig(http_phases[i].as<JsonObject>(), config.PowerMeter.Http_Phase[i]);
} Configuration.deserializePowerMeterSerialSdmConfig(root["serial_sdm"].as<JsonObject>(),
config.PowerMeter.Http_Phase[0].Enabled = true; config.PowerMeter.SerialSdm);
Configuration.deserializePowerMeterHttpJsonConfig(root["http_json"].as<JsonObject>(),
config.PowerMeter.HttpJson);
Configuration.deserializePowerMeterHttpSmlConfig(root["http_sml"].as<JsonObject>(),
config.PowerMeter.HttpSml);
WebApi.writeConfig(retMsg); WebApi.writeConfig(retMsg);
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__); WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
PowerMeter.updateSettings();
// reboot requiered as per https://github.com/helgeerbe/OpenDTU-OnBattery/issues/565#issuecomment-1872552559
yield();
delay(1000);
yield();
ESP.restart();
} }
void WebApiPowerMeterClass::onTestHttpRequest(AsyncWebServerRequest* request) void WebApiPowerMeterClass::onTestHttpJsonRequest(AsyncWebServerRequest* request)
{ {
if (!WebApi.checkCredentials(request)) { if (!WebApi.checkCredentials(request)) {
return; return;
@ -202,26 +185,60 @@ void WebApiPowerMeterClass::onTestHttpRequest(AsyncWebServerRequest* request)
auto& retMsg = asyncJsonResponse->getRoot(); 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);
return;
}
char response[256]; char response[256];
int phase = 0;//"absuing" index 0 of the float power[3] in HttpPowerMeter to store the result auto powerMeterConfig = std::make_unique<PowerMeterHttpJsonConfig>();
PowerMeterHttpConfig phaseConfig; Configuration.deserializePowerMeterHttpJsonConfig(root["http_json"].as<JsonObject>(),
decodeJsonPhaseConfig(root.as<JsonObject>(), phaseConfig); *powerMeterConfig);
if (HttpPowerMeter.queryPhase(phase, phaseConfig)) { auto upMeter = std::make_unique<PowerMeterHttpJson>(*powerMeterConfig);
upMeter->init();
auto res = upMeter->poll();
using values_t = PowerMeterHttpJson::power_values_t;
if (std::holds_alternative<values_t>(res)) {
retMsg["type"] = "success"; retMsg["type"] = "success";
snprintf_P(response, sizeof(response), "Success! Power: %5.2fW", HttpPowerMeter.getPower(phase + 1)); auto vals = std::get<values_t>(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 { } else {
snprintf_P(response, sizeof(response), "%s", HttpPowerMeter.httpPowerMeterError); snprintf(response, sizeof(response), "%s", std::get<String>(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];
auto powerMeterConfig = std::make_unique<PowerMeterHttpSmlConfig>();
Configuration.deserializePowerMeterHttpSmlConfig(root["http_sml"].as<JsonObject>(),
*powerMeterConfig);
auto upMeter = std::make_unique<PowerMeterHttpSml>(*powerMeterConfig);
upMeter->init();
auto res = upMeter->poll();
if (res.isEmpty()) {
retMsg["type"] = "success";
snprintf(response, sizeof(response), "Result: %5.2fW", upMeter->getPowerTotal());
} else {
snprintf(response, sizeof(response), "%s", res.c_str());
} }
retMsg["message"] = response; retMsg["message"] = response;

View File

@ -98,12 +98,12 @@ void WebApiWsLiveClass::generateOnBatteryJsonResponse(JsonVariant& root, bool al
if (!all) { _lastPublishBattery = millis(); } if (!all) { _lastPublishBattery = millis(); }
} }
if (all || (PowerMeter.getLastPowerMeterUpdate() - _lastPublishPowerMeter) < halfOfAllMillis) { if (all || (PowerMeter.getLastUpdate() - _lastPublishPowerMeter) < halfOfAllMillis) {
auto powerMeterObj = root["power_meter"].to<JsonObject>(); auto powerMeterObj = root["power_meter"].to<JsonObject>();
powerMeterObj["enabled"] = config.PowerMeter.Enabled; powerMeterObj["enabled"] = config.PowerMeter.Enabled;
if (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(); } if (!all) { _lastPublishPowerMeter = millis(); }

View File

@ -0,0 +1,94 @@
<template>
<div>
<InputElement
:label="$t('httprequestsettings.url')"
v-model="cfg.url"
type="text"
maxlength="1024"
placeholder="http://admin:supersecret@mypowermeter.home/status"
prefix="GET "
:tooltip="$t('httprequestsettings.urlDescription')"
wide />
<div class="row mb-3">
<label for="auth_type" class="col-sm-4 col-form-label">{{ $t('httprequestsettings.authorization') }}</label>
<div class="col-sm-8">
<select id="auth_type" class="form-select" v-model="cfg.auth_type">
<option v-for="a in authTypeList" :key="a.key" :value="a.key">
{{ $t('httprequestsettings.authType' + a.value) }}
</option>
</select>
</div>
</div>
<InputElement
v-if="cfg.auth_type != 0"
:label="$t('httprequestsettings.username')"
v-model="cfg.username"
type="text"
maxlength="64"
wide />
<InputElement
v-if="cfg.auth_type != 0"
:label="$t('httprequestsettings.password')"
v-model="cfg.password"
type="password"
maxlength="64"
wide />
<InputElement
:label="$t('httprequestsettings.headerKey')"
v-model="cfg.header_key"
type="text"
maxlength="64"
:tooltip="$t('httprequestsettings.headerKeyDescription')"
wide />
<InputElement
:label="$t('httprequestsettings.headerValue')"
v-model="cfg.header_value"
type="text"
maxlength="256"
wide />
<InputElement
:label="$t('httprequestsettings.timeout')"
v-model="cfg.timeout"
type="number"
:postfix="$t('httprequestsettings.milliSeconds')"
wide />
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import type { HttpRequestConfig } from '@/types/HttpRequestConfig';
import InputElement from '@/components/InputElement.vue';
export default defineComponent({
props: { 'modelValue': Object as () => HttpRequestConfig },
computed: {
cfg: {
get(): HttpRequestConfig {
return this.modelValue || {} as HttpRequestConfig;
},
set(newValue: HttpRequestConfig): void {
this.$emit('update:modelValue', newValue);
}
}
},
components: {
InputElement
},
data() {
return {
authTypeList: [
{ key: 0, value: "None" },
{ key: 1, value: "Basic" },
{ key: 2, value: "Digest" },
]
};
}
});
</script>

View File

@ -554,40 +554,53 @@
"PowerMeterConfiguration": "Stromzähler Konfiguration", "PowerMeterConfiguration": "Stromzähler Konfiguration",
"PowerMeterEnable": "Aktiviere Stromzähler", "PowerMeterEnable": "Aktiviere Stromzähler",
"VerboseLogging": "@:base.VerboseLogging", "VerboseLogging": "@:base.VerboseLogging",
"PowerMeterParameter": "Power Meter Parameter",
"PowerMeterSource": "Stromzählertyp", "PowerMeterSource": "Stromzählertyp",
"MQTT": "MQTT Konfiguration", "pollingInterval": "Abfrageintervall",
"seconds": "@:base.Seconds",
"typeMQTT": "MQTT", "typeMQTT": "MQTT",
"typeSDM1ph": "SDM 1 phase (SDM120/220/230)", "typeSDM1ph": "SDM mit 1 Phase (SDM120/220/230)",
"typeSDM3ph": "SDM 3 phase (SDM72/630)", "typeSDM3ph": "SDM mit 3 Phasen (SDM72/630)",
"typeHTTP": "HTTP(S) + JSON", "typeHTTP_JSON": "HTTP(S) + JSON",
"typeSML": "SML (OBIS 16.7.0)", "typeSML": "SML/OBIS via serieller Verbindung (z.B. Hichi TTL)",
"typeSMAHM2": "SMA Homemanager 2.0", "typeSMAHM2": "SMA Homemanager 2.0",
"MqttTopicPowerMeter1": "MQTT topic - Stromzähler #1", "typeHTTP_SML": "HTTP(S) + SML (z.B. Tibber Pulse via Tibber Bridge)",
"MqttTopicPowerMeter2": "MQTT topic - Stromzähler #2 (Optional)", "MqttValue": "Konfiguration Wert {valueNumber}",
"MqttTopicPowerMeter3": "MQTT topic - Stromzähler #3 (Optional)", "MqttTopic": "MQTT Topic",
"mqttJsonPath": "Optional: JSON-Pfad",
"SDM": "SDM-Stromzähler Konfiguration", "SDM": "SDM-Stromzähler Konfiguration",
"sdmbaudrate": "Baudrate",
"sdmaddress": "Modbus Adresse", "sdmaddress": "Modbus Adresse",
"HTTP": "HTTP(S) + JSON - Allgemeine Konfiguration", "HTTP_JSON": "HTTP(S) + JSON - Allgemeine Konfiguration",
"httpIndividualRequests": "Individuelle HTTP requests pro Phase", "httpIndividualRequests": "Individuelle HTTP(S) Anfragen pro Wert",
"urlExamplesHeading": "Beispiele für URLs", "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.", "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)!.", "httpValue": "Konfiguration Wert {valueNumber}",
"httpPhase": "HTTP(S) + JSON Konfiguration - Phase {phaseNumber}", "httpEnabled": "Wert aktiviert",
"httpEnabled": "Phase aktiviert", "valueJsonPath": "JSON-Pfad",
"httpUrl": "URL", "valueJsonPathDescription": "Anwendungsspezifischer JSON-Pfad um den Leistungswert in den JSON Nutzdatzen zu finden, z.B. 'power/total/watts' oder nur 'total'.",
"httpHeaderKey": "Optional: HTTP request header - Key", "valueUnit": "Einheit",
"httpHeaderKeyDescription": "Ein individueller HTTP request header kann hier definiert werden. Das kann z.B. verwendet werden um einen eigenen Authorization header mitzugeben.", "valueSignInverted": "Vorzeichen umkehren",
"httpHeaderValue": "Optional: HTTP request header - Wert", "valueSignInvertedHint": "Positive Werte werden als Leistungsabnahme aus dem Netz interpretiert. Diese Option muss aktiviert werden, wenn das Vorzeichen des Wertes die gegenteilige Bedeutung hat.",
"httpJsonPath": "JSON Pfad", "testHttpJsonHeader": "Konfiguration testen",
"httpJsonPathDescription": "Anwendungsspezifischer JSON-Pfad um den Leistungswert in the HTTP(S) Antwort zu finden, z.B. 'power/total/watts' oder nur 'total'.", "testHttpJsonRequest": "HTTP(S)-Anfrage(n) senden und Antwort(en) verarbeiten",
"httpUnit": "Einheit", "testHttpSmlHeader": "Konfiguration testen",
"httpSignInverted": "Vorzeichen umkehren", "testHttpSmlRequest": "HTTP(S)-Anfrage senden und Antwort verarbeiten",
"httpSignInvertedHint": "Positive Werte werden als Leistungsabnahme aus dem Netz interpretiert. Diese Option muss aktiviert werden, wenn das Vorzeichen des Wertes die gegenteilige Bedeutung hat.", "HTTP_SML": "HTTP(S) + SML - Konfiguration"
"httpTimeout": "Timeout", },
"testHttpRequest": "Testen" "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": { "powerlimiteradmin": {
"PowerLimiterSettings": "Dynamic Power Limiter Einstellungen", "PowerLimiterSettings": "Dynamic Power Limiter Einstellungen",

View File

@ -556,44 +556,53 @@
"PowerMeterConfiguration": "Power Meter Configuration", "PowerMeterConfiguration": "Power Meter Configuration",
"PowerMeterEnable": "Enable Power Meter", "PowerMeterEnable": "Enable Power Meter",
"VerboseLogging": "@:base.VerboseLogging", "VerboseLogging": "@:base.VerboseLogging",
"PowerMeterParameter": "Power Meter Parameter", "PowerMeterSource": "Power Meter Type",
"PowerMeterSource": "Power Meter type", "pollingInterval": "Polling Interval",
"MQTT": "MQTT Parameter", "seconds": "@:base.Seconds",
"typeMQTT": "MQTT", "typeMQTT": "MQTT",
"typeSDM1ph": "SDM 1 phase (SDM120/220/230)", "typeSDM1ph": "SDM for 1 phase (SDM120/220/230)",
"typeSDM3ph": "SDM 3 phase (SDM72/630)", "typeSDM3ph": "SDM for 3 phases (SDM72/630)",
"typeHTTP": "HTTP(s) + JSON", "typeHTTP_JSON": "HTTP(S) + JSON",
"typeSML": "SML (OBIS 16.7.0)", "typeSML": "SML/OBIS via serial connection (e.g. Hichi TTL)",
"typeSMAHM2": "SMA Homemanager 2.0", "typeSMAHM2": "SMA Homemanager 2.0",
"MqttTopicPowerMeter1": "MQTT topic - Power meter #1", "typeHTTP_SML": "HTTP(S) + SML (e.g. Tibber Pulse via Tibber Bridge)",
"MqttTopicPowerMeter2": "MQTT topic - Power meter #2", "MqttValue": "Value {valueNumber} Configuration",
"MqttTopicPowerMeter3": "MQTT topic - Power meter #3", "mqttJsonPath": "Optional: JSON Path",
"MqttTopic": "MQTT Topic",
"SDM": "SDM-Power Meter Parameter", "SDM": "SDM-Power Meter Parameter",
"sdmbaudrate": "Baudrate",
"sdmaddress": "Modbus Address", "sdmaddress": "Modbus Address",
"HTTP": "HTTP(S) + Json - General configuration", "HTTP": "HTTP(S) + JSON - General configuration",
"httpIndividualRequests": "Individual HTTP requests per phase", "httpIndividualRequests": "Individual HTTP(S) requests per value",
"urlExamplesHeading": "URL Examples", "urlExamplesHeading": "URL Examples",
"jsonPathExamplesHeading": "JSON Path Examples", "jsonPathExamplesHeading": "JSON Path Examples",
"jsonPathExamplesExplanation": "The following paths each find the value '123.4' in the respective example JSON.", "jsonPathExamplesExplanation": "The following paths each find the value '123.4' in the respective example JSON.",
"httpPhase": "HTTP(S) + Json configuration - Phase {phaseNumber}", "httpValue": "Value {valueNumber} Configuration",
"httpEnabled": "Phase enabled", "httpEnabled": "Value Enabled",
"httpUrl": "URL", "valueJsonPath": "JSON Path",
"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)!", "valueJsonPathDescription": "Application specific JSON path to find the power value in the JSON payload, e.g., 'power/total/watts' or simply 'total'.",
"httpAuthorization": "Authorization Type", "valueUnit": "Unit",
"httpUsername": "Username", "valueSignInverted": "Change Sign",
"httpPassword": "Password", "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.",
"httpHeaderKey": "Optional: HTTP request header - Key", "testHttpJsonHeader": "Test Configuration",
"httpHeaderKeyDescription": "A custom HTTP request header can be defined. Might be useful if you have to send something like a custom Authorization header.", "testHttpJsonRequest": "Send HTTP(S) request(s) and process response(s)",
"httpHeaderValue": "Optional: HTTP request header - Value", "testHttpSmlHeader": "Test Configuration",
"httpJsonPath": "JSON path", "testHttpSmlRequest": "Send HTTP(S) request and process response",
"httpJsonPathDescription": "Application specific JSON path to find the power value in the HTTP(S) response, e.g., 'power/total/watts' or simply 'total'.", "HTTP_SML": "Configuration"
"httpUnit": "Unit", },
"httpSignInverted": "Change Sign", "httprequestsettings": {
"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.", "url": "URL",
"httpTimeout": "Timeout", "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)!",
"testHttpRequest": "Run test", "authorization": "Authorization Type",
"milliSeconds": "ms" "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": { "powerlimiteradmin": {
"PowerLimiterSettings": "Dynamic Power Limiter Settings", "PowerLimiterSettings": "Dynamic Power Limiter Settings",

View File

@ -0,0 +1,9 @@
export interface HttpRequestConfig {
url: string;
auth_type: number;
username: string;
password: string;
header_key: string;
header_value: string;
timeout: number;
}

View File

@ -1,28 +1,47 @@
export interface PowerMeterHttpPhaseConfig { import type { HttpRequestConfig } from '@/types/HttpRequestConfig';
index: number;
enabled: boolean; export interface PowerMeterMqttValue {
url: string; topic: string;
auth_type: number;
username: string;
password: string;
header_key: string;
header_value: string;
json_path: string; json_path: string;
timeout: number;
unit: number; unit: number;
sign_inverted: boolean; sign_inverted: boolean;
} }
export interface PowerMeterMqttConfig {
values: Array<PowerMeterMqttValue>;
}
export interface PowerMeterSerialSdmConfig {
polling_interval: number;
address: number;
};
export interface PowerMeterHttpJsonValue {
http_request: HttpRequestConfig;
enabled: boolean;
json_path: string;
unit: number;
sign_inverted: boolean;
}
export interface PowerMeterHttpJsonConfig {
polling_interval: number;
individual_requests: boolean;
values: Array<PowerMeterHttpJsonValue>;
}
export interface PowerMeterHttpSmlConfig {
polling_interval: number;
http_request: HttpRequestConfig;
}
export interface PowerMeterConfig { export interface PowerMeterConfig {
enabled: boolean; enabled: boolean;
verbose_logging: boolean; verbose_logging: boolean;
source: number; source: number;
interval: number; interval: number;
mqtt_topic_powermeter_1: string; mqtt: PowerMeterMqttConfig;
mqtt_topic_powermeter_2: string; serial_sdm: PowerMeterSerialSdmConfig;
mqtt_topic_powermeter_3: string; http_json: PowerMeterHttpJsonConfig;
sdmbaudrate: number; http_sml: PowerMeterHttpSmlConfig;
sdmaddress: number;
http_individual_requests: boolean;
http_phases: Array<PowerMeterHttpPhaseConfig>;
} }

View File

@ -7,25 +7,20 @@
<form @submit="savePowerMeterConfig"> <form @submit="savePowerMeterConfig">
<CardElement :text="$t('powermeteradmin.PowerMeterConfiguration')" <CardElement :text="$t('powermeteradmin.PowerMeterConfiguration')"
textVariant="text-bg-primary"> textVariant="text-bg-primary">
<div class="row mb-3">
<label class="col-sm-2 form-check-label" for="inputPowerMeterEnable">{{ $t('powermeteradmin.PowerMeterEnable') }}</label> <InputElement :label="$t('powermeteradmin.PowerMeterEnable')"
<div class="col-sm-10"> v-model="powerMeterConfigList.enabled"
<div class="form-check form-switch"> type="checkbox" wide />
<input class="form-check-input" type="checkbox" id="inputPowerMeterEnable"
v-model="powerMeterConfigList.enabled" />
</div>
</div>
</div>
<InputElement v-show="powerMeterConfigList.enabled" <InputElement v-show="powerMeterConfigList.enabled"
:label="$t('powermeteradmin.VerboseLogging')" :label="$t('powermeteradmin.VerboseLogging')"
v-model="powerMeterConfigList.verbose_logging" v-model="powerMeterConfigList.verbose_logging"
type="checkbox"/> type="checkbox" wide />
<div class="row mb-3" v-show="powerMeterConfigList.enabled"> <div class="row mb-3" v-show="powerMeterConfigList.enabled">
<label for="inputTimezone" class="col-sm-2 col-form-label">{{ $t('powermeteradmin.PowerMeterSource') }}</label> <label for="inputPowerMeterSource" class="col-sm-4 col-form-label">{{ $t('powermeteradmin.PowerMeterSource') }}</label>
<div class="col-sm-10"> <div class="col-sm-8">
<select class="form-select" v-model="powerMeterConfigList.source"> <select id="inputPowerMeterSource" class="form-select" v-model="powerMeterConfigList.source">
<option v-for="source in powerMeterSourceList" :key="source.key" :value="source.key"> <option v-for="source in powerMeterSourceList" :key="source.key" :value="source.key">
{{ source.value }} {{ source.value }}
</option> </option>
@ -35,85 +30,8 @@
</CardElement> </CardElement>
<div v-if="powerMeterConfigList.enabled"> <div v-if="powerMeterConfigList.enabled">
<CardElement v-if="powerMeterConfigList.source === 0" <div v-if="powerMeterConfigList.source === 0 || powerMeterConfigList.source === 3">
:text="$t('powermeteradmin.MQTT')"
textVariant="text-bg-primary"
add-space>
<div class="row mb-3">
<label for="inputMqttTopicPowerMeter1" class="col-sm-2 col-form-label">{{ $t('powermeteradmin.MqttTopicPowerMeter1') }}:</label>
<div class="col-sm-10">
<div class="input-group">
<input type="text" class="form-control" id="inputMqttTopicPowerMeter1"
placeholder="shellies/shellyem3/emeter/0/power" v-model="powerMeterConfigList.mqtt_topic_powermeter_1" />
</div>
</div>
</div>
<div class="row mb-3">
<label for="inputMqttTopicPowerMeter2" class="col-sm-2 col-form-label">{{ $t('powermeteradmin.MqttTopicPowerMeter2') }}:</label>
<div class="col-sm-10">
<div class="input-group">
<input type="text" class="form-control" id="inputMqttTopicPowerMeter2"
placeholder="shellies/shellyem3/emeter/1/power" v-model="powerMeterConfigList.mqtt_topic_powermeter_2" />
</div>
</div>
</div>
<div class="row mb-3">
<label for="inputMqttTopicPowerMeter3" class="col-sm-2 col-form-label">{{ $t('powermeteradmin.MqttTopicPowerMeter3') }}:</label>
<div class="col-sm-10">
<div class="input-group">
<input type="text" class="form-control" id="inputMqttTopicPowerMeter3"
placeholder="shellies/shellyem3/emeter/2/power" v-model="powerMeterConfigList.mqtt_topic_powermeter_3" />
</div>
</div>
</div>
</CardElement>
<CardElement v-if="(powerMeterConfigList.source === 1 || powerMeterConfigList.source === 2)"
:text="$t('powermeteradmin.SDM')"
textVariant="text-bg-primary"
add-space>
<div class="row mb-3">
<label for="sdmbaudrate" class="col-sm-2 col-form-label">{{ $t('powermeteradmin.sdmbaudrate') }}:</label>
<div class="col-sm-10">
<div class="input-group">
<input type="text" class="form-control" id="sdmbaudrate"
placeholder="9600" v-model="powerMeterConfigList.sdmbaudrate" />
</div>
</div>
</div>
<div class="row mb-3">
<label for="sdmaddress" class="col-sm-2 col-form-label">{{ $t('powermeteradmin.sdmaddress') }}:</label>
<div class="col-sm-10">
<div class="input-group">
<input type="text" class="form-control" id="sdmaddress"
placeholder="1" v-model="powerMeterConfigList.sdmaddress" />
</div>
</div>
</div>
</CardElement>
<div v-if="powerMeterConfigList.source === 3">
<CardElement :text="$t('powermeteradmin.HTTP')"
textVariant="text-bg-primary"
add-space>
<InputElement :label="$t('powermeteradmin.httpIndividualRequests')"
v-model="powerMeterConfigList.http_individual_requests"
type="checkbox"
wide />
</CardElement>
<div class="alert alert-secondary mt-5" role="alert"> <div class="alert alert-secondary mt-5" role="alert">
<h2>{{ $t('powermeteradmin.urlExamplesHeading') }}:</h2>
<ul>
<li>http://admin:secret@shelly3em.home/status</li>
<li>https://admin:secret@shelly3em.home/status</li>
<li>http://tasmota-123.home/cm?cmnd=status%208</li>
<li>http://12.34.56.78/emeter/0</li>
</ul>
<h2>{{ $t('powermeteradmin.jsonPathExamplesHeading') }}:</h2> <h2>{{ $t('powermeteradmin.jsonPathExamplesHeading') }}:</h2>
{{ $t('powermeteradmin.jsonPathExamplesExplanation') }} {{ $t('powermeteradmin.jsonPathExamplesExplanation') }}
<ul> <ul>
@ -122,102 +40,190 @@
<li><code>total</code> &mdash; <code>{ "othervalue": 66, "total": 123.4 }</code></li> <li><code>total</code> &mdash; <code>{ "othervalue": 66, "total": 123.4 }</code></li>
</ul> </ul>
</div> </div>
</div>
<!-- yarn linter wants us to not combine v-if with v-for, so we need to wrap the CardElements //-->
<div v-if="powerMeterConfigList.source === 0">
<CardElement
v-for="(mqtt, index) in powerMeterConfigList.mqtt.values" v-bind:key="index"
:text="$t('powermeteradmin.MqttValue', { valueNumber: index + 1})"
textVariant="text-bg-primary"
add-space>
<InputElement :label="$t('powermeteradmin.MqttTopic')"
v-model="mqtt.topic"
type="text"
maxlength="256"
wide />
<InputElement :label="$t('powermeteradmin.mqttJsonPath')"
v-model="mqtt.json_path"
type="text"
maxlength="256"
:tooltip="$t('powermeteradmin.valueJsonPathDescription')"
wide />
<div class="row mb-3">
<label for="mqtt_power_unit" class="col-sm-4 col-form-label">
{{ $t('powermeteradmin.valueUnit') }}
</label>
<div class="col-sm-8">
<select id="mqtt_power_unit" class="form-select" v-model="mqtt.unit">
<option v-for="u in unitTypeList" :key="u.key" :value="u.key">
{{ u.value }}
</option>
</select>
</div>
</div>
<InputElement
:label="$t('powermeteradmin.valueSignInverted')"
v-model="mqtt.sign_inverted"
:tooltip="$t('powermeteradmin.valueSignInvertedHint')"
type="checkbox"
wide />
</CardElement>
</div>
<CardElement v-if="(powerMeterConfigList.source === 1 || powerMeterConfigList.source === 2)"
:text="$t('powermeteradmin.SDM')"
textVariant="text-bg-primary"
add-space>
<InputElement :label="$t('powermeteradmin.pollingInterval')"
v-model="powerMeterConfigList.serial_sdm.polling_interval"
type="number"
min=1
max=15
:postfix="$t('powermeteradmin.seconds')"
wide />
<InputElement :label="$t('powermeteradmin.sdmaddress')"
v-model="powerMeterConfigList.serial_sdm.address"
type="number"
wide />
</CardElement>
<div v-if="powerMeterConfigList.source === 3">
<div class="alert alert-secondary mt-5" role="alert">
<h2>{{ $t('powermeteradmin.urlExamplesHeading') }}:</h2>
<ul>
<li>http://shelly3em.home/status</li>
<li>https://shelly3em.home/status</li>
<li>http://tasmota-123.home/cm?cmnd=status%208</li>
<li>http://12.34.56.78:8080/emeter/0</li>
</ul>
</div>
<CardElement :text="$t('powermeteradmin.HTTP')"
textVariant="text-bg-primary"
add-space>
<InputElement :label="$t('powermeteradmin.httpIndividualRequests')"
v-model="powerMeterConfigList.http_json.individual_requests"
type="checkbox"
wide />
<InputElement :label="$t('powermeteradmin.pollingInterval')"
v-model="powerMeterConfigList.http_json.polling_interval"
type="number"
min=1
max=15
:postfix="$t('powermeteradmin.seconds')"
wide />
</CardElement>
<CardElement <CardElement
v-for="(http_phase, index) in powerMeterConfigList.http_phases" v-for="(httpJson, index) in powerMeterConfigList.http_json.values"
:key="http_phase.index" :key="index"
:text="$t('powermeteradmin.httpPhase', { phaseNumber: http_phase.index })" :text="$t('powermeteradmin.httpValue', { valueNumber: index + 1 })"
textVariant="text-bg-primary" textVariant="text-bg-primary"
add-space> add-space>
<InputElement <InputElement
v-if="index > 0" v-if="index > 0"
:label="$t('powermeteradmin.httpEnabled')" :label="$t('powermeteradmin.httpEnabled')"
v-model="http_phase.enabled" v-model="httpJson.enabled"
type="checkbox" wide /> type="checkbox" wide />
<div v-if="http_phase.enabled"> <div v-if="httpJson.enabled || index == 0">
<div v-if="index == 0 || powerMeterConfigList.http_individual_requests">
<InputElement :label="$t('powermeteradmin.httpUrl')"
v-model="http_phase.url"
type="text"
maxlength="1024"
placeholder="http://admin:supersecret@mypowermeter.home/status"
prefix="GET "
:tooltip="$t('powermeteradmin.httpUrlDescription')" />
<div class="row mb-3"> <HttpRequestSettings v-model="httpJson.http_request" v-if="index == 0 || powerMeterConfigList.http_json.individual_requests"/>
<label for="inputTimezone" class="col-sm-2 col-form-label">{{ $t('powermeteradmin.httpAuthorization') }}</label>
<div class="col-sm-10">
<select class="form-select" v-model="http_phase.auth_type">
<option v-for="source in powerMeterAuthList" :key="source.key" :value="source.key">
{{ source.value }}
</option>
</select>
</div>
</div>
<div v-if="http_phase.auth_type != 0">
<InputElement :label="$t('powermeteradmin.httpUsername')"
v-model="http_phase.username"
type="text" maxlength="64"/>
<InputElement :label="$t('powermeteradmin.httpPassword')" <InputElement :label="$t('powermeteradmin.valueJsonPath')"
v-model="http_phase.password" v-model="httpJson.json_path"
type="password" maxlength="64"/>
</div>
<InputElement :label="$t('powermeteradmin.httpHeaderKey')"
v-model="http_phase.header_key"
type="text"
maxlength="64"
:tooltip="$t('powermeteradmin.httpHeaderKeyDescription')" />
<InputElement :label="$t('powermeteradmin.httpHeaderValue')"
v-model="http_phase.header_value"
type="text"
maxlength="256" />
<InputElement :label="$t('powermeteradmin.httpTimeout')"
v-model="http_phase.timeout"
type="number"
:postfix="$t('powermeteradmin.milliSeconds')" />
</div>
<InputElement :label="$t('powermeteradmin.httpJsonPath')"
v-model="http_phase.json_path"
type="text" type="text"
maxlength="256" maxlength="256"
placeholder="total_power" :tooltip="$t('powermeteradmin.valueJsonPathDescription')"
:tooltip="$t('powermeteradmin.httpJsonPathDescription')" /> wide />
<div class="row mb-3"> <div class="row mb-3">
<label for="power_unit" class="col-sm-2 col-form-label"> <label for="power_unit" class="col-sm-4 col-form-label">
{{ $t('powermeteradmin.httpUnit') }} {{ $t('powermeteradmin.valueUnit') }}
</label> </label>
<div class="col-sm-10"> <div class="col-sm-8">
<select id="power_unit" class="form-select" v-model="http_phase.unit"> <select id="power_unit" class="form-select" v-model="httpJson.unit">
<option value="1">mW</option> <option v-for="u in unitTypeList" :key="u.key" :value="u.key">
<option value="0">W</option> {{ u.value }}
<option value="2">kW</option> </option>
</select> </select>
</div> </div>
</div> </div>
<InputElement <InputElement
:label="$t('powermeteradmin.httpSignInverted')" :label="$t('powermeteradmin.valueSignInverted')"
v-model="http_phase.sign_inverted" v-model="httpJson.sign_inverted"
:tooltip="$t('powermeteradmin.httpSignInvertedHint')" :tooltip="$t('powermeteradmin.valueSignInvertedHint')"
type="checkbox" /> type="checkbox"
wide />
</div>
</CardElement>
<div class="text-center mb-3"> <CardElement
<button type="button" class="btn btn-danger" @click="testHttpRequest(index)"> :text="$t('powermeteradmin.testHttpJsonHeader')"
{{ $t('powermeteradmin.testHttpRequest') }} textVariant="text-bg-primary"
add-space>
<div class="text-center mt-3 mb-3">
<button type="button" class="btn btn-primary" @click="testHttpJsonRequest()">
{{ $t('powermeteradmin.testHttpJsonRequest') }}
</button> </button>
</div> </div>
<BootstrapAlert v-model="testHttpRequestAlert[index].show" dismissible :variant="testHttpRequestAlert[index].type"> <BootstrapAlert v-model="testHttpJsonRequestAlert.show" dismissible :variant="testHttpJsonRequestAlert.type">
{{ testHttpRequestAlert[index].message }} {{ testHttpJsonRequestAlert.message }}
</BootstrapAlert> </BootstrapAlert>
</CardElement>
</div> </div>
<div v-if="powerMeterConfigList.source === 6">
<CardElement :text="$t('powermeteradmin.HTTP_SML')"
textVariant="text-bg-primary"
add-space>
<InputElement :label="$t('powermeteradmin.pollingInterval')"
v-model="powerMeterConfigList.http_sml.polling_interval"
type="number"
min=1
max=15
:postfix="$t('powermeteradmin.seconds')"
wide />
<HttpRequestSettings v-model="powerMeterConfigList.http_sml.http_request" />
</CardElement>
<CardElement
:text="$t('powermeteradmin.testHttpSmlHeader')"
textVariant="text-bg-primary"
add-space>
<div class="text-center mt-3 mb-3">
<button type="button" class="btn btn-primary" @click="testHttpSmlRequest()">
{{ $t('powermeteradmin.testHttpSmlRequest') }}
</button>
</div>
<BootstrapAlert v-model="testHttpSmlRequestAlert.show" dismissible :variant="testHttpSmlRequestAlert.type">
{{ testHttpSmlRequestAlert.message }}
</BootstrapAlert>
</CardElement> </CardElement>
</div> </div>
</div> </div>
@ -235,8 +241,9 @@ import BootstrapAlert from "@/components/BootstrapAlert.vue";
import CardElement from '@/components/CardElement.vue'; import CardElement from '@/components/CardElement.vue';
import FormFooter from '@/components/FormFooter.vue'; import FormFooter from '@/components/FormFooter.vue';
import InputElement from '@/components/InputElement.vue'; import InputElement from '@/components/InputElement.vue';
import HttpRequestSettings from '@/components/HttpRequestSettings.vue';
import { handleResponse, authHeader } from '@/utils/authentication'; import { handleResponse, authHeader } from '@/utils/authentication';
import type { PowerMeterHttpPhaseConfig, PowerMeterConfig } from "@/types/PowerMeterConfig"; import type { PowerMeterConfig } from "@/types/PowerMeterConfig";
export default defineComponent({ export default defineComponent({
components: { components: {
@ -244,6 +251,7 @@ export default defineComponent({
BootstrapAlert, BootstrapAlert,
CardElement, CardElement,
FormFooter, FormFooter,
HttpRequestSettings,
InputElement InputElement
}, },
data() { data() {
@ -254,19 +262,21 @@ export default defineComponent({
{ key: 0, value: this.$t('powermeteradmin.typeMQTT') }, { key: 0, value: this.$t('powermeteradmin.typeMQTT') },
{ key: 1, value: this.$t('powermeteradmin.typeSDM1ph') }, { key: 1, value: this.$t('powermeteradmin.typeSDM1ph') },
{ key: 2, value: this.$t('powermeteradmin.typeSDM3ph') }, { 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: 4, value: this.$t('powermeteradmin.typeSML') },
{ key: 5, value: this.$t('powermeteradmin.typeSMAHM2') }, { key: 5, value: this.$t('powermeteradmin.typeSMAHM2') },
{ key: 6, value: this.$t('powermeteradmin.typeHTTP_SML') },
], ],
powerMeterAuthList: [ unitTypeList: [
{ key: 0, value: "None" }, { key: 1, value: "mW" },
{ key: 1, value: "Basic" }, { key: 0, value: "W" },
{ key: 2, value: "Digest" }, { key: 2, value: "kW" },
], ],
alertMessage: "", alertMessage: "",
alertType: "info", alertType: "info",
showAlert: false, 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() { created() {
@ -280,14 +290,6 @@ export default defineComponent({
.then((data) => { .then((data) => {
this.powerMeterConfigList = data; this.powerMeterConfigList = data;
this.dataLoading = false; this.dataLoading = false;
for (let i = 0; i < this.powerMeterConfigList.http_phases.length; i++) {
this.testHttpRequestAlert.push({
message: "",
type: "",
show: false,
});
}
}); });
}, },
savePowerMeterConfig(e: Event) { savePowerMeterConfig(e: Event) {
@ -311,27 +313,17 @@ export default defineComponent({
} }
); );
}, },
testHttpRequest(index: number) { testHttpJsonRequest() {
let phaseConfig:PowerMeterHttpPhaseConfig; this.testHttpJsonRequestAlert = {
message: "Triggering HTTP request...",
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...",
type: "info", type: "info",
show: true, show: true,
}; };
const formData = new FormData(); const formData = new FormData();
formData.append("data", JSON.stringify(phaseConfig)); formData.append("data", JSON.stringify(this.powerMeterConfigList));
fetch("/api/powermeter/testhttprequest", { fetch("/api/powermeter/testhttpjsonrequest", {
method: "POST", method: "POST",
headers: authHeader(), headers: authHeader(),
body: formData, body: formData,
@ -339,7 +331,33 @@ export default defineComponent({
.then((response) => handleResponse(response, this.$emitter, this.$router)) .then((response) => handleResponse(response, this.$emitter, this.$router))
.then( .then(
(response) => { (response) => {
this.testHttpRequestAlert[index] = { 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/testhttpsmlrequest", {
method: "POST",
headers: authHeader(),
body: formData,
})
.then((response) => handleResponse(response, this.$emitter, this.$router))
.then(
(response) => {
this.testHttpSmlRequestAlert = {
message: response.message, message: response.message,
type: response.type, type: response.type,
show: true, show: true,