* Optimize Sun data calculation * Remove not required enum * Split config struct into different sub structs * Feature: Allow configuration of LWT QoS * Made resetreason methods static * Feature: Implement offset cache for "YieldDay" Thanks to @broth-itk for the idea! Fix: #1258 #1397 * Add Esp32-Stick-PoE-A * remove broken LilyGO_T_ETH_POE config, use device profile instead * Feature: High resolution Icon and PWA (Progressive Web App) functionality Fix: #1289 * webapp: Update dependencies * Initialize TaskScheduler * Migrate SunPosition to TaskScheduler * Migrate Datastore to TaskScheduler * Migrate MqttHandleInverterTotal to TaskSchedule * Migrate MqttHandleHass to TaskScheduler * Migrate MqttHandleDtu to TaskScheduler * Migrate MqttHandleInverter to TaskScheduler * Migrate LedSingle to TaskScheduler * Migrate NetworkSettings to TaskScheduler * Migrate InverterSettings to TaskScheduler * Migrate MessageOutput to TaskScheduler * Migrate Display_Graphic to TaskScheduler * Migrate WebApi to TaskScheduler * Split InverterSettings into multiple tasks * Calculate SunPosition only every 5 seconds * Split LedSingle into multiple tasks * Upgrade espMqttClient from 1.4.5 to 1.5.0 * Doc: Correct amount of MPP-Tracker * Added HMT-1600-4T and HMT-1800-4T to DevInfoParser Fix #1524 * Adjusted inverter names for HMS-1600/1800/2000-4T * Add channel count to description of detected inverter type (DevInfoParser) * Adjust device web api endpoint for dynamic led count * Feature: Added ability to change the brightness of the LEDs Based on the idea of @moritzlerch with several modifications like pwmTable and structure * webapp: Update dependencies * Update olikraus/U8g2 from 2.35.7 to 2.35.8 * Remove not required onWebsocketEvent * Remove code nesting * Introduce several const statements * Remove not required AsyncEventSource * Doc: Added byte specification to each command * Feature: Added basic Grid Profile parser which shows the used profile and version Other values are still outstanding. * Optimize AlarmLogParser to save memory * Add libfrozen to project to create constexpr maps * Feature: First version of GridProfile Parser which shows all values contained in the profile. * webapp: Update dependencies * Apply better variable names * Remove not required casts * Add additional compiler flags to prevent errors * Add const statement to several variables * Replace NULL by nullptr * Update bblanchon/ArduinoJson from 6.21.3 to 6.21.4 * Add const keyword to method parameters * Add const keyword to methods * Use references instead of pointers whenver possible * Adjust member variable names in MqttSettings * Adjust member variable names in NetworkSettings * webapp: Update timezone database to latest version * webapp: Beautify and unify form footers * Feature: Allow setting of an inverter limit of 0% and 0W Thanks to @madmartin in #1270 * Feature: Allow links in device profiles These links will be shown on the hardware settings page. * Doc: Added hint regarding HMS-xxxx-xT-NA inverters * Feature: Added DeviceProfile for CASmo-DTU Based on #1565 * Upgrade actions/upload-artifact from v3 to v4 * Upgrade actions/download-artifact from v3 to v4 * webapp: add app.js.gz * Gridprofileparser: Added latest known values Thanks to @stefan123t and @noone2k * webapp: Fix lint errors * Feature: Add DTU to Home Assistant Auto Discovery This is based on PR 1365 from @CFenner with several fixes and optimizations * Fix: Remove debug output as it floods the console * Fix: Gridprofileparser: Add additional error handling if profile is unknown * webapp: add app.js.gz * Fix: Offset cache for "YieldDay" did not work correctly * webapp: update dependencies * webapp: add app.js.gz * Fix: yarn.lock was outdated * Fix: yarn build error * Fix: Reset Yield day correction in combination with Zero Yield Day on Midnight lead to wrong values. * Fix: Allow negative values in GridProfileParser * Correct variable name * Fix #1579: Static IP in Ethernet mode did not work correctly * Feature: Added diagram to display This is based on the idea of @Henrik-Ingenieur and was discussed in #1504 * webapp: update dependencies * webapp: add app.js.gz --------- Co-authored-by: Thomas Basler <thomas@familie-basler.net> Co-authored-by: Pierre Kancir <pierre.kancir.emn@gmail.com>
265 lines
9.2 KiB
C++
265 lines
9.2 KiB
C++
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
#include "Configuration.h"
|
|
#include "HttpPowerMeter.h"
|
|
#include "MessageOutput.h"
|
|
#include <WiFiClientSecure.h>
|
|
#include <FirebaseJson.h>
|
|
#include <Crypto.h>
|
|
#include <SHA256.h>
|
|
#include <base64.h>
|
|
#include <memory>
|
|
|
|
void HttpPowerMeterClass::init()
|
|
{
|
|
}
|
|
|
|
float HttpPowerMeterClass::getPower(int8_t phase)
|
|
{
|
|
return power[phase - 1];
|
|
}
|
|
|
|
bool HttpPowerMeterClass::updateValues()
|
|
{
|
|
const CONFIG_T& config = Configuration.get();
|
|
|
|
char response[2000],
|
|
errorMessage[256];
|
|
|
|
for (uint8_t i = 0; i < POWERMETER_MAX_PHASES; i++) {
|
|
POWERMETER_HTTP_PHASE_CONFIG_T phaseConfig = config.PowerMeter.Http_Phase[i];
|
|
|
|
if (!phaseConfig.Enabled) {
|
|
power[i] = 0.0;
|
|
continue;
|
|
}
|
|
|
|
if (i == 0 || config.PowerMeter.HttpIndividualRequests) {
|
|
if (httpRequest(phaseConfig.Url, phaseConfig.AuthType, phaseConfig.Username, phaseConfig.Password, phaseConfig.HeaderKey, phaseConfig.HeaderValue, phaseConfig.Timeout,
|
|
response, sizeof(response), errorMessage, sizeof(errorMessage))) {
|
|
if (!getFloatValueByJsonPath(response, phaseConfig.JsonPath, power[i])) {
|
|
MessageOutput.printf("[HttpPowerMeter] Couldn't find a value with Json query \"%s\"\r\n", phaseConfig.JsonPath);
|
|
return false;
|
|
}
|
|
} else {
|
|
MessageOutput.printf("[HttpPowerMeter] Getting the power of phase %d failed. Error: %s\r\n",
|
|
i + 1, errorMessage);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool HttpPowerMeterClass::httpRequest(const char* url, Auth authType, const char* username, const char* password, const char* httpHeader, const char* httpValue, uint32_t timeout,
|
|
char* response, size_t responseSize, char* error, size_t errorSize)
|
|
{
|
|
String urlProtocol;
|
|
String urlHostname;
|
|
String urlUri;
|
|
extractUrlComponents(url, urlProtocol, urlHostname, urlUri);
|
|
|
|
response[0] = '\0';
|
|
error[0] = '\0';
|
|
|
|
// secureWifiClient MUST be created before HTTPClient
|
|
// see discussion: https://github.com/helgeerbe/OpenDTU-OnBattery/issues/381
|
|
std::unique_ptr<WiFiClient> wifiClient;
|
|
|
|
if (urlProtocol == "https") {
|
|
auto secureWifiClient = std::make_unique<WiFiClientSecure>();
|
|
secureWifiClient->setInsecure();
|
|
wifiClient = std::move(secureWifiClient);
|
|
} else {
|
|
wifiClient = std::make_unique<WiFiClient>();
|
|
}
|
|
|
|
|
|
if (!httpClient.begin(*wifiClient, url)) {
|
|
snprintf_P(error, errorSize, "httpClient.begin(%s) failed", url);
|
|
return false;
|
|
}
|
|
prepareRequest(timeout, httpHeader, httpValue);
|
|
|
|
if (authType == Auth::digest) {
|
|
const char *headers[1] = {"WWW-Authenticate"};
|
|
httpClient.collectHeaders(headers, 1);
|
|
} else if (authType == Auth::basic) {
|
|
String authString = username;
|
|
authString += ":";
|
|
authString += password;
|
|
String auth = "Basic ";
|
|
auth.concat(base64::encode(authString));
|
|
httpClient.addHeader("Authorization", auth);
|
|
}
|
|
|
|
int httpCode = httpClient.GET();
|
|
if (httpCode == HTTP_CODE_UNAUTHORIZED && authType == Auth::digest) {
|
|
// Handle authentication challenge
|
|
char realm[256]; // Buffer to store the realm received from the server
|
|
char nonce[256]; // Buffer to store the nonce received from the server
|
|
if (httpClient.hasHeader("WWW-Authenticate")) {
|
|
String authHeader = httpClient.header("WWW-Authenticate");
|
|
if (authHeader.indexOf("Digest") != -1) {
|
|
int realmIndex = authHeader.indexOf("realm=\"");
|
|
int nonceIndex = authHeader.indexOf("nonce=\"");
|
|
if (realmIndex != -1 && nonceIndex != -1) {
|
|
int realmEndIndex = authHeader.indexOf("\"", realmIndex + 7);
|
|
int nonceEndIndex = authHeader.indexOf("\"", nonceIndex + 7);
|
|
if (realmEndIndex != -1 && nonceEndIndex != -1) {
|
|
authHeader.substring(realmIndex + 7, realmEndIndex).toCharArray(realm, sizeof(realm));
|
|
authHeader.substring(nonceIndex + 7, nonceEndIndex).toCharArray(nonce, sizeof(nonce));
|
|
}
|
|
}
|
|
String cnonce = String(random(1000)); // Generate client nonce
|
|
String str = username;
|
|
str += ":";
|
|
str += realm;
|
|
str += ":";
|
|
str += password;
|
|
String ha1 = sha256(str);
|
|
str = "GET:";
|
|
str += urlUri;
|
|
String ha2 = sha256(str);
|
|
str = ha1;
|
|
str += ":";
|
|
str += nonce;
|
|
str += ":00000001:";
|
|
str += cnonce;
|
|
str += ":auth:";
|
|
str += ha2;
|
|
String response = sha256(str);
|
|
|
|
String authorization = "Digest username=\"";
|
|
authorization += username;
|
|
authorization += "\", realm=\"";
|
|
authorization += realm;
|
|
authorization += "\", nonce=\"";
|
|
authorization += nonce;
|
|
authorization += "\", uri=\"";
|
|
authorization += urlUri;
|
|
authorization += "\", cnonce=\"";
|
|
authorization += cnonce;
|
|
authorization += "\", nc=00000001, qop=auth, response=\"";
|
|
authorization += response;
|
|
authorization += "\", algorithm=SHA-256";
|
|
httpClient.end();
|
|
if (!httpClient.begin(*wifiClient, url)) {
|
|
snprintf_P(error, errorSize, "httpClient.begin(%s) for digest auth failed", url);
|
|
return false;
|
|
}
|
|
prepareRequest(timeout, httpHeader, httpValue);
|
|
httpClient.addHeader("Authorization", authorization);
|
|
httpCode = httpClient.GET();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (httpCode == HTTP_CODE_OK) {
|
|
String responseBody = httpClient.getString();
|
|
|
|
if (responseBody.length() > (responseSize - 1)) {
|
|
snprintf_P(error, errorSize, "Response too large! Response length: %d Body start: %s",
|
|
httpClient.getSize(), responseBody.c_str());
|
|
} else {
|
|
snprintf(response, responseSize, responseBody.c_str());
|
|
}
|
|
} else if (httpCode <= 0) {
|
|
snprintf_P(error, errorSize, "Error(%s): %s", url, httpClient.errorToString(httpCode).c_str());
|
|
} else if (httpCode != HTTP_CODE_OK) {
|
|
snprintf_P(error, errorSize, "Bad HTTP code: %d", httpCode);
|
|
}
|
|
|
|
httpClient.end();
|
|
|
|
if (error[0] != '\0') {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
float HttpPowerMeterClass::getFloatValueByJsonPath(const char* jsonString, const char* jsonPath, float& value)
|
|
{
|
|
FirebaseJson firebaseJson;
|
|
firebaseJson.setJsonData(jsonString);
|
|
|
|
FirebaseJsonData firebaseJsonResult;
|
|
if (!firebaseJson.get(firebaseJsonResult, jsonPath)) {
|
|
return false;
|
|
}
|
|
|
|
value = firebaseJsonResult.to<float>();
|
|
|
|
firebaseJson.clear();
|
|
|
|
return true;
|
|
}
|
|
|
|
void HttpPowerMeterClass::extractUrlComponents(const String& url, String& protocol, String& hostname, String& uri) {
|
|
// Find protocol delimiter
|
|
int protocolEndIndex = url.indexOf(":");
|
|
if (protocolEndIndex != -1) {
|
|
protocol = url.substring(0, protocolEndIndex);
|
|
|
|
// Find double slash delimiter
|
|
int doubleSlashIndex = url.indexOf("//", protocolEndIndex);
|
|
if (doubleSlashIndex != -1) {
|
|
// Find slash after double slash delimiter
|
|
int slashIndex = url.indexOf("/", doubleSlashIndex + 2);
|
|
if (slashIndex != -1) {
|
|
// Extract hostname and uri
|
|
hostname = url.substring(doubleSlashIndex + 2, slashIndex);
|
|
uri = url.substring(slashIndex);
|
|
} else {
|
|
// No slash after double slash delimiter, so the whole remaining part is the hostname
|
|
hostname = url.substring(doubleSlashIndex + 2);
|
|
uri = "/";
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove username:password if present in the hostname
|
|
int atIndex = hostname.indexOf("@");
|
|
if (atIndex != -1) {
|
|
hostname = hostname.substring(atIndex + 1);
|
|
}
|
|
}
|
|
|
|
#define HASH_SIZE 32
|
|
|
|
String HttpPowerMeterClass::sha256(const String& data) {
|
|
SHA256 sha256;
|
|
uint8_t hash[HASH_SIZE];
|
|
|
|
sha256.reset();
|
|
sha256.update(data.c_str(), data.length());
|
|
sha256.finalize(hash, HASH_SIZE);
|
|
|
|
String hashStr = "";
|
|
for (int i = 0; i < HASH_SIZE; i++) {
|
|
String hex = String(hash[i], HEX);
|
|
if (hex.length() == 1) {
|
|
hashStr += "0";
|
|
}
|
|
hashStr += hex;
|
|
}
|
|
|
|
return hashStr;
|
|
}
|
|
|
|
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;
|