Skip to content

Commit

Permalink
Merge branch 'helgeerbe:development' into development
Browse files Browse the repository at this point in the history
  • Loading branch information
Snoopy-HSS authored Sep 4, 2024
2 parents 9d82dac + a87f9fa commit 36d917c
Show file tree
Hide file tree
Showing 11 changed files with 132 additions and 42 deletions.
2 changes: 1 addition & 1 deletion include/BatteryStats.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class BatteryStats {
uint32_t getAgeSeconds() const { return (millis() - _lastUpdate) / 1000; }
bool updateAvailable(uint32_t since) const;

uint8_t getSoC() const { return _soc; }
float getSoC() const { return _soc; }
uint32_t getSoCAgeSeconds() const { return (millis() - _lastUpdateSoC) / 1000; }
uint8_t getSoCPrecision() const { return _socPrecision; }

Expand Down
17 changes: 15 additions & 2 deletions include/HttpGetter.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,17 @@
#include <HTTPClient.h>
#include <WiFiClient.h>

using up_http_client_t = std::unique_ptr<HTTPClient>;
class HttpGetterClient : public HTTPClient {
public:
void restartTCP() {
// keeps the NetworkClient, and closes the TCP connections (as we
// effectively do not support keep-alive with HTTP 1.0).
HTTPClient::disconnect(true);
HTTPClient::connect();
}
};

using up_http_client_t = std::unique_ptr<HttpGetterClient>;
using sp_wifi_client_t = std::shared_ptr<WiFiClient>;

class HttpRequestResult {
Expand Down Expand Up @@ -59,7 +69,7 @@ class HttpGetter {
char const* getErrorText() const { return _errBuffer; }

private:
String getAuthDigest(String const& authReq, unsigned int counter);
std::pair<bool, String> getAuthDigest();
HttpRequestConfig const& _config;

template<typename... Args>
Expand All @@ -71,6 +81,9 @@ class HttpGetter {
String _uri;
uint16_t _port;

String _wwwAuthenticate = "";
unsigned _nonceCounter = 0;

sp_wifi_client_t _spWiFiClient; // reused for multiple HTTP requests

std::vector<std::pair<std::string, std::string>> _additionalHeaders;
Expand Down
2 changes: 1 addition & 1 deletion include/PowerMeterHttpJson.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class PowerMeterHttpJson : public PowerMeterProvider {
uint32_t _lastPoll = 0;

mutable std::mutex _valueMutex;
power_values_t _powerValues;
power_values_t _powerValues = {};

std::array<std::unique_ptr<HttpGetter>, POWERMETER_HTTP_JSON_MAX_VALUES> _httpGetters;

Expand Down
4 changes: 2 additions & 2 deletions include/defaults.h
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,8 @@
#define POWERLIMITER_VOLTAGE_LOAD_CORRECTION_FACTOR 0.001
#define POWERLIMITER_RESTART_HOUR -1
#define POWERLIMITER_FULL_SOLAR_PASSTHROUGH_SOC 100
#define POWERLIMITER_FULL_SOLAR_PASSTHROUGH_START_VOLTAGE 100.0
#define POWERLIMITER_FULL_SOLAR_PASSTHROUGH_STOP_VOLTAGE 100.0
#define POWERLIMITER_FULL_SOLAR_PASSTHROUGH_START_VOLTAGE 66.0
#define POWERLIMITER_FULL_SOLAR_PASSTHROUGH_STOP_VOLTAGE 66.0

#define BATTERY_ENABLED false
#define BATTERY_PROVIDER 0 // Pylontech CAN receiver
Expand Down
9 changes: 9 additions & 0 deletions src/BatteryCanReceiver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ bool BatteryCanReceiver::init(bool verboseLogging, char const* providerName)
auto rx = static_cast<gpio_num_t>(pin.battery_rx);
twai_general_config_t g_config = TWAI_GENERAL_CONFIG_DEFAULT(tx, rx, TWAI_MODE_NORMAL);

// interrupts at level 1 are in high demand, at least on ESP32-S3 boards,
// but only a limited amount can be allocated. failing to allocate an
// interrupt in the TWAI driver will cause a bootloop. we therefore
// register the TWAI driver's interrupt at level 2. level 2 interrupts
// should be available -- we don't really know. we would love to have the
// esp_intr_dump() function, but that's not available yet in our version
// of the underlying esp-idf.
g_config.intr_flags = ESP_INTR_FLAG_LEVEL2;

// Initialize configuration structures using macro initializers
twai_timing_config_t t_config = TWAI_TIMING_CONFIG_500KBITS();
twai_filter_config_t f_config = TWAI_FILTER_CONFIG_ACCEPT_ALL();
Expand Down
125 changes: 98 additions & 27 deletions src/HttpGetter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include "HttpGetter.h"
#include <WiFiClientSecure.h>
#include "mbedtls/sha256.h"
#include "mbedtls/md5.h"
#include <base64.h>
#include <ESPmDNS.h>

Expand Down Expand Up @@ -100,7 +101,7 @@ HttpRequestResult HttpGetter::performGetRequest()
}
}

auto upTmpHttpClient = std::make_unique<HTTPClient>();
auto upTmpHttpClient = std::make_unique<HttpGetterClient>();

// use HTTP1.0 to avoid problems with chunked transfer encoding when the
// stream is later used to read the server's response.
Expand Down Expand Up @@ -135,23 +136,60 @@ HttpRequestResult HttpGetter::performGetRequest()
break;
}
case Auth_t::Digest: {
const char *headers[1] = {"WWW-Authenticate"};
upTmpHttpClient->collectHeaders(headers, 1);
// send "Connection: keep-alive" (despite using HTTP/1.0, where
// "Connection: close" is the default) so there is a chance to
// reuse the TCP connection when performing the second GET request.
upTmpHttpClient->setReuse(true);

const char *headers[2] = {"WWW-Authenticate", "Connection"};
upTmpHttpClient->collectHeaders(headers, 2);

// try with new auth response based on previous WWW-Authenticate
// header, which allows us to retrieve the resource without a
// second GET request. if the server decides that we reused the
// previous challenge too often, it will respond with HTTP401 and
// a new challenge, which we handle as if we had no challenge yet.
auto authorization = getAuthDigest();
if (authorization.first) {
upTmpHttpClient->addHeader("Authorization", authorization.second);
}
break;
}
}

int httpCode = upTmpHttpClient->GET();

if (httpCode == HTTP_CODE_UNAUTHORIZED && _config.AuthType == Auth_t::Digest) {
_wwwAuthenticate = "";

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);

_wwwAuthenticate = upTmpHttpClient->header("WWW-Authenticate");

// using a new WWW-Authenticate challenge means
// we never used the server's nonce in a response
_nonceCounter = 0;

auto authorization = getAuthDigest();
if (!authorization.first) {
logError("Digest Error: %s", authorization.second.c_str());
return { false };
}
upTmpHttpClient->addHeader("Authorization", authorization.second);

// use a new TCP connection if the server sent "Connection: close".
bool restart = true;
if (upTmpHttpClient->hasHeader("Connection")) {
String connection = upTmpHttpClient->header("Connection");
connection.toLowerCase();
restart = connection.indexOf("keep-alive") == -1;
}
if (restart) { upTmpHttpClient->restartTCP(); }

httpCode = upTmpHttpClient->GET();
}

Expand All @@ -168,6 +206,29 @@ HttpRequestResult HttpGetter::performGetRequest()
return { true, std::move(upTmpHttpClient), _spWiFiClient };
}

template<size_t binLen>
static String bin2hex(uint8_t* hash) {
size_t constexpr kOutLen = binLen * 2 + 1;
char res[kOutLen];
for (int i = 0; i < binLen; i++) {
snprintf(res + (i*2), sizeof(res) - (i*2), "%02x", hash[i]);
}
return res;
}

static String md5(const String& data) {
uint8_t hash[16];

mbedtls_md5_context ctx;
mbedtls_md5_init(&ctx);
mbedtls_md5_starts_ret(&ctx);
mbedtls_md5_update_ret(&ctx, reinterpret_cast<const unsigned char*>(data.c_str()), data.length());
mbedtls_md5_finish_ret(&ctx, hash);
mbedtls_md5_free(&ctx);

return bin2hex<sizeof(hash)>(hash);
}

static String sha256(const String& data) {
uint8_t hash[32];

Expand All @@ -178,12 +239,7 @@ static String sha256(const String& data) {
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;
return bin2hex<sizeof(hash)>(hash);
}

static String extractParam(String const& authReq, String const& param, char delimiter) {
Expand All @@ -204,30 +260,45 @@ static String getcNonce(int len) {
return s;
}

String HttpGetter::getAuthDigest(String const& authReq, unsigned int counter) {
static std::pair<bool, String> getAlgo(String const& authReq) {
// the algorithm is NOT enclosed in double quotes, so we can't use extractParam
auto paramBegin = authReq.indexOf("algorithm=");
if (paramBegin == -1) { return { true, "MD5" }; } // default as per RFC2617
auto valueBegin = paramBegin + 10;

String algo = authReq.substring(valueBegin, valueBegin + 3);
if (algo == "MD5") { return { true, algo }; }

algo = authReq.substring(valueBegin, valueBegin + 7);
if (algo == "SHA-256") { return { true, algo }; }

return { false, "unsupported digest algorithm" };
}

std::pair<bool, String> HttpGetter::getAuthDigest() {
if (_wwwAuthenticate.isEmpty()) { return { false, "no digest challenge yet" }; }

// extracting required parameters for RFC 2617 Digest
String realm = extractParam(authReq, "realm=\"", '"');
String nonce = extractParam(authReq, "nonce=\"", '"');
String cNonce = getcNonce(8);
String realm = extractParam(_wwwAuthenticate, "realm=\"", '"');
String nonce = extractParam(_wwwAuthenticate, "nonce=\"", '"');
String cNonce = getcNonce(8); // client nonce

char nc[9];
snprintf(nc, sizeof(nc), "%08x", counter);

// sha256 of the user:realm:password
String ha1 = sha256(String(_config.Username) + ":" + realm + ":" + _config.Password);
snprintf(nc, sizeof(nc), "%08x", ++_nonceCounter);

// sha256 of method:uri
String ha2 = sha256("GET:" + _uri);
auto algo = getAlgo(_wwwAuthenticate);
if (!algo.first) { return { false, algo.second }; }

// sha256 of h1:nonce:nc:cNonce:auth:h2
String response = sha256(ha1 + ":" + nonce + ":" + String(nc) +
auto hash = (algo.second == "SHA-256") ? &sha256 : &md5;
String ha1 = hash(String(_config.Username) + ":" + realm + ":" + _config.Password);
String ha2 = hash("GET:" + _uri);
String response = hash(ha1 + ":" + nonce + ":" + String(nc) +
":" + cNonce + ":" + "auth" + ":" + ha2);

// Final authorization String
return String("Digest username=\"") + _config.Username +
return { true, String("Digest username=\"") + _config.Username +
"\", realm=\"" + realm + "\", nonce=\"" + nonce + "\", uri=\"" +
_uri + "\", cnonce=\"" + cNonce + "\", nc=" + nc +
", qop=auth, response=\"" + response + "\", algorithm=SHA-256";
", qop=auth, response=\"" + response + "\", algorithm=" + algo.second };
}

void HttpGetter::addHeader(char const* key, char const* value)
Expand Down
3 changes: 2 additions & 1 deletion src/Huawei_can.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,8 @@ void HuaweiCanClass::updateSettings(uint8_t huawei_miso, uint8_t huawei_mosi, ui
_mode = HUAWEI_MODE_AUTO_INT;
}

xTaskCreate(HuaweiCanCommunicationTask,"HUAWEI_CAN_0",2000,NULL,0,&_HuaweiCanCommunicationTaskHdl);
xTaskCreate(HuaweiCanCommunicationTask, "HUAWEI_CAN_0", 2048/*stack size*/,
NULL/*params*/, 0/*prio*/, &_HuaweiCanCommunicationTaskHdl);

MessageOutput.println("[HuaweiCanClass::init] MCP2515 Initialized Successfully!");
_initialized = true;
Expand Down
2 changes: 1 addition & 1 deletion src/PowerLimiter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ void PowerLimiterClass::loop()
_batteryDischargeEnabled = getBatteryPower();

if (_verboseLogging && !config.PowerLimiter.IsInverterSolarPowered) {
MessageOutput.printf("[DPL::loop] battery interface %s, SoC: %d %%, StartTH: %d %%, StopTH: %d %%, SoC age: %d s, ignore: %s\r\n",
MessageOutput.printf("[DPL::loop] battery interface %s, SoC: %f %%, StartTH: %d %%, StopTH: %d %%, SoC age: %d s, ignore: %s\r\n",
(config.Battery.Enabled?"enabled":"disabled"),
Battery.getStats()->getSoC(),
config.PowerLimiter.BatterySocStartThreshold,
Expand Down
2 changes: 1 addition & 1 deletion src/PylontechCanReceiver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ void PylontechCanReceiver::onMessage(twai_message_t rx_message)
_stats->_stateOfHealth = this->readUnsignedInt16(rx_message.data + 2);

if (_verboseLogging) {
MessageOutput.printf("[Pylontech] soc: %d soh: %d\r\n",
MessageOutput.printf("[Pylontech] soc: %f soh: %d\r\n",
_stats->getSoC(), _stats->_stateOfHealth);
}
break;
Expand Down
2 changes: 1 addition & 1 deletion src/PytesCanReceiver.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ void PytesCanReceiver::onMessage(twai_message_t rx_message)
_stats->_stateOfHealth = this->readUnsignedInt16(rx_message.data + 2);

if (_verboseLogging) {
MessageOutput.printf("[Pytes] soc: %d soh: %d\r\n",
MessageOutput.printf("[Pytes] soc: %f soh: %d\r\n",
_stats->getSoC(), _stats->_stateOfHealth);
}
break;
Expand Down
6 changes: 1 addition & 5 deletions webapp/src/views/PowerLimiterAdminView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -501,11 +501,7 @@ export default defineComponent({
canUseSolarPassthrough() {
const cfg = this.powerLimiterConfigList;
const meta = this.powerLimiterMetaData;
const canUse = this.isEnabled() && meta.charge_controller_enabled && !cfg.is_inverter_solar_powered;
if (!canUse) {
cfg.solar_passthrough_enabled = false;
}
return canUse;
return this.isEnabled() && meta.charge_controller_enabled && !cfg.is_inverter_solar_powered;
},
canUseSoCThresholds() {
const cfg = this.powerLimiterConfigList;
Expand Down

0 comments on commit 36d917c

Please sign in to comment.