powermeter refactor: generalize HTTP request config

the parameters to peform an HTTP request by the HTTP(S)+JSON power meter
have been generalized by introducing a new config struct. this is now
used for all values which the HTTP(S)+JSON power meter can retrieve, and
also used by the HTTP+SML power meter implementation. we anticipate that
other feature will use this config as well.

generalizing also allows to share serialization and deserialization
methods in the configuration handler and the web API handler, leading to
de-duplication of code and reduced flash memory usage.

a new web UI component is implemented to manage a set of HTTP request
settings.
This commit is contained in:
Bernhard Kirchen 2024-05-11 21:30:27 +02:00
parent ccba7d8036
commit 297b149f84
15 changed files with 413 additions and 374 deletions

View File

@ -3,6 +3,7 @@
#include "PinMapping.h"
#include <cstdint>
#include <ArduinoJson.h>
#define CONFIG_FILENAME "/config.json"
#define CONFIG_VERSION 0x00011c00 // 0.1.28 // make sure to clean all after change
@ -30,14 +31,14 @@
#define DEV_MAX_MAPPING_NAME_STRLEN 63
#define POWERMETER_MAX_PHASES 3
#define POWERMETER_MAX_HTTP_URL_STRLEN 1024
#define POWERMETER_MAX_USERNAME_STRLEN 64
#define POWERMETER_MAX_PASSWORD_STRLEN 64
#define POWERMETER_MAX_HTTP_HEADER_KEY_STRLEN 64
#define POWERMETER_MAX_HTTP_HEADER_VALUE_STRLEN 256
#define POWERMETER_MAX_HTTP_JSON_PATH_STRLEN 256
#define POWERMETER_HTTP_TIMEOUT 1000
#define HTTP_REQUEST_MAX_URL_STRLEN 1024
#define HTTP_REQUEST_MAX_USERNAME_STRLEN 64
#define HTTP_REQUEST_MAX_PASSWORD_STRLEN 64
#define HTTP_REQUEST_MAX_HEADER_KEY_STRLEN 64
#define HTTP_REQUEST_MAX_HEADER_VALUE_STRLEN 256
#define POWERMETER_HTTP_JSON_MAX_VALUES 3
#define POWERMETER_HTTP_JSON_MAX_PATH_STRLEN 256
struct CHANNEL_CONFIG_T {
uint16_t MaxChannelPower;
@ -61,30 +62,36 @@ struct INVERTER_CONFIG_T {
CHANNEL_CONFIG_T channel[INV_MAX_CHAN_COUNT];
};
struct POWERMETER_HTTP_PHASE_CONFIG_T {
struct HTTP_REQUEST_CONFIG_T {
char Url[HTTP_REQUEST_MAX_URL_STRLEN + 1];
enum Auth { None, Basic, Digest };
enum Unit { Watts = 0, MilliWatts = 1, KiloWatts = 2 };
bool Enabled;
char Url[POWERMETER_MAX_HTTP_URL_STRLEN + 1];
Auth AuthType;
char Username[POWERMETER_MAX_USERNAME_STRLEN +1];
char Password[POWERMETER_MAX_USERNAME_STRLEN +1];
char HeaderKey[POWERMETER_MAX_HTTP_HEADER_KEY_STRLEN + 1];
char HeaderValue[POWERMETER_MAX_HTTP_HEADER_VALUE_STRLEN + 1];
char Username[HTTP_REQUEST_MAX_USERNAME_STRLEN + 1];
char Password[HTTP_REQUEST_MAX_PASSWORD_STRLEN + 1];
char HeaderKey[HTTP_REQUEST_MAX_HEADER_KEY_STRLEN + 1];
char HeaderValue[HTTP_REQUEST_MAX_HEADER_VALUE_STRLEN + 1];
uint16_t Timeout;
char JsonPath[POWERMETER_MAX_HTTP_JSON_PATH_STRLEN + 1];
};
using HttpRequestConfig = struct HTTP_REQUEST_CONFIG_T;
struct POWERMETER_HTTP_JSON_CONFIG_T {
HttpRequestConfig HttpRequest;
bool Enabled;
char JsonPath[POWERMETER_HTTP_JSON_MAX_PATH_STRLEN + 1];
enum Unit { Watts = 0, MilliWatts = 1, KiloWatts = 2 };
Unit PowerUnit;
bool SignInverted;
};
using PowerMeterHttpConfig = struct POWERMETER_HTTP_PHASE_CONFIG_T;
using PowerMeterHttpJsonConfig = struct POWERMETER_HTTP_JSON_CONFIG_T;
struct POWERMETER_TIBBER_CONFIG_T {
char Url[POWERMETER_MAX_HTTP_URL_STRLEN + 1];
char Username[POWERMETER_MAX_USERNAME_STRLEN + 1];
char Password[POWERMETER_MAX_USERNAME_STRLEN + 1];
uint16_t Timeout;
struct POWERMETER_HTTP_SML_CONFIG_T {
HttpRequestConfig HttpRequest;
};
using PowerMeterTibberConfig = struct POWERMETER_TIBBER_CONFIG_T;
using PowerMeterHttpSmlConfig = struct POWERMETER_HTTP_SML_CONFIG_T;
struct CONFIG_T {
struct {
@ -204,10 +211,9 @@ struct CONFIG_T {
char MqttTopicPowerMeter2[MQTT_MAX_TOPIC_STRLEN + 1];
char MqttTopicPowerMeter3[MQTT_MAX_TOPIC_STRLEN + 1];
uint32_t SdmAddress;
uint32_t HttpInterval;
bool HttpIndividualRequests;
PowerMeterHttpConfig Http_Phase[POWERMETER_MAX_PHASES];
PowerMeterTibberConfig Tibber;
PowerMeterHttpJsonConfig HttpJson[POWERMETER_HTTP_JSON_MAX_VALUES];
PowerMeterHttpSmlConfig HttpSml;
} PowerMeter;
struct {
@ -280,6 +286,14 @@ public:
INVERTER_CONFIG_T* getFreeInverterSlot();
INVERTER_CONFIG_T* getInverterConfig(const uint64_t serial);
void deleteInverterById(const uint8_t id);
static void serializeHttpRequestConfig(HttpRequestConfig const& source, JsonObject& target);
static void serializePowerMeterHttpJsonConfig(PowerMeterHttpJsonConfig const& source, JsonObject& target);
static void serializePowerMeterHttpSmlConfig(PowerMeterHttpSmlConfig const& source, JsonObject& target);
static void deserializeHttpRequestConfig(JsonObject const& source, HttpRequestConfig& target);
static void deserializePowerMeterHttpJsonConfig(JsonObject const& source, PowerMeterHttpJsonConfig& target);
static void deserializePowerMeterHttpSmlConfig(JsonObject const& source, PowerMeterHttpSmlConfig& target);
};
extern ConfigurationClass Configuration;

View File

@ -8,8 +8,8 @@
#include "Configuration.h"
#include "PowerMeterProvider.h"
using Auth_t = PowerMeterHttpConfig::Auth;
using Unit_t = PowerMeterHttpConfig::Unit;
using Auth_t = HttpRequestConfig::Auth;
using Unit_t = PowerMeterHttpJsonConfig::Unit;
class PowerMeterHttpJson : public PowerMeterProvider {
public:
@ -20,19 +20,19 @@ public:
float getPowerTotal() const final;
void doMqttPublish() const final;
bool queryPhase(int phase, PowerMeterHttpConfig const& config);
bool queryValue(int phase, PowerMeterHttpJsonConfig const& config);
char httpPowerMeterError[256];
float getCached(size_t idx) { return _cache[idx]; }
private:
uint32_t _lastPoll;
std::array<float,POWERMETER_MAX_PHASES> _cache;
std::array<float,POWERMETER_MAX_PHASES> _powerValues;
std::array<float, POWERMETER_HTTP_JSON_MAX_VALUES> _cache;
std::array<float, POWERMETER_HTTP_JSON_MAX_VALUES> _powerValues;
std::unique_ptr<WiFiClient> wifiClient;
std::unique_ptr<HTTPClient> httpClient;
String httpResponse;
bool httpRequest(int phase, const String& host, uint16_t port, const String& uri, bool https, PowerMeterHttpConfig const& config);
bool httpRequest(int phase, const String& host, uint16_t port, const String& uri, bool https, PowerMeterHttpJsonConfig 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);

View File

@ -16,7 +16,7 @@ public:
void loop() final;
bool updateValues();
char tibberPowerMeterError[256];
bool query(PowerMeterTibberConfig const& config);
bool query(HttpRequestConfig const& config);
private:
uint32_t _lastPoll = 0;
@ -24,7 +24,7 @@ private:
std::unique_ptr<WiFiClient> wifiClient;
std::unique_ptr<HTTPClient> httpClient;
String httpResponse;
bool httpRequest(const String& host, uint16_t port, const String& uri, bool https, PowerMeterTibberConfig const& config);
bool httpRequest(const String& host, uint16_t port, const String& uri, bool https, HttpRequestConfig const& config);
bool extractUrlComponents(String url, String& _protocol, String& _hostname, String& _uri, uint16_t& uint16_t, String& _base64Authorization);
void prepareRequest(uint32_t timeout);
};

View File

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

View File

@ -119,6 +119,8 @@
#define POWERMETER_SOURCE 2
#define POWERMETER_SDMADDRESS 1
#define HTTP_REQUEST_TIMEOUT_MS 1000
#define POWERLIMITER_ENABLED false
#define POWERLIMITER_SOLAR_PASSTHROUGH_ENABLED true
#define POWERLIMITER_SOLAR_PASSTHROUGH_LOSSES 3

View File

@ -6,7 +6,6 @@
#include "MessageOutput.h"
#include "Utils.h"
#include "defaults.h"
#include <ArduinoJson.h>
#include <LittleFS.h>
#include <nvs_flash.h>
@ -17,6 +16,33 @@ void ConfigurationClass::init()
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::serializePowerMeterHttpJsonConfig(PowerMeterHttpJsonConfig const& source, JsonObject& target)
{
serializeHttpRequestConfig(source.HttpRequest, target);
target["enabled"] = source.Enabled;
target["json_path"] = source.JsonPath;
target["unit"] = source.PowerUnit;
target["sign_inverted"] = source.SignInverted;
}
void ConfigurationClass::serializePowerMeterHttpSmlConfig(PowerMeterHttpSmlConfig const& source, JsonObject& target)
{
serializeHttpRequestConfig(source.HttpRequest, target);
}
bool ConfigurationClass::write()
{
File f = LittleFS.open(CONFIG_FILENAME, "w");
@ -158,27 +184,14 @@ bool ConfigurationClass::write()
powermeter["sdmaddress"] = config.PowerMeter.SdmAddress;
powermeter["http_individual_requests"] = config.PowerMeter.HttpIndividualRequests;
JsonObject tibber = powermeter["tibber"].to<JsonObject>();
tibber["url"] = config.PowerMeter.Tibber.Url;
tibber["username"] = config.PowerMeter.Tibber.Username;
tibber["password"] = config.PowerMeter.Tibber.Password;
tibber["timeout"] = config.PowerMeter.Tibber.Timeout;
JsonObject powermeter_http_sml = powermeter["http_sml"].to<JsonObject>();
serializePowerMeterHttpSmlConfig(config.PowerMeter.HttpSml, powermeter_http_sml);
JsonArray powermeter_http_phases = powermeter["http_phases"].to<JsonArray>();
for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) {
JsonObject powermeter_phase = powermeter_http_phases.add<JsonObject>();
powermeter_phase["enabled"] = config.PowerMeter.Http_Phase[i].Enabled;
powermeter_phase["url"] = config.PowerMeter.Http_Phase[i].Url;
powermeter_phase["auth_type"] = config.PowerMeter.Http_Phase[i].AuthType;
powermeter_phase["username"] = config.PowerMeter.Http_Phase[i].Username;
powermeter_phase["password"] = config.PowerMeter.Http_Phase[i].Password;
powermeter_phase["header_key"] = config.PowerMeter.Http_Phase[i].HeaderKey;
powermeter_phase["header_value"] = config.PowerMeter.Http_Phase[i].HeaderValue;
powermeter_phase["timeout"] = config.PowerMeter.Http_Phase[i].Timeout;
powermeter_phase["json_path"] = config.PowerMeter.Http_Phase[i].JsonPath;
powermeter_phase["unit"] = config.PowerMeter.Http_Phase[i].PowerUnit;
powermeter_phase["sign_inverted"] = config.PowerMeter.Http_Phase[i].SignInverted;
JsonArray powermeter_http_json = powermeter["http_json"].to<JsonArray>();
for (uint8_t i = 0; i < POWERMETER_HTTP_JSON_MAX_VALUES; i++) {
JsonObject powermeter_json_config = powermeter_http_json.add<JsonObject>();
serializePowerMeterHttpJsonConfig(config.PowerMeter.HttpJson[i],
powermeter_json_config);
}
JsonObject powerlimiter = doc["powerlimiter"].to<JsonObject>();
@ -246,6 +259,38 @@ bool ConfigurationClass::write()
return true;
}
void ConfigurationClass::deserializeHttpRequestConfig(JsonObject const& source, HttpRequestConfig& target)
{
JsonObject source_http_config = source["http_request"];
// http request parameters of HTTP/JSON power meter were
// previously stored alongside other settings
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::deserializePowerMeterHttpJsonConfig(JsonObject const& source, PowerMeterHttpJsonConfig& target)
{
deserializeHttpRequestConfig(source, target.HttpRequest);
target.Enabled = source["enabled"] | false;
strlcpy(target.JsonPath, source["json_path"] | "", sizeof(target.JsonPath));
target.PowerUnit = source["unit"] | PowerMeterHttpJsonConfig::Unit::Watts;
target.SignInverted = source["sign_inverted"] | false;
}
void ConfigurationClass::deserializePowerMeterHttpSmlConfig(JsonObject const& source, PowerMeterHttpSmlConfig& target)
{
deserializeHttpRequestConfig(source, target.HttpRequest);
}
bool ConfigurationClass::read()
{
File f = LittleFS.open(CONFIG_FILENAME, "r", false);
@ -424,27 +469,16 @@ bool ConfigurationClass::read()
config.PowerMeter.SdmAddress = powermeter["sdmaddress"] | POWERMETER_SDMADDRESS;
config.PowerMeter.HttpIndividualRequests = powermeter["http_individual_requests"] | false;
JsonObject tibber = powermeter["tibber"];
strlcpy(config.PowerMeter.Tibber.Url, tibber["url"] | "", sizeof(config.PowerMeter.Tibber.Url));
strlcpy(config.PowerMeter.Tibber.Username, tibber["username"] | "", sizeof(config.PowerMeter.Tibber.Username));
strlcpy(config.PowerMeter.Tibber.Password, tibber["password"] | "", sizeof(config.PowerMeter.Tibber.Password));
config.PowerMeter.Tibber.Timeout = tibber["timeout"] | POWERMETER_HTTP_TIMEOUT;
JsonObject powermeter_sml = powermeter["http_sml"];
deserializePowerMeterHttpSmlConfig(powermeter_sml, config.PowerMeter.HttpSml);
JsonArray powermeter_http_phases = powermeter["http_phases"];
for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) {
JsonObject powermeter_phase = powermeter_http_phases[i].as<JsonObject>();
config.PowerMeter.Http_Phase[i].Enabled = powermeter_phase["enabled"] | (i == 0);
strlcpy(config.PowerMeter.Http_Phase[i].Url, powermeter_phase["url"] | "", sizeof(config.PowerMeter.Http_Phase[i].Url));
config.PowerMeter.Http_Phase[i].AuthType = powermeter_phase["auth_type"] | PowerMeterHttpConfig::Auth::None;
strlcpy(config.PowerMeter.Http_Phase[i].Username, powermeter_phase["username"] | "", sizeof(config.PowerMeter.Http_Phase[i].Username));
strlcpy(config.PowerMeter.Http_Phase[i].Password, powermeter_phase["password"] | "", sizeof(config.PowerMeter.Http_Phase[i].Password));
strlcpy(config.PowerMeter.Http_Phase[i].HeaderKey, powermeter_phase["header_key"] | "", sizeof(config.PowerMeter.Http_Phase[i].HeaderKey));
strlcpy(config.PowerMeter.Http_Phase[i].HeaderValue, powermeter_phase["header_value"] | "", sizeof(config.PowerMeter.Http_Phase[i].HeaderValue));
config.PowerMeter.Http_Phase[i].Timeout = powermeter_phase["timeout"] | POWERMETER_HTTP_TIMEOUT;
strlcpy(config.PowerMeter.Http_Phase[i].JsonPath, powermeter_phase["json_path"] | "", sizeof(config.PowerMeter.Http_Phase[i].JsonPath));
config.PowerMeter.Http_Phase[i].PowerUnit = powermeter_phase["unit"] | PowerMeterHttpConfig::Unit::Watts;
config.PowerMeter.Http_Phase[i].SignInverted = powermeter_phase["sign_inverted"] | false;
JsonArray powermeter_http_json = powermeter["http_json"];
if (powermeter_http_json.isNull()) {
powermeter_http_json = powermeter["http_phases"]; // http_phases is a legacy key
}
for (uint8_t i = 0; i < POWERMETER_HTTP_JSON_MAX_VALUES; i++) {
JsonObject powermeter_json_config = powermeter_http_json[i].as<JsonObject>();
deserializePowerMeterHttpJsonConfig(powermeter_json_config, config.PowerMeter.HttpJson[i]);
}
JsonObject powerlimiter = doc["powerlimiter"];

View File

@ -27,16 +27,16 @@ void PowerMeterHttpJson::loop()
_lastPoll = millis();
for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) {
auto const& phaseConfig = config.PowerMeter.Http_Phase[i];
for (uint8_t i = 0; i < POWERMETER_HTTP_JSON_MAX_VALUES; i++) {
auto const& valueConfig = config.PowerMeter.HttpJson[i];
if (!phaseConfig.Enabled) {
if (!valueConfig.Enabled) {
_cache[i] = 0.0;
continue;
}
if (i == 0 || config.PowerMeter.HttpIndividualRequests) {
if (!queryPhase(i, phaseConfig)) {
if (!queryValue(i, valueConfig)) {
MessageOutput.printf("[PowerMeterHttpJson] Getting HTTP response for phase %d failed.\r\n", i + 1);
MessageOutput.printf("%s\r\n", httpPowerMeterError);
return;
@ -44,7 +44,7 @@ void PowerMeterHttpJson::loop()
continue;
}
if(!tryGetFloatValueForPhase(i, phaseConfig.JsonPath, phaseConfig.PowerUnit, phaseConfig.SignInverted)) {
if(!tryGetFloatValueForPhase(i, valueConfig.JsonPath, valueConfig.PowerUnit, valueConfig.SignInverted)) {
MessageOutput.printf("[PowerMeterHttpJson] Reading power of phase %d (from JSON fetched with Phase 1 config) failed.\r\n", i + 1);
MessageOutput.printf("%s\r\n", httpPowerMeterError);
return;
@ -70,7 +70,7 @@ void PowerMeterHttpJson::doMqttPublish() const
mqttPublish("power3", _powerValues[2]);
}
bool PowerMeterHttpJson::queryPhase(int phase, PowerMeterHttpConfig const& config)
bool PowerMeterHttpJson::queryValue(int phase, PowerMeterHttpJsonConfig const& config)
{
//hostByName in WiFiGeneric fails to resolve local names. issue described in
//https://github.com/espressif/arduino-esp32/issues/3822
@ -82,7 +82,7 @@ bool PowerMeterHttpJson::queryPhase(int phase, PowerMeterHttpConfig const& confi
String uri;
String base64Authorization;
uint16_t port;
extractUrlComponents(config.Url, protocol, host, uri, port, base64Authorization);
extractUrlComponents(config.HttpRequest.Url, protocol, host, uri, port, base64Authorization);
IPAddress ipaddr((uint32_t)0);
//first check if "host" is already an IP adress
@ -123,7 +123,7 @@ bool PowerMeterHttpJson::queryPhase(int phase, PowerMeterHttpConfig const& confi
return httpRequest(phase, ipaddr.toString(), port, uri, https, config);
}
bool PowerMeterHttpJson::httpRequest(int phase, const String& host, uint16_t port, const String& uri, bool https, PowerMeterHttpConfig const& config)
bool PowerMeterHttpJson::httpRequest(int phase, const String& host, uint16_t port, const String& uri, bool https, PowerMeterHttpJsonConfig const& powerMeterConfig)
{
if (!httpClient) { httpClient = std::make_unique<HTTPClient>(); }
@ -132,6 +132,7 @@ bool PowerMeterHttpJson::httpRequest(int phase, const String& host, uint16_t por
return false;
}
auto const& config = powerMeterConfig.HttpRequest;
prepareRequest(config.Timeout, config.HeaderKey, config.HeaderValue);
if (config.AuthType == Auth_t::Digest) {
const char *headers[1] = {"WWW-Authenticate"};
@ -178,7 +179,7 @@ bool PowerMeterHttpJson::httpRequest(int phase, const String& host, uint16_t por
// 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);
return tryGetFloatValueForPhase(phase, powerMeterConfig.JsonPath, powerMeterConfig.PowerUnit, powerMeterConfig.SignInverted);
}
String PowerMeterHttpJson::extractParam(String& authReq, const String& param, const char delimit) {

View File

@ -24,15 +24,13 @@ void PowerMeterHttpSml::loop()
_lastPoll = millis();
auto const& tibberConfig = config.PowerMeter.Tibber;
if (!query(tibberConfig)) {
if (!query(config.PowerMeter.HttpSml.HttpRequest)) {
MessageOutput.printf("[PowerMeterHttpSml] Getting the power value failed.\r\n");
MessageOutput.printf("%s\r\n", tibberPowerMeterError);
}
}
bool PowerMeterHttpSml::query(PowerMeterTibberConfig const& config)
bool PowerMeterHttpSml::query(HttpRequestConfig const& config)
{
//hostByName in WiFiGeneric fails to resolve local names. issue described in
//https://github.com/espressif/arduino-esp32/issues/3822
@ -85,7 +83,7 @@ bool PowerMeterHttpSml::query(PowerMeterTibberConfig const& config)
return httpRequest(ipaddr.toString(), port, uri, https, config);
}
bool PowerMeterHttpSml::httpRequest(const String& host, uint16_t port, const String& uri, bool https, PowerMeterTibberConfig const& config)
bool PowerMeterHttpSml::httpRequest(const String& host, uint16_t port, const String& uri, bool https, HttpRequestConfig const& config)
{
if (!httpClient) { httpClient = std::make_unique<HTTPClient>(); }

View File

@ -26,31 +26,8 @@ void WebApiPowerMeterClass::init(AsyncWebServer& server, Scheduler& scheduler)
_server->on("/api/powermeter/status", HTTP_GET, std::bind(&WebApiPowerMeterClass::onStatus, this, _1));
_server->on("/api/powermeter/config", HTTP_GET, std::bind(&WebApiPowerMeterClass::onAdminGet, this, _1));
_server->on("/api/powermeter/config", HTTP_POST, std::bind(&WebApiPowerMeterClass::onAdminPost, this, _1));
_server->on("/api/powermeter/testhttprequest", HTTP_POST, std::bind(&WebApiPowerMeterClass::onTestHttpRequest, this, _1));
_server->on("/api/powermeter/testtibberrequest", HTTP_POST, std::bind(&WebApiPowerMeterClass::onTestTibberRequest, 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::decodeJsonTibberConfig(JsonObject const& json, PowerMeterTibberConfig& config) const
{
strlcpy(config.Url, json["url"].as<String>().c_str(), sizeof(config.Url));
strlcpy(config.Username, json["username"].as<String>().c_str(), sizeof(config.Username));
strlcpy(config.Password, json["password"].as<String>().c_str(), sizeof(config.Password));
config.Timeout = json["timeout"].as<uint16_t>();
_server->on("/api/powermeter/testhttpjsonrequest", HTTP_POST, std::bind(&WebApiPowerMeterClass::onTestHttpJsonRequest, this, _1));
_server->on("/api/powermeter/testhttpsmlrequest", HTTP_POST, std::bind(&WebApiPowerMeterClass::onTestHttpSmlRequest, this, _1));
}
void WebApiPowerMeterClass::onStatus(AsyncWebServerRequest* request)
@ -69,29 +46,14 @@ void WebApiPowerMeterClass::onStatus(AsyncWebServerRequest* request)
root["sdmaddress"] = config.PowerMeter.SdmAddress;
root["http_individual_requests"] = config.PowerMeter.HttpIndividualRequests;
auto tibber = root["tibber"].to<JsonObject>();
tibber["url"] = String(config.PowerMeter.Tibber.Url);
tibber["username"] = String(config.PowerMeter.Tibber.Username);
tibber["password"] = String(config.PowerMeter.Tibber.Password);
tibber["timeout"] = config.PowerMeter.Tibber.Timeout;
auto httpSml = root["http_sml"].to<JsonObject>();
Configuration.serializePowerMeterHttpSmlConfig(config.PowerMeter.HttpSml, httpSml);
auto httpPhases = root["http_phases"].to<JsonArray>();
for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) {
auto phaseObject = httpPhases.add<JsonObject>();
phaseObject["index"] = i + 1;
phaseObject["enabled"] = config.PowerMeter.Http_Phase[i].Enabled;
phaseObject["url"] = String(config.PowerMeter.Http_Phase[i].Url);
phaseObject["auth_type"]= config.PowerMeter.Http_Phase[i].AuthType;
phaseObject["username"] = String(config.PowerMeter.Http_Phase[i].Username);
phaseObject["password"] = String(config.PowerMeter.Http_Phase[i].Password);
phaseObject["header_key"] = String(config.PowerMeter.Http_Phase[i].HeaderKey);
phaseObject["header_value"] = String(config.PowerMeter.Http_Phase[i].HeaderValue);
phaseObject["timeout"] = config.PowerMeter.Http_Phase[i].Timeout;
phaseObject["json_path"] = String(config.PowerMeter.Http_Phase[i].JsonPath);
phaseObject["unit"] = config.PowerMeter.Http_Phase[i].PowerUnit;
phaseObject["sign_inverted"] = config.PowerMeter.Http_Phase[i].SignInverted;
auto httpJson = root["http_json"].to<JsonArray>();
for (uint8_t i = 0; i < POWERMETER_HTTP_JSON_MAX_VALUES; i++) {
auto valueConfig = httpJson.add<JsonObject>();
valueConfig["index"] = i + 1;
Configuration.serializePowerMeterHttpJsonConfig(config.PowerMeter.HttpJson[i], valueConfig);
}
WebApi.sendJsonResponse(request, response, __FUNCTION__, __LINE__);
@ -127,44 +89,52 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request)
return;
}
if (static_cast<PowerMeterProvider::Type>(root["source"].as<uint8_t>()) == PowerMeterProvider::Type::HTTP_JSON) {
JsonArray http_phases = root["http_phases"];
for (uint8_t i = 0; i < http_phases.size(); i++) {
JsonObject phase = http_phases[i].as<JsonObject>();
auto checkHttpConfig = [&](JsonObject const& cfg) -> bool {
if (!cfg.containsKey("url")
|| (!cfg["url"].as<String>().startsWith("http://")
&& !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) {
JsonArray httpJson = root["http_json"];
for (uint8_t i = 0; i < httpJson.size(); i++) {
JsonObject valueConfig = httpJson[i].as<JsonObject>();
if (i > 0 && !valueConfig["enabled"].as<bool>()) {
continue;
}
if (i == 0 || phase["http_individual_requests"].as<bool>()) {
if (!phase.containsKey("url")
|| (!phase["url"].as<String>().startsWith("http://")
&& !phase["url"].as<String>().startsWith("https://"))) {
retMsg["message"] = "URL must either start with http:// or https://!";
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);
if (i == 0 || valueConfig["http_individual_requests"].as<bool>()) {
if (!checkHttpConfig(valueConfig["http_request"].as<JsonObject>())) {
return;
}
}
if (!phase.containsKey("json_path")
|| phase["json_path"].as<String>().length() == 0) {
if (!valueConfig.containsKey("json_path")
|| valueConfig["json_path"].as<String>().length() == 0) {
retMsg["message"] = "Json path must not be empty!";
response->setLength();
request->send(response);
@ -174,29 +144,8 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request)
}
if (static_cast<PowerMeterProvider::Type>(root["source"].as<uint8_t>()) == PowerMeterProvider::Type::HTTP_SML) {
JsonObject tibber = root["tibber"];
if (!tibber.containsKey("url")
|| (!tibber["url"].as<String>().startsWith("http://")
&& !tibber["url"].as<String>().startsWith("https://"))) {
retMsg["message"] = "URL must either start with http:// or https://!";
response->setLength();
request->send(response);
return;
}
if ((tibber["username"].as<String>().length() == 0 || tibber["password"].as<String>().length() == 0)) {
retMsg["message"] = "Username or password must not be empty!";
response->setLength();
request->send(response);
return;
}
if (!tibber.containsKey("timeout")
|| tibber["timeout"].as<uint16_t>() <= 0) {
retMsg["message"] = "Timeout must be greater than 0 ms!";
response->setLength();
request->send(response);
JsonObject httpSml = root["http_sml"];
if (!checkHttpConfig(httpSml["http_request"].as<JsonObject>())) {
return;
}
}
@ -212,13 +161,15 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request)
config.PowerMeter.SdmAddress = root["sdmaddress"].as<uint8_t>();
config.PowerMeter.HttpIndividualRequests = root["http_individual_requests"].as<bool>();
decodeJsonTibberConfig(root["tibber"].as<JsonObject>(), config.PowerMeter.Tibber);
Configuration.deserializePowerMeterHttpSmlConfig(root["http_sml"].as<JsonObject>(),
config.PowerMeter.HttpSml);
JsonArray http_phases = root["http_phases"];
for (uint8_t i = 0; i < http_phases.size(); i++) {
decodeJsonPhaseConfig(http_phases[i].as<JsonObject>(), config.PowerMeter.Http_Phase[i]);
JsonArray httpJson = root["http_json"];
for (uint8_t i = 0; i < httpJson.size(); i++) {
Configuration.deserializePowerMeterHttpJsonConfig(httpJson[i].as<JsonObject>(),
config.PowerMeter.HttpJson[i]);
}
config.PowerMeter.Http_Phase[0].Enabled = true;
config.PowerMeter.HttpJson[0].Enabled = true;
WebApi.writeConfig(retMsg);
@ -227,7 +178,7 @@ void WebApiPowerMeterClass::onAdminPost(AsyncWebServerRequest* request)
PowerMeter.updateSettings();
}
void WebApiPowerMeterClass::onTestHttpRequest(AsyncWebServerRequest* request)
void WebApiPowerMeterClass::onTestHttpJsonRequest(AsyncWebServerRequest* request)
{
if (!WebApi.checkCredentials(request)) {
return;
@ -241,9 +192,15 @@ void WebApiPowerMeterClass::onTestHttpRequest(AsyncWebServerRequest* request)
auto& retMsg = asyncJsonResponse->getRoot();
if (!root.containsKey("url") || !root.containsKey("auth_type") || !root.containsKey("username") || !root.containsKey("password")
|| !root.containsKey("header_key") || !root.containsKey("header_value")
|| !root.containsKey("timeout") || !root.containsKey("json_path")) {
JsonObject requestConfig = root["http_request"];
if (!requestConfig.containsKey("url")
|| !requestConfig.containsKey("auth_type")
|| !requestConfig.containsKey("username")
|| !requestConfig.containsKey("password")
|| !requestConfig.containsKey("header_key")
|| !requestConfig.containsKey("header_value")
|| !requestConfig.containsKey("timeout")
|| !root.containsKey("json_path")) {
retMsg["message"] = "Missing fields!";
asyncJsonResponse->setLength();
request->send(asyncJsonResponse);
@ -253,10 +210,10 @@ void WebApiPowerMeterClass::onTestHttpRequest(AsyncWebServerRequest* request)
char response[256];
PowerMeterHttpConfig phaseConfig;
decodeJsonPhaseConfig(root.as<JsonObject>(), phaseConfig);
PowerMeterHttpJsonConfig httpJsonConfig;
Configuration.deserializePowerMeterHttpJsonConfig(root.as<JsonObject>(), httpJsonConfig);
auto upMeter = std::make_unique<PowerMeterHttpJson>();
if (upMeter->queryPhase(0/*phase*/, phaseConfig)) {
if (upMeter->queryValue(0/*value index*/, httpJsonConfig)) {
retMsg["type"] = "success";
snprintf_P(response, sizeof(response), "Success! Power: %5.2fW", upMeter->getCached(0));
} else {
@ -268,7 +225,7 @@ void WebApiPowerMeterClass::onTestHttpRequest(AsyncWebServerRequest* request)
request->send(asyncJsonResponse);
}
void WebApiPowerMeterClass::onTestTibberRequest(AsyncWebServerRequest* request)
void WebApiPowerMeterClass::onTestHttpSmlRequest(AsyncWebServerRequest* request)
{
if (!WebApi.checkCredentials(request)) {
return;
@ -293,10 +250,10 @@ void WebApiPowerMeterClass::onTestTibberRequest(AsyncWebServerRequest* request)
char response[256];
PowerMeterTibberConfig tibberConfig;
decodeJsonTibberConfig(root.as<JsonObject>(), tibberConfig);
PowerMeterHttpSmlConfig httpSmlConfig;
Configuration.deserializePowerMeterHttpSmlConfig(root.as<JsonObject>(), httpSmlConfig);
auto upMeter = std::make_unique<PowerMeterHttpSml>();
if (upMeter->query(tibberConfig)) {
if (upMeter->query(httpSmlConfig.HttpRequest)) {
retMsg["type"] = "success";
snprintf_P(response, sizeof(response), "Success! Power: %5.2fW", upMeter->getPowerTotal());
} else {

View File

@ -0,0 +1,77 @@
<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')" />
<div class="row mb-3">
<label for="auth_type" class="col-sm-2 col-form-label">{{ $t('httprequestsettings.authorization') }}</label>
<div class="col-sm-10">
<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"/>
<InputElement
v-if="cfg.auth_type != 0"
:label="$t('httprequestsettings.password')"
v-model="cfg.password"
type="password" maxlength="64"/>
<InputElement
:label="$t('httprequestsettings.headerKey')"
v-model="cfg.header_key"
type="text"
maxlength="64"
:tooltip="$t('httprequestsettings.headerKeyDescription')" />
<InputElement
:label="$t('httprequestsettings.headerValue')"
v-model="cfg.header_value"
type="text"
maxlength="256" />
<InputElement
:label="$t('httprequestsettings.timeout')"
v-model="cfg.timeout"
type="number"
:postfix="$t('httprequestsettings.milliSeconds')" />
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import InputElement from '@/components/InputElement.vue';
export default defineComponent({
props: {
'cfg': { type: Object, required: true }
},
components: {
InputElement
},
data() {
return {
authTypeList: [
{ key: 0, value: "None" },
{ key: 1, value: "Basic" },
{ key: 2, value: "Digest" },
]
};
}
});
</script>

View File

@ -558,37 +558,47 @@
"PowerMeterSource": "Stromzählertyp",
"MQTT": "MQTT Konfiguration",
"typeMQTT": "MQTT",
"typeSDM1ph": "SDM 1 phase (SDM120/220/230)",
"typeSDM3ph": "SDM 3 phase (SDM72/630)",
"typeHTTP": "HTTP(S) + JSON",
"typeSML": "SML (OBIS 16.7.0)",
"typeSDM1ph": "SDM mit 1 Phase (SDM120/220/230)",
"typeSDM3ph": "SDM mit 3 Phasen (SDM72/630)",
"typeHTTP_JSON": "HTTP(S) + JSON",
"typeSML": "SML/OBIS via serieller Verbindung (z.B. Hichi TTL)",
"typeSMAHM2": "SMA Homemanager 2.0",
"typeTIBBER": "Tibber Pulse (via Tibber Bridge)",
"typeHTTP_SML": "HTTP(S) + SML (z.B. Tibber Pulse via Tibber Bridge)",
"MqttTopicPowerMeter1": "MQTT topic - Stromzähler #1",
"MqttTopicPowerMeter2": "MQTT topic - Stromzähler #2 (Optional)",
"MqttTopicPowerMeter3": "MQTT topic - Stromzähler #3 (Optional)",
"SDM": "SDM-Stromzähler Konfiguration",
"sdmaddress": "Modbus Adresse",
"HTTP": "HTTP(S) + JSON - Allgemeine Konfiguration",
"httpIndividualRequests": "Individuelle HTTP requests pro Phase",
"HTTP_JSON": "HTTP(S) + JSON - Allgemeine Konfiguration",
"httpIndividualRequests": "Individuelle HTTP Anfragen pro Wert",
"urlExamplesHeading": "Beispiele für URLs",
"jsonPathExamplesHeading": "Beispiele für JSON Pfade",
"jsonPathExamplesExplanation": "Die folgenden Pfade finden jeweils den Wert '123.4' im jeweiligen Beispiel-JSON.",
"httpUrlDescription": "Die URL muss mit http:// oder https:// beginnen. Manche Zeichen wie Leerzeichen und = müssen mit URL-Kodierung kodiert werden (%xx). Achtung: Ein Überprüfung von SSL Server Zertifikaten ist nicht implementiert (MITM-Attacken sind möglich)!.",
"httpPhase": "HTTP(S) + JSON Konfiguration - Phase {phaseNumber}",
"httpEnabled": "Phase aktiviert",
"httpUrl": "URL",
"httpHeaderKey": "Optional: HTTP request header - Key",
"httpHeaderKeyDescription": "Ein individueller HTTP request header kann hier definiert werden. Das kann z.B. verwendet werden um einen eigenen Authorization header mitzugeben.",
"httpHeaderValue": "Optional: HTTP request header - Wert",
"httpValue": "Konfiguration für Wert {valueNumber}",
"httpEnabled": "Wert aktiviert",
"httpJsonPath": "JSON Pfad",
"httpJsonPathDescription": "Anwendungsspezifischer JSON-Pfad um den Leistungswert in the HTTP(S) Antwort zu finden, z.B. 'power/total/watts' oder nur 'total'.",
"httpUnit": "Einheit",
"httpSignInverted": "Vorzeichen umkehren",
"httpSignInvertedHint": "Positive Werte werden als Leistungsabnahme aus dem Netz interpretiert. Diese Option muss aktiviert werden, wenn das Vorzeichen des Wertes die gegenteilige Bedeutung hat.",
"httpTimeout": "Timeout",
"testHttpRequest": "Testen",
"TIBBER": "Tibber Pulse (via Tibber Bridge) - Konfiguration"
"testHttpJsonRequest": "Konfiguration testen (HTTP(S)-Anfrage senden)",
"testHttpSmlRequest": "Konfiguration testen (HTTP(S)-Anfrage senden)",
"HTTP_SML": "HTTP(S) + SML - Konfiguration"
},
"httprequestsettings": {
"url": "URL",
"urlDescription": "Die URL muss mit 'http://' oder 'https://' beginnen. Zeichen wie Leerzeichen und = müssen mit URL-kodiert werden (%xx). Achtung: Eine Überprüfung von SSL-Server-Zertifikaten ist nicht implementiert (MITM-Attacken sind möglich)!.",
"authorization": "Authentifizierungsverfahren",
"authTypeNone": "Ohne",
"authTypeBasic": "Basic",
"authTypeDigest": "Digest",
"username": "Benutzername",
"password": "Passwort",
"headerKey": "HTTP Header - Name",
"headerKeyDescription": "Optional. Ein benutzerdefinierter HTTP header kann definiert werden. Nützlich um z.B. ein (zusätzlichen) Authentifizierungstoken zu übermitteln.",
"headerValue": "HTTP Header - Wert",
"timeout": "Zeitüberschreitung",
"milliSeconds": "ms"
},
"powerlimiteradmin": {
"PowerLimiterSettings": "Dynamic Power Limiter Einstellungen",

View File

@ -560,41 +560,47 @@
"PowerMeterSource": "Power Meter type",
"MQTT": "MQTT Parameter",
"typeMQTT": "MQTT",
"typeSDM1ph": "SDM 1 phase (SDM120/220/230)",
"typeSDM3ph": "SDM 3 phase (SDM72/630)",
"typeHTTP": "HTTP(s) + JSON",
"typeSML": "SML (OBIS 16.7.0)",
"typeSDM1ph": "SDM for 1 phase (SDM120/220/230)",
"typeSDM3ph": "SDM for 3 phases (SDM72/630)",
"typeHTTP_JSON": "HTTP(S) + JSON",
"typeSML": "SML/OBIS via serial connection (e.g. Hichi TTL)",
"typeSMAHM2": "SMA Homemanager 2.0",
"typeTIBBER": "Tibber Pulse (via Tibber Bridge)",
"typeHTTP_SML": "HTTP(S) + SML (e.g. Tibber Pulse via Tibber Bridge)",
"MqttTopicPowerMeter1": "MQTT topic - Power meter #1",
"MqttTopicPowerMeter2": "MQTT topic - Power meter #2",
"MqttTopicPowerMeter3": "MQTT topic - Power meter #3",
"SDM": "SDM-Power Meter Parameter",
"sdmaddress": "Modbus Address",
"HTTP": "HTTP(S) + Json - General configuration",
"httpIndividualRequests": "Individual HTTP requests per phase",
"HTTP": "HTTP(S) + JSON - General configuration",
"httpIndividualRequests": "Individual HTTP requests per value",
"urlExamplesHeading": "URL Examples",
"jsonPathExamplesHeading": "JSON Path Examples",
"jsonPathExamplesExplanation": "The following paths each find the value '123.4' in the respective example JSON.",
"httpPhase": "HTTP(S) + Json configuration - Phase {phaseNumber}",
"httpEnabled": "Phase enabled",
"httpUrl": "URL",
"httpUrlDescription": "URL must start with http:// or https://. Some characters like spaces and = have to be encoded with URL encoding (%xx). Warning: SSL server certificate check is not implemented (MITM attacks are possible)!",
"httpAuthorization": "Authorization Type",
"httpUsername": "Username",
"httpPassword": "Password",
"httpHeaderKey": "Optional: HTTP request header - Key",
"httpHeaderKeyDescription": "A custom HTTP request header can be defined. Might be useful if you have to send something like a custom Authorization header.",
"httpHeaderValue": "Optional: HTTP request header - Value",
"httpValue": "Configuration for value {valueNumber}",
"httpEnabled": "Value enabled",
"httpJsonPath": "JSON path",
"httpJsonPathDescription": "Application specific JSON path to find the power value in the HTTP(S) response, e.g., 'power/total/watts' or simply 'total'.",
"httpUnit": "Unit",
"httpSignInverted": "Change Sign",
"httpSignInvertedHint": "Is is expected that positive values denote power usage from the grid. Check this option if the sign of this value has the opposite meaning.",
"httpTimeout": "Timeout",
"testHttpRequest": "Run test",
"milliSeconds": "ms",
"TIBBER": "Tibber Pulse (via Tibber Bridge) - Configuration"
"testHttpJsonRequest": "Test configuration (send HTTP(S) request)",
"testHttpSmlRequest": "Test configuration (send HTTP(S) request)",
"HTTP_SML": "Configuration"
},
"httprequestsettings": {
"url": "URL",
"urlDescription": "URL must start with 'http://' or 'https://'. Characters like spaces and '=' have to be URL-encoded (%xx). Warning: SSL server certificate check is not implemented (MITM attacks are possible)!",
"authorization": "Authorization Type",
"authTypeNone": "None",
"authTypeBasic": "Basic",
"authTypeDigest": "Digest",
"username": "Username",
"password": "Password",
"headerKey": "HTTP Header - Key",
"headerKeyDescription": "Optional. A custom HTTP header key-value pair can be defined. Useful, e.g., to send an (additional) authentication token.",
"headerValue": "HTTP Header - Value",
"timeout": "Timeout",
"milliSeconds": "ms"
},
"powerlimiteradmin": {
"PowerLimiterSettings": "Dynamic Power Limiter Settings",

View File

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

View File

@ -1,23 +1,16 @@
export interface PowerMeterHttpPhaseConfig {
import type { HttpRequestConfig } from '@/types/HttpRequestConfig';
export interface PowerMeterHttpJsonConfig {
index: number;
http_request: HttpRequestConfig;
enabled: boolean;
url: string;
auth_type: number;
username: string;
password: string;
header_key: string;
header_value: string;
json_path: string;
timeout: number;
unit: number;
sign_inverted: boolean;
}
export interface PowerMeterTibberConfig {
url: string;
username: string;
password: string;
timeout: number;
export interface PowerMeterHttpSmlConfig {
http_request: HttpRequestConfig;
}
export interface PowerMeterConfig {
@ -30,6 +23,6 @@ export interface PowerMeterConfig {
mqtt_topic_powermeter_3: string;
sdmaddress: number;
http_individual_requests: boolean;
http_phases: Array<PowerMeterHttpPhaseConfig>;
tibber: PowerMeterTibberConfig;
http_json: Array<PowerMeterHttpJsonConfig>;
http_sml: PowerMeterHttpSmlConfig;
}

View File

@ -114,66 +114,23 @@
</div>
<CardElement
v-for="(http_phase, index) in powerMeterConfigList.http_phases"
:key="http_phase.index"
:text="$t('powermeteradmin.httpPhase', { phaseNumber: http_phase.index })"
v-for="(httpJson, index) in powerMeterConfigList.http_json"
:key="httpJson.index"
:text="$t('powermeteradmin.httpValue', { valueNumber: httpJson.index })"
textVariant="text-bg-primary"
add-space>
<InputElement
v-if="index > 0"
:label="$t('powermeteradmin.httpEnabled')"
v-model="http_phase.enabled"
v-model="httpJson.enabled"
type="checkbox" wide />
<div v-if="http_phase.enabled">
<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 v-if="httpJson.enabled">
<div class="row mb-3">
<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')"
v-model="http_phase.password"
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>
<HttpRequestSettings :cfg="httpJson.http_request" v-if="index == 0 || powerMeterConfigList.http_individual_requests"/>
<InputElement :label="$t('powermeteradmin.httpJsonPath')"
v-model="http_phase.json_path"
v-model="httpJson.json_path"
type="text"
maxlength="256"
placeholder="total_power"
@ -184,67 +141,48 @@
{{ $t('powermeteradmin.httpUnit') }}
</label>
<div class="col-sm-10">
<select id="power_unit" class="form-select" v-model="http_phase.unit">
<option value="1">mW</option>
<option value="0">W</option>
<option value="2">kW</option>
<select id="power_unit" class="form-select" v-model="httpJson.unit">
<option v-for="u in unitTypeList" :key="u.key" :value="u.key">
{{ u.value }}
</option>
</select>
</div>
</div>
<InputElement
:label="$t('powermeteradmin.httpSignInverted')"
v-model="http_phase.sign_inverted"
v-model="httpJson.sign_inverted"
:tooltip="$t('powermeteradmin.httpSignInvertedHint')"
type="checkbox" />
<div class="text-center mb-3">
<button type="button" class="btn btn-danger" @click="testHttpRequest(index)">
{{ $t('powermeteradmin.testHttpRequest') }}
<button type="button" class="btn btn-danger" @click="testHttpJsonRequest(index)">
{{ $t('powermeteradmin.testHttpJsonRequest') }}
</button>
</div>
<BootstrapAlert v-model="testHttpRequestAlert[index].show" dismissible :variant="testHttpRequestAlert[index].type">
{{ testHttpRequestAlert[index].message }}
<BootstrapAlert v-model="testHttpJsonRequestAlert[index].show" dismissible :variant="testHttpJsonRequestAlert[index].type">
{{ testHttpJsonRequestAlert[index].message }}
</BootstrapAlert>
</div>
</CardElement>
</div>
<div v-if="powerMeterConfigList.source === 6">
<CardElement :text="$t('powermeteradmin.TIBBER')"
<CardElement :text="$t('powermeteradmin.HTTP_SML')"
textVariant="text-bg-primary"
add-space>
<InputElement :label="$t('powermeteradmin.httpUrl')"
v-model="powerMeterConfigList.tibber.url"
type="text"
maxlength="1024"
placeholder="http://admin:supersecret@mypowermeter.home/status"
prefix="GET "
:tooltip="$t('powermeteradmin.httpUrlDescription')" />
<InputElement :label="$t('powermeteradmin.httpUsername')"
v-model="powerMeterConfigList.tibber.username"
type="text" maxlength="64"/>
<InputElement :label="$t('powermeteradmin.httpPassword')"
v-model="powerMeterConfigList.tibber.password"
type="password" maxlength="64"/>
<InputElement :label="$t('powermeteradmin.httpTimeout')"
v-model="powerMeterConfigList.tibber.timeout"
type="number"
:postfix="$t('powermeteradmin.milliSeconds')" />
<HttpRequestSettings :cfg="powerMeterConfigList.http_sml.http_request" />
<div class="text-center mb-3">
<button type="button" class="btn btn-danger" @click="testTibberRequest()">
{{ $t('powermeteradmin.testHttpRequest') }}
<button type="button" class="btn btn-danger" @click="testHttpSmlRequest()">
{{ $t('powermeteradmin.testHttpSmlRequest') }}
</button>
</div>
<BootstrapAlert v-model="testTibberRequestAlert.show" dismissible :variant="testTibberRequestAlert.type">
{{ testTibberRequestAlert.message }}
<BootstrapAlert v-model="testHttpSmlRequestAlert.show" dismissible :variant="testHttpSmlRequestAlert.type">
{{ testHttpSmlRequestAlert.message }}
</BootstrapAlert>
</CardElement>
</div>
@ -263,8 +201,9 @@ import BootstrapAlert from "@/components/BootstrapAlert.vue";
import CardElement from '@/components/CardElement.vue';
import FormFooter from '@/components/FormFooter.vue';
import InputElement from '@/components/InputElement.vue';
import HttpRequestSettings from '@/components/HttpRequestSettings.vue';
import { handleResponse, authHeader } from '@/utils/authentication';
import type { PowerMeterHttpPhaseConfig, PowerMeterConfig } from "@/types/PowerMeterConfig";
import type { PowerMeterHttpJsonConfig, PowerMeterConfig } from "@/types/PowerMeterConfig";
export default defineComponent({
components: {
@ -272,6 +211,7 @@ export default defineComponent({
BootstrapAlert,
CardElement,
FormFooter,
HttpRequestSettings,
InputElement
},
data() {
@ -282,21 +222,21 @@ export default defineComponent({
{ key: 0, value: this.$t('powermeteradmin.typeMQTT') },
{ key: 1, value: this.$t('powermeteradmin.typeSDM1ph') },
{ key: 2, value: this.$t('powermeteradmin.typeSDM3ph') },
{ key: 3, value: this.$t('powermeteradmin.typeHTTP') },
{ key: 3, value: this.$t('powermeteradmin.typeHTTP_JSON') },
{ key: 4, value: this.$t('powermeteradmin.typeSML') },
{ key: 5, value: this.$t('powermeteradmin.typeSMAHM2') },
{ key: 6, value: this.$t('powermeteradmin.typeTIBBER') },
{ key: 6, value: this.$t('powermeteradmin.typeHTTP_SML') },
],
powerMeterAuthList: [
{ key: 0, value: "None" },
{ key: 1, value: "Basic" },
{ key: 2, value: "Digest" },
unitTypeList: [
{ key: 1, value: "mW" },
{ key: 0, value: "W" },
{ key: 2, value: "kW" },
],
alertMessage: "",
alertType: "info",
showAlert: false,
testHttpRequestAlert: [{message: "", type: "", show: false}] as { message: string; type: string; show: boolean; }[],
testTibberRequestAlert: {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() {
@ -311,8 +251,8 @@ export default defineComponent({
this.powerMeterConfigList = data;
this.dataLoading = false;
for (let i = 0; i < this.powerMeterConfigList.http_phases.length; i++) {
this.testHttpRequestAlert.push({
for (let i = 0; i < this.powerMeterConfigList.http_json.length; i++) {
this.testHttpJsonRequestAlert.push({
message: "",
type: "",
show: false,
@ -341,27 +281,27 @@ export default defineComponent({
}
);
},
testHttpRequest(index: number) {
let phaseConfig:PowerMeterHttpPhaseConfig;
testHttpJsonRequest(index: number) {
let valueConfig:PowerMeterHttpJsonConfig;
if (this.powerMeterConfigList.http_individual_requests) {
phaseConfig = this.powerMeterConfigList.http_phases[index];
valueConfig = this.powerMeterConfigList.http_json[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;
valueConfig = { ...this.powerMeterConfigList.http_json[0] };
valueConfig.index = this.powerMeterConfigList.http_json[index].index;
valueConfig.json_path = this.powerMeterConfigList.http_json[index].json_path;
}
this.testHttpRequestAlert[index] = {
this.testHttpJsonRequestAlert[index] = {
message: "Sending HTTP request...",
type: "info",
show: true,
};
const formData = new FormData();
formData.append("data", JSON.stringify(phaseConfig));
formData.append("data", JSON.stringify(valueConfig));
fetch("/api/powermeter/testhttprequest", {
fetch("/api/powermeter/testhttpjsonrequest", {
method: "POST",
headers: authHeader(),
body: formData,
@ -369,7 +309,7 @@ export default defineComponent({
.then((response) => handleResponse(response, this.$emitter, this.$router))
.then(
(response) => {
this.testHttpRequestAlert[index] = {
this.testHttpJsonRequestAlert[index] = {
message: response.message,
type: response.type,
show: true,
@ -377,17 +317,17 @@ export default defineComponent({
}
)
},
testTibberRequest() {
this.testTibberRequestAlert = {
message: "Sending Tibber request...",
testHttpSmlRequest() {
this.testHttpSmlRequestAlert = {
message: "Sending HTTP SML request...",
type: "info",
show: true,
};
const formData = new FormData();
formData.append("data", JSON.stringify(this.powerMeterConfigList.tibber));
formData.append("data", JSON.stringify(this.powerMeterConfigList.http_sml));
fetch("/api/powermeter/testtibberrequest", {
fetch("/api/powermeter/testhttpsmlrequest", {
method: "POST",
headers: authHeader(),
body: formData,
@ -395,7 +335,7 @@ export default defineComponent({
.then((response) => handleResponse(response, this.$emitter, this.$router))
.then(
(response) => {
this.testTibberRequestAlert = {
this.testHttpSmlRequestAlert = {
message: response.message,
type: response.type,
show: true,