Merge PR #1077 from helgeerbe/powermeter-refactoring
this PowerMeter refactoring tackles many issues and prepares to solve many more.
This commit is contained in:
commit
e358513495
@ -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
77
include/HttpGetter.h
Normal 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;
|
||||||
|
};
|
||||||
@ -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;
|
|
||||||
@ -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;
|
||||||
|
|||||||
53
include/PowerMeterHttpJson.h
Normal file
53
include/PowerMeterHttpJson.h
Normal 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;
|
||||||
|
};
|
||||||
45
include/PowerMeterHttpSml.h
Normal file
45
include/PowerMeterHttpSml.h
Normal 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
37
include/PowerMeterMqtt.h
Normal 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;
|
||||||
|
};
|
||||||
51
include/PowerMeterProvider.h
Normal file
51
include/PowerMeterProvider.h
Normal 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;
|
||||||
|
};
|
||||||
60
include/PowerMeterSerialSdm.h
Normal file
60
include/PowerMeterSerialSdm.h
Normal 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;
|
||||||
|
};
|
||||||
44
include/PowerMeterSerialSml.h
Normal file
44
include/PowerMeterSerialSml.h
Normal 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
69
include/PowerMeterSml.h
Normal 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"}
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -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;
|
|
||||||
@ -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);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 ¤tByte)
|
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);
|
||||||
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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();
|
||||||
|
}
|
||||||
|
|||||||
@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 )
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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"];
|
||||||
|
|||||||
@ -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
236
src/HttpGetter.cpp
Normal 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 });
|
||||||
|
}
|
||||||
@ -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;
|
|
||||||
@ -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());
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
192
src/PowerMeterHttpJson.cpp
Normal 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
117
src/PowerMeterHttpSml.cpp
Normal 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
114
src/PowerMeterMqtt.cpp
Normal 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]);
|
||||||
|
}
|
||||||
29
src/PowerMeterProvider.cpp
Normal file
29
src/PowerMeterProvider.cpp
Normal 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
204
src/PowerMeterSerialSdm.cpp
Normal 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
122
src/PowerMeterSerialSml.cpp
Normal 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
73
src/PowerMeterSml.cpp
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
@ -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);
|
||||||
|
|||||||
@ -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 phaseObject = httpPhases.add<JsonObject>();
|
auto serialSdm = root["serial_sdm"].to<JsonObject>();
|
||||||
|
Configuration.serializePowerMeterSerialSdmConfig(config.PowerMeter.SerialSdm, serialSdm);
|
||||||
phaseObject["index"] = i + 1;
|
|
||||||
phaseObject["enabled"] = config.PowerMeter.Http_Phase[i].Enabled;
|
auto httpJson = root["http_json"].to<JsonObject>();
|
||||||
phaseObject["url"] = String(config.PowerMeter.Http_Phase[i].Url);
|
Configuration.serializePowerMeterHttpJsonConfig(config.PowerMeter.HttpJson, httpJson);
|
||||||
phaseObject["auth_type"]= config.PowerMeter.Http_Phase[i].AuthType;
|
|
||||||
phaseObject["username"] = String(config.PowerMeter.Http_Phase[i].Username);
|
auto httpSml = root["http_sml"].to<JsonObject>();
|
||||||
phaseObject["password"] = String(config.PowerMeter.Http_Phase[i].Password);
|
Configuration.serializePowerMeterHttpSmlConfig(config.PowerMeter.HttpSml, httpSml);
|
||||||
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://"))) {
|
||||||
|
retMsg["message"] = "URL must either start with http:// or https://!";
|
||||||
|
response->setLength();
|
||||||
|
request->send(response);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (i > 0 && !phase["enabled"].as<bool>()) {
|
if ((cfg["auth_type"].as<uint8_t>() != HttpRequestConfig::Auth::None)
|
||||||
|
&& (cfg["username"].as<String>().length() == 0 || cfg["password"].as<String>().length() == 0)) {
|
||||||
|
retMsg["message"] = "Username or password must not be empty!";
|
||||||
|
response->setLength();
|
||||||
|
request->send(response);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cfg.containsKey("timeout")
|
||||||
|
|| cfg["timeout"].as<uint16_t>() <= 0) {
|
||||||
|
retMsg["message"] = "Timeout must be greater than 0 ms!";
|
||||||
|
response->setLength();
|
||||||
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i == 0 || phase["http_individual_requests"].as<bool>()) {
|
if (i == 0 || httpJson["individual_requests"].as<bool>()) {
|
||||||
if (!phase.containsKey("url")
|
if (!checkHttpConfig(valueConfig["http_request"].as<JsonObject>())) {
|
||||||
|| (!phase["url"].as<String>().startsWith("http://")
|
|
||||||
&& !phase["url"].as<String>().startsWith("https://"))) {
|
|
||||||
retMsg["message"] = "URL must either start with http:// or https://!";
|
|
||||||
response->setLength();
|
|
||||||
request->send(response);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((phase["auth_type"].as<uint8_t>() != PowerMeterHttpConfig::Auth::None)
|
|
||||||
&& ( phase["username"].as<String>().length() == 0 || phase["password"].as<String>().length() == 0)) {
|
|
||||||
retMsg["message"] = "Username or password must not be empty!";
|
|
||||||
response->setLength();
|
|
||||||
request->send(response);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!phase.containsKey("timeout")
|
|
||||||
|| phase["timeout"].as<uint16_t>() <= 0) {
|
|
||||||
retMsg["message"] = "Timeout must be greater than 0 ms!";
|
|
||||||
response->setLength();
|
|
||||||
request->send(response);
|
|
||||||
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;
|
||||||
|
|||||||
@ -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(); }
|
||||||
|
|||||||
94
webapp/src/components/HttpRequestSettings.vue
Normal file
94
webapp/src/components/HttpRequestSettings.vue
Normal 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>
|
||||||
@ -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",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
9
webapp/src/types/HttpRequestConfig.ts
Normal file
9
webapp/src/types/HttpRequestConfig.ts
Normal 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;
|
||||||
|
}
|
||||||
@ -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>;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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> — <code>{ "othervalue": 66, "total": 123.4 }</code></li>
|
<li><code>total</code> — <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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<BootstrapAlert v-model="testHttpJsonRequestAlert.show" dismissible :variant="testHttpJsonRequestAlert.type">
|
||||||
|
{{ testHttpJsonRequestAlert.message }}
|
||||||
|
</BootstrapAlert>
|
||||||
|
</CardElement>
|
||||||
|
</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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BootstrapAlert v-model="testHttpRequestAlert[index].show" dismissible :variant="testHttpRequestAlert[index].type">
|
<BootstrapAlert v-model="testHttpSmlRequestAlert.show" dismissible :variant="testHttpSmlRequestAlert.type">
|
||||||
{{ testHttpRequestAlert[index].message }}
|
{{ testHttpSmlRequestAlert.message }}
|
||||||
</BootstrapAlert>
|
</BootstrapAlert>
|
||||||
</div>
|
|
||||||
</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,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user