Merge remote-tracking branch 'emsesp/core3' into core3

This commit is contained in:
MichaelDvP
2026-05-29 07:08:12 +02:00
43 changed files with 1115 additions and 1224 deletions

View File

@@ -41,7 +41,6 @@ void MqttSettingsService::startClient() {
delete _mqttClient;
_mqttClient = nullptr;
}
#ifndef NO_TLS_SUPPORT
if (_state.enableTLS) {
isSecure = true;
if (emsesp::EMSESP::system_.PSram() == 0) {
@@ -62,7 +61,6 @@ void MqttSettingsService::startClient() {
});
return;
}
#endif
isSecure = false;
if (emsesp::EMSESP::system_.PSram() == 0) {
_mqttClient = new espMqttClient(espMqttClientTypes::UseInternalTask::NO);
@@ -164,7 +162,6 @@ bool MqttSettingsService::configureMqtt() {
}
_reconfigureMqtt = false;
#ifndef NO_TLS_SUPPORT
if (_state.enableTLS) {
if (_state.rootCA == "insecure") {
#if defined(EMSESP_DEBUG)
@@ -189,7 +186,6 @@ bool MqttSettingsService::configureMqtt() {
static_cast<espMqttClientSecure *>(_mqttClient)->setWill(will_topic, 1, true, "offline"); // QOS 1, retain
return _mqttClient->connect();
}
#endif
static_cast<espMqttClient *>(_mqttClient)->setServer(_state.host.c_str(), _state.port);
if (_state.username.length() > 0) {
static_cast<espMqttClient *>(_mqttClient)->setCredentials(_state.username.c_str(), _state.password.length() > 0 ? _state.password.c_str() : nullptr);
@@ -205,10 +201,8 @@ bool MqttSettingsService::configureMqtt() {
}
void MqttSettings::read(MqttSettings & settings, JsonObject root) {
#ifndef NO_TLS_SUPPORT
root["enableTLS"] = settings.enableTLS;
root["rootCA"] = settings.rootCA;
#endif
root["enableTLS"] = settings.enableTLS;
root["rootCA"] = settings.rootCA;
root["enabled"] = settings.enabled;
root["host"] = settings.host;
root["port"] = settings.port;
@@ -244,12 +238,8 @@ StateUpdateResult MqttSettings::update(JsonObject root, MqttSettings & settings)
MqttSettings newSettings;
bool changed = false;
#ifndef NO_TLS_SUPPORT
newSettings.enableTLS = root["enableTLS"];
newSettings.rootCA = root["rootCA"] | "";
#else
newSettings.enableTLS = false;
#endif
newSettings.enableTLS = root["enableTLS"];
newSettings.rootCA = root["rootCA"] | "";
newSettings.enabled = root["enabled"] | FACTORY_MQTT_ENABLED;
newSettings.host = root["host"] | FACTORY_MQTT_HOST;
newSettings.port = static_cast<uint16_t>(root["port"] | FACTORY_MQTT_PORT);
@@ -375,7 +365,6 @@ StateUpdateResult MqttSettings::update(JsonObject root, MqttSettings & settings)
emsesp::EMSESP::mqtt_.set_publish_time_heartbeat(newSettings.publish_time_heartbeat);
}
#ifndef NO_TLS_SUPPORT
// strip down to certificate only
newSettings.rootCA.replace("\r", "");
newSettings.rootCA.replace("\n", "");
@@ -388,7 +377,6 @@ StateUpdateResult MqttSettings::update(JsonObject root, MqttSettings & settings)
if (newSettings.enableTLS != settings.enableTLS || newSettings.rootCA != settings.rootCA) {
changed = true;
}
#endif
// save the new settings
settings = newSettings;

View File

@@ -2,10 +2,6 @@
#include <emsesp.h>
#ifdef NO_TLS_SUPPORT
#include "lwip/dns.h"
#endif
NetworkStatus::NetworkStatus(AsyncWebServer * server, SecurityManager * securityManager) {
securityManager->addEndpoint(server, NETWORK_STATUS_SERVICE_PATH, AuthenticationPredicates::IS_AUTHENTICATED, [this](AsyncWebServerRequest * request) {
networkStatus(request);

View File

@@ -110,10 +110,12 @@ DeviceValue::DeviceValue(uint8_t device_type,
const char * DeviceValue::DeviceValueUOM_s[] = {
F_(uom_blank), // 0
F_(uom_degrees), F_(uom_degrees), F_(uom_percent), F_(uom_lmin), F_(uom_kwh), F_(uom_wh), FL_(hours)[0], FL_(minutes)[0],
F_(uom_ua), F_(uom_bar), F_(uom_kw), F_(uom_w), F_(uom_kb), FL_(seconds)[0], F_(uom_dbm), F_(uom_fahrenheit),
F_(uom_mv), F_(uom_sqm), F_(uom_m3), F_(uom_l), F_(uom_kmin), F_(uom_k), F_(uom_volts), F_(uom_mbar),
F_(uom_lh), F_(uom_ctkwh), F_(uom_hz), F_(uom_blank)
F_(uom_degrees), F_(uom_degrees), F_(uom_percent), F_(uom_lmin), F_(uom_kwh), F_(uom_wh), FL_(hours)[0], FL_(minutes)[0], F_(uom_ua),
F_(uom_bar), F_(uom_kw), F_(uom_w), F_(uom_kb), FL_(seconds)[0], F_(uom_dbm), F_(uom_fahrenheit), F_(uom_mv), F_(uom_sqm),
F_(uom_m3), F_(uom_l), F_(uom_kmin), F_(uom_k), F_(uom_volts), F_(uom_mbar), F_(uom_lh), F_(uom_ctkwh), F_(uom_hz),
F_(uom_blank), // connectivity
F_(uom_blank), // timestamp
F_(uom_blank) // blank
};

View File

@@ -77,7 +77,8 @@ class DeviceValue {
LH, // 25 - l/h - volume flow rate
CTKWH, // 26 - ct/kWh - monetary
HERTZ, // 27 - Hz - frequency
CONNECTIVITY // 28 - used in HA - connectivity
CONNECTIVITY, // 28 - used in HA - connectivity
TIMESTAMP, // 29 - used in HA - timestamp
};
// TAG mapping - maps to DeviceValueTAG_s in emsdevicevalue.cpp

View File

@@ -1757,6 +1757,12 @@ void EMSESP::start() {
// start network services. This will initialise WiFi or Ethernet depending on the settings.
network_.begin();
// start the core web services, as this loads the settings from the filesystem
// this will also handle any MQTT subscriptions
webCustomizationService.begin(); // load the customizations
webSchedulerService.begin(); // load the scheduler events
webCustomEntityService.begin(); // load the custom telegram reads
// perform any system upgrades
if (!factory_settings) {
if (system_.check_upgrade()) {
@@ -1771,10 +1777,6 @@ void EMSESP::start() {
};
LOG_INFO("Library loaded: %d EMS devices, %d device entities, %s", device_library_.size(), EMSESP_TRANSLATION_COUNT, system_.languages_string().c_str());
webCustomizationService.begin(); // load the customizations
webSchedulerService.begin(); // load the scheduler events
webCustomEntityService.begin(); // load the custom telegram reads
// start telnet service if it's enabled
// default idle is 10 minutes, default write timeout is 0 (automatic)
// note, this must be started after the network/wifi for ESP32 otherwise it'll crash

View File

@@ -272,6 +272,8 @@ MAKE_WORD_CUSTOM(uom_l, "l")
MAKE_WORD_CUSTOM(uom_kmin, "K*min")
MAKE_WORD_CUSTOM(uom_k, "K")
MAKE_WORD_CUSTOM(uom_volts, "V")
MAKE_WORD_CUSTOM(uom_connectivity, "connectivity")
MAKE_WORD_CUSTOM(uom_timestamp, "timestamp")
MAKE_WORD_CUSTOM(uom_mbar, "mbar")
MAKE_WORD_CUSTOM(uom_lh, "l/h")
MAKE_WORD_CUSTOM(uom_ctkwh, "ct/kWh")

View File

@@ -567,8 +567,6 @@ void Mqtt::ha_status() {
#endif
publish_system_ha_sensor_config(DeviceValueType::STRING, "EMS Bus", "bus_status", DeviceValueUOM::NONE);
publish_system_ha_sensor_config(DeviceValueType::STRING, "Uptime", "uptime", DeviceValueUOM::NONE);
publish_system_ha_sensor_config(DeviceValueType::INT8, "Uptime (sec)", "uptime_sec", DeviceValueUOM::SECONDS);
publish_system_ha_sensor_config(DeviceValueType::INT8, "Free memory", "freemem", DeviceValueUOM::KB);
publish_system_ha_sensor_config(DeviceValueType::INT8, "Max alloc", "max_alloc", DeviceValueUOM::KB);
publish_system_ha_sensor_config(DeviceValueType::INT8, "MQTT fails", "mqttfails", DeviceValueUOM::NONE);
@@ -585,8 +583,10 @@ void Mqtt::ha_status() {
if (!EMSESP::network_.ethernet_connected()) {
publish_system_ha_sensor_config(DeviceValueType::INT16, "WiFi reconnects", "wifireconnects", DeviceValueUOM::NONE);
}
// This one comes from the info MQTT topic - and handled in the publish_ha_sensor_config function
// These come from the info MQTT topic - and handled in the publish_ha_sensor_config function
publish_system_ha_sensor_config(DeviceValueType::STRING, "Version", "version", DeviceValueUOM::NONE);
publish_system_ha_sensor_config(DeviceValueType::STRING, "Boot time", "bootTime", DeviceValueUOM::TIMESTAMP);
}
// add sub or pub task to the queue.
@@ -1061,15 +1061,19 @@ bool Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdev
// add state_topic and it's value_template. This is not needed for commands, only sensors
if (type != DeviceValueType::CMD || is_sensor) {
// state topic, except for commands
char stat_t[MQTT_TOPIC_MAX_SIZE];
// This is where we determine which MQTT topic to pull the data from
// There is one exception for DeviceType::SYSTEM, which uses the heartbeat topic, and when fetching the version we want to take this from the info topic instead
if ((device_type == EMSdevice::DeviceType::SYSTEM) && (strncmp(entity, "version", 7) == 0)) {
snprintf(stat_t, sizeof(stat_t), "~/%s", F_(info));
} else {
snprintf(stat_t, sizeof(stat_t), "~/%s", tag_to_topic(device_type, tag).c_str());
char stat_t[MQTT_TOPIC_MAX_SIZE]; // state topic, except for commands
snprintf(stat_t, sizeof(stat_t), "~/%s", tag_to_topic(device_type, tag).c_str());
// Override - there are exceptions for DeviceType::SYSTEM, which uses the heartbeat topic
// and when fetching the version and bootTime we want to take this from the info topic instead
if (device_type == EMSdevice::DeviceType::SYSTEM) {
// handle the exceptions
if (strncmp(entity, "version", 7) == 0) {
snprintf(stat_t, sizeof(stat_t), "~/%s", F_(info));
} else if (strncmp(entity, "bootTime", 8) == 0) {
snprintf(stat_t, sizeof(stat_t), "~/%s", F_(info));
}
}
doc["stat_t"] = stat_t;
@@ -1095,7 +1099,14 @@ bool Mqtt::publish_ha_sensor_config(uint8_t type, // EMSdev
// don't bother with value template conditions if using Domoticz which doesn't fully support MQTT Discovery
if (discovery_type() == discoveryType::HOMEASSISTANT) {
doc["val_tpl"] = (std::string) "{{" + val_obj + " if " + val_cond + " else " + sample_val + "}}";
if (uom == DeviceValueUOM::TIMESTAMP) {
// special case for timestamp, using "value_template": "{{ (value_json.bootTime | as_datetime).isoformat() }}",
char val_tpl[100];
snprintf(val_tpl, sizeof(val_tpl), "{{ (value_json.%s | as_datetime).isoformat() }}", entity);
doc["val_tpl"] = val_tpl;
} else {
doc["val_tpl"] = (std::string) "{{" + val_obj + " if " + val_cond + " else " + sample_val + "}}";
}
add_ha_avty_section(doc.as<JsonObject>(), stat_t, val_cond); // adds availability section
} else {
// Domoticz doesn't support value templates, so we just use the value directly
@@ -1166,8 +1177,13 @@ void Mqtt::add_ha_classes(JsonObject doc, const uint8_t device_type, const uint8
doc[uom_ha] = "L/h";
} else if (uom == DeviceValueUOM::L) {
doc[uom_ha] = "L";
} else if (uom == DeviceValueUOM::TIMESTAMP) {
// do nothing
} else if (uom != DeviceValueUOM::NONE) {
doc[uom_ha] = EMSdevice::uom_to_string(uom); // use default
auto uom_str = EMSdevice::uom_to_string(uom);
if (strlen(uom_str)) {
doc[uom_ha] = uom_str;
}
} else if (discovery_type() != discoveryType::HOMEASSISTANT) {
doc[uom_ha] = " "; // Domoticz uses " " for a no-uom
}
@@ -1259,6 +1275,10 @@ void Mqtt::add_ha_classes(JsonObject doc, const uint8_t device_type, const uint8
doc[sc_ha] = sc_ha_measurement;
doc[dc_ha] = "connectivity";
break;
case DeviceValueUOM::TIMESTAMP:
doc[sc_ha] = sc_ha_measurement;
doc[dc_ha] = "timestamp";
break;
case DeviceValueUOM::MV:
case DeviceValueUOM::VOLTS:
doc[sc_ha] = sc_ha_measurement;

View File

@@ -22,6 +22,9 @@
#include "shuntingYard.h"
#include <WiFiClient.h>
#include <ESP_SSLClient.h>
namespace emsesp {
// find tokens - optimized to reduce string allocations
@@ -683,6 +686,94 @@ std::string calculate(const std::string & expr) {
return result;
}
// perform an HTTP/HTTPS request; returns the HTTP status code (0 on failure or unsupported scheme)
// the response headers are always stripped, so `result` contains only the body
int http_request(std::string url, const std::string & method, const std::string & value, JsonObjectConst headers, std::string & result) {
int httpResult = 0;
const bool is_post = value.length() || Helpers::toLower(method) == "post";
const auto lower_url = Helpers::toLower(url.c_str());
const bool is_https = lower_url.starts_with("https://");
if (!is_https && !lower_url.starts_with("http://")) {
return 0; // unsupported scheme
}
WiFiClient * basic_client = new WiFiClient;
ESP_SSLClient * ssl_client = new ESP_SSLClient;
if (is_https) {
ssl_client->setInsecure(); // with root CA we should set here: ssl_client->setCACert(rootCACert);
// NOTE: 1 KB RX buffer is fine for small JSON-style endpoints used by the scheduler/shunting-yard,
// but it is NOT enough for servers that send full-size TLS records (>1 KB), e.g. GitHub release
// assets / large CDN responses. Such servers do not negotiate max_fragment_length, so the body
// can't be decoded and reads return 0. If this path is ever used to fetch large or CDN-hosted
// payloads, bump the RX buffer to 16384 (see uploadFirmwareURL in core/system.cpp for reference).
ssl_client->setBufferSizes(1024, 1024);
ssl_client->setSessionTimeout(120); // Set the timeout in seconds (>=120 seconds)
}
ssl_client->setClient(basic_client, is_https); // enableSSL = false for plain HTTP
url.replace(0, is_https ? 8 : 7, "");
std::string host = url;
auto index = url.find_first_of('/');
if (index != std::string::npos) {
host = url.substr(0, index);
url.replace(0, index, "");
} else {
url = "/";
}
const uint16_t port = is_https ? 443 : 80;
if (ssl_client->connect(host.c_str(), port)) {
bool content_set = false;
ssl_client->print(is_post ? "POST " : "GET ");
ssl_client->print(url.c_str());
ssl_client->println(" HTTP/1.1");
ssl_client->print("Host: ");
ssl_client->println(host.c_str());
for (JsonPairConst p : headers) {
content_set |= (Helpers::toLower(p.key().c_str()) == "content-type");
ssl_client->print(p.key().c_str());
ssl_client->print(": ");
ssl_client->println(p.value().as<std::string>().c_str());
}
if (is_post) {
if (!content_set) {
ssl_client->print("Content-Type: ");
ssl_client->println(value.starts_with('{') ? asyncsrv::T_application_json : asyncsrv::T_text_plain);
}
ssl_client->print("Content-Length: ");
ssl_client->println(value.length());
ssl_client->println("Connection: close");
ssl_client->print("\r\n");
ssl_client->print(value.c_str());
} else {
ssl_client->println("Connection: close");
}
auto ms = millis();
while (ssl_client->connected() && !ssl_client->available() && millis() - ms < 3000) {
delay(0);
}
while (ssl_client->available()) {
result += (char)ssl_client->read();
}
ssl_client->stop();
index = result.find_first_of(' ');
if (index != std::string::npos) {
httpResult = stoi(result.substr(index + 1, 3));
}
index = result.find("\r\n\r\n");
if (index != std::string::npos) {
result.replace(0, index + 4, "");
}
} else {
EMSESP::logger().warning("%s connection failed", is_https ? "HTTPS" : "HTTP");
}
delete ssl_client;
delete basic_client;
return httpResult;
}
// check for multiple instances of <cond> ? <expr1> : <expr2>
std::string compute(const std::string & expr) {
std::string expr_new = expr;
@@ -723,119 +814,10 @@ std::string compute(const std::string & expr) {
keys_s = p.key().c_str();
}
}
bool content_set = false;
std::string value = doc[value_s] | "";
std::string method = doc[method_s] | "GET";
if (value.length()) {
method = "POST";
}
std::string value = doc[value_s] | "";
std::string method = doc[method_s] | "GET";
std::string result;
int httpResult = 0;
#ifndef NO_TLS_SUPPORT
if (Helpers::toLower(url.c_str()).starts_with("https://")) {
WiFiClient * basic_client = new WiFiClient;
ESP_SSLClient * ssl_client = new ESP_SSLClient;
ssl_client->setInsecure(); // with root CA we should set here: ssl_client->setCACert(rootCACert);
ssl_client->setBufferSizes(1024, 1024);
ssl_client->setSessionTimeout(120); // Set the timeout in seconds (>=120 seconds)
url.replace(0, 8, "");
std::string host = url;
auto index = url.find_first_of('/');
if (index != std::string::npos) {
host = url.substr(0, index);
url.replace(0, index, "");
}
/*
index = host.find_first_of('@');
std::string auth;
if (index != std::string::npos) {
auth = base64::encode(host.substr(0, index));
host.replace(0, index, "");
}
*/
ssl_client->setClient(basic_client);
if (ssl_client->connect(host.c_str(), 443)) {
if (value.length() || Helpers::toLower(method) == "post") {
ssl_client->print("POST ");
ssl_client->print(url.c_str());
ssl_client->println(" HTTP/1.1");
ssl_client->print("Host: ");
ssl_client->println(host.c_str());
for (JsonPair p : doc[header_s].as<JsonObject>()) {
content_set |= (emsesp::Helpers::toLower(p.key().c_str()) == "content-type");
ssl_client->print(p.key().c_str());
ssl_client->print(": ");
ssl_client->println(p.value().as<std::string>().c_str());
}
if (!content_set) {
ssl_client->print("Content-Type: ");
if (value.starts_with('{')) {
ssl_client->println(asyncsrv::T_application_json);
} else {
ssl_client->println(asyncsrv::T_text_plain);
}
}
ssl_client->print("Content-Length: ");
ssl_client->println(value.length());
ssl_client->println("Connection: close");
ssl_client->print("\r\n");
ssl_client->print(value.c_str());
} else {
ssl_client->print("GET ");
ssl_client->print(url.c_str());
ssl_client->println(" HTTP/1.1");
ssl_client->print("Host: ");
ssl_client->println(host.c_str());
for (JsonPair p : doc[header_s].as<JsonObject>()) {
ssl_client->print(p.key().c_str());
ssl_client->print(": ");
ssl_client->println(p.value().as<std::string>().c_str());
}
ssl_client->println("Connection: close");
}
auto ms = millis();
while (!ssl_client->available() && millis() - ms < 3000) {
delay(0);
}
while (ssl_client->available()) {
result += (char)ssl_client->read();
}
ssl_client->stop();
index = result.find_first_of(' ');
if (index != std::string::npos) {
httpResult = stoi(result.substr(index + 1, 3));
}
index = result.find("\r\n\r\n");
if (index != std::string::npos) {
result.replace(0, index + 4, "");
}
}
delete ssl_client;
delete basic_client;
} else
#endif
if (Helpers::toLower(url.c_str()).starts_with("http://")) {
HTTPClient * http = new HTTPClient;
if (http->begin(url.c_str())) {
for (JsonPair p : doc[header_s].as<JsonObject>()) {
http->addHeader(p.key().c_str(), p.value().as<std::string>().c_str());
content_set |= (emsesp::Helpers::toLower(p.key().c_str()) == "content-type");
}
if (value.length() || Helpers::toLower(method) == "post") {
if (!content_set) {
http->addHeader("Content-Type", value.starts_with('{') ? asyncsrv::T_application_json : asyncsrv::T_text_plain);
}
httpResult = http->POST(value.c_str());
} else {
httpResult = http->GET(); // normal GET
}
if (httpResult > 0) {
result = http->getString().c_str();
}
}
http->end();
delete http;
}
int httpResult = http_request(url, method, value, doc[header_s].as<JsonObjectConst>(), result);
if (httpResult == 200) {
std::string key = doc[key_s] | "";
JsonDocument keys_doc; // JsonDocument to hold "keys" after doc is parsed with HTTP body

View File

@@ -21,7 +21,6 @@
#ifndef EMSESP_SHUNTING_YARD_H_
#define EMSESP_SHUNTING_YARD_H_
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <string>
@@ -85,6 +84,8 @@ std::string calculate(const std::string & expr);
// check for multiple instances of <cond> ? <expr1> : <expr2>
std::string compute(const std::string & expr);
int http_request(std::string url, const std::string & method, const std::string & value, JsonObjectConst headers, std::string & result);
#endif
} // namespace emsesp

View File

@@ -29,7 +29,6 @@
#include <nvs.h>
#endif
#include <HTTPClient.h>
#include <map>
#include "firmwareVersion.h"
@@ -38,7 +37,7 @@
#include "../test/test.h"
#endif
#ifndef NO_TLS_SUPPORT
#ifndef EMSESP_STANDALONE
#define ENABLE_SMTP
#define USE_ESP_SSLCLIENT
#define READYCLIENT_SSL_CLIENT ESP_SSLClient
@@ -138,7 +137,7 @@ bool System::command_sendmail(const char * value, const int8_t id) {
bool success = false;
#ifndef NO_TLS_SUPPORT
#ifndef EMSESP_STANDALONE
WiFiClient * basic_client = new WiFiClient;
ESP_SSLClient * ssl_client = new ESP_SSLClient;
ReadyClient * r_client = new ReadyClient(*ssl_client);
@@ -1523,14 +1522,27 @@ bool System::check_upgrade() {
// changes going to v3.9 from an earlier version
if (settings_version.major() == 3 && settings_version.minor() < 9) {
#ifndef EMSESP_STANDALONE
// AP_MODE_ALWAYS has been removed
EMSESP::esp32React.getAPSettingsService()->update([&](APSettings & apSettings) {
if (apSettings.provisionMode == 0) {
apSettings.provisionMode = AP_MODE_DISCONNECTED; // AP_MODE_ALWAYS has been removed
apSettings.provisionMode = AP_MODE_DISCONNECTED; // AP_MODE_DISCONNECTED is the new default
LOG_INFO("Upgrade: Setting AP provision mode to auto");
return StateUpdateResult::CHANGED;
}
return StateUpdateResult::UNCHANGED;
});
// Scheduler name is now mandatory, update FS
uint8_t i = 0;
bool schedule_changed = false;
EMSESP::webSchedulerService.update([&](WebScheduler & scheduler) {
for (ScheduleItem & scheduleItem : scheduler.scheduleItems) {
if (scheduleItem.name[0] == '\0') {
snprintf(scheduleItem.name, sizeof(scheduleItem.name), "schedule_%d", i++);
schedule_changed = true;
}
}
return schedule_changed ? StateUpdateResult::CHANGED : StateUpdateResult::UNCHANGED;
});
#endif
}
@@ -2956,65 +2968,252 @@ bool System::uploadFirmwareURL(const char * url) {
Shell::loop_all(); // flush log buffers so latest messages are shown in console
// Configure temporary client
HTTPClient http;
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); // important for GitHub 302's
http.setTimeout(8000);
http.useHTTP10(true); // use HTTP/1.0 for update since the update handler does not support any transfer Encoding
http.begin(saved_url);
String scheme = saved_url.substring(0, 8);
scheme.toLowerCase();
const bool is_https = scheme.startsWith("https://");
const int scheme_len = is_https ? 8 : 7; // "https://" vs "http://"
// start a connection, returns -1 if fails
int httpCode = http.GET();
if (httpCode != HTTP_CODE_OK) {
LOG_ERROR("Firmware upload failed - HTTP code %d", httpCode);
http.end();
return false; // error
WiFiClient basic_client;
ESP_SSLClient ssl_client;
Stream * stream = nullptr;
int firmware_size = 0;
if (is_https) {
ssl_client.setInsecure(); // no CA validation, matches the rest of the project
// BearSSL needs a receive buffer large enough to hold one full TLS record.
// GitHub's release-assets CDN sends standard up-to-16 KB records and does NOT
// negotiate max_fragment_length, so a small (e.g. 1 KB) RX buffer makes the
// body unreadable (headers still fit one small record, hence Content-Length
// looks fine, but the first body record cannot be decoded). 16384 + overhead
// is the safe value the library itself uses by default; we go a bit smaller
// to be friendlier to 4 MB / no-PSRAM boards while still big enough for any
// record the CDN actually sends in practice.
ssl_client.setBufferSizes(16384, 1024);
ssl_client.setSessionTimeout(120);
}
basic_client.setTimeout(15000); // socket-level read timeout
ssl_client.setTimeout(15000); // Stream::readBytes timeout used by Update
ssl_client.setClient(&basic_client, is_https); // enableSSL = false for plain HTTP
const uint16_t port = is_https ? 443 : 80;
String url_remain = saved_url.substring(scheme_len);
int redirect_count = 0;
while (true) {
// split url_remain into host and path
String host;
String path;
int s = url_remain.indexOf('/');
if (s < 0) {
host = url_remain;
path = "/";
} else {
host = url_remain.substring(0, s);
path = url_remain.substring(s);
}
LOG_DEBUG("Connecting to %s", host.c_str());
if (!ssl_client.connect(host.c_str(), port)) {
LOG_ERROR("Firmware upload failed - connection failed");
return false;
}
// send a minimal HTTP/1.0 GET so we don't have to deal with chunked encoding
ssl_client.print("GET ");
ssl_client.print(path);
ssl_client.println(" HTTP/1.0");
ssl_client.print("Host: ");
ssl_client.println(host);
ssl_client.println("User-Agent: EMS-ESP");
ssl_client.println("Connection: close");
ssl_client.print("\r\n");
// wait for the first byte (up to 8s, matching the previous HTTP timeout)
uint32_t ms = millis();
while (ssl_client.connected() && !ssl_client.available() && millis() - ms < 8000) {
delay(1);
}
// parse status line: "HTTP/1.x CODE TEXT"
String status_line = ssl_client.readStringUntil('\n');
int sp = status_line.indexOf(' ');
int http_code = (sp >= 0) ? status_line.substring(sp + 1, sp + 4).toInt() : 0;
// parse response headers, looking for Content-Length and Location
int content_length = -1;
String location;
while (ssl_client.connected() || ssl_client.available()) {
String line = ssl_client.readStringUntil('\n');
line.trim();
if (line.isEmpty()) {
break; // end of headers
}
int colon = line.indexOf(':');
if (colon < 0) {
continue;
}
String name = line.substring(0, colon);
name.toLowerCase();
String val = line.substring(colon + 1);
val.trim();
if (name == "content-length") {
content_length = val.toInt();
} else if (name == "location") {
location = val;
}
}
// follow redirects manually (GitHub releases redirect to objects.githubusercontent.com)
if (http_code == 301 || http_code == 302 || http_code == 303 || http_code == 307 || http_code == 308) {
ssl_client.stop();
if (location.isEmpty() || ++redirect_count > 5) {
LOG_ERROR("Firmware upload failed - too many redirects");
return false;
}
String lower_loc = location;
lower_loc.toLowerCase();
if (lower_loc.startsWith("https://") || lower_loc.startsWith("http://")) {
// scheme-changing redirect is not supported - the SSL state is
// baked in at setClient() time and we don't want to re-init mid-flight
const bool new_is_https = lower_loc.startsWith("https://");
if (new_is_https != is_https) {
LOG_ERROR("Firmware upload failed - cross-scheme redirect to %s", location.c_str());
return false;
}
url_remain = location.substring(new_is_https ? 8 : 7);
} else if (location.startsWith("/")) {
url_remain = host + location; // relative redirect, same host
} else {
LOG_ERROR("Firmware upload failed - unsupported redirect to %s", location.c_str());
return false;
}
LOG_DEBUG("Following redirect to %s", url_remain.c_str());
continue;
}
if (http_code != 200) {
ssl_client.stop();
LOG_ERROR("Firmware upload failed - HTTP code %d", http_code);
return false;
}
if (content_length <= 0) {
ssl_client.stop();
LOG_ERROR("Firmware upload failed - missing Content-Length");
return false;
}
// wait for the first byte of the body so the read loop sees real data
// (headers and body may arrive in separate TLS records)
uint32_t body_wait = millis();
while (ssl_client.connected() && !ssl_client.available() && millis() - body_wait < 8000) {
delay(1);
}
if (!ssl_client.available()) {
ssl_client.stop();
LOG_ERROR("Firmware upload failed - no body received");
return false;
}
stream = &ssl_client;
firmware_size = content_length;
break;
}
int firmware_size = http.getSize();
// check we have a valid size
if (firmware_size < 2097152) { // 2MB or greater is required
if (firmware_size < 1677721) { // 1.6MB or greater is required
LOG_ERROR("Firmware upload failed - invalid size");
http.end();
return false; // error
}
// check we have enough space for the upload in the ota partition
if (!Update.begin(firmware_size)) {
LOG_ERROR("Firmware upload failed - no space");
http.end();
return false; // error
}
LOG_INFO("Firmware uploading (size: %d KB). Please wait...", firmware_size / 1024);
LOG_INFO("Firmware uploading (size: %d KB) over %s. Please wait...", firmware_size / 1024, is_https ? "HTTPS" : "HTTP");
Shell::loop_all(); // flush log buffers so latest messages are shown in console
// we're about to start the upload, set the status so the Web System Monitor spots it
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_UPLOADING);
// set a callback so we can monitor progress in the WebUI
Update.onProgress([](size_t progress, size_t total) { EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_UPLOADING + (progress * 100 / total)); });
// explicit chunked read loop instead of Update.writeStream():
constexpr size_t CHUNK_SIZE = 1024;
constexpr uint32_t READ_TIMEOUT_MS = 30000; // overall stall timeout per chunk
uint8_t buf[CHUNK_SIZE];
size_t total_read = 0;
bool magic_ok = false;
int last_pct = -1;
// get tcp stream and send it to Updater
WiFiClient * stream = http.getStreamPtr();
if (Update.writeStream(*stream) != firmware_size) {
LOG_ERROR("Firmware upload failed - size differences");
http.end();
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD);
return false; // error
while (total_read < (size_t)firmware_size) {
// wait for some data or for the connection to drop
uint32_t wait_start = millis();
while (!stream->available()) {
if (!ssl_client.connected()) {
break;
}
if (millis() - wait_start > READ_TIMEOUT_MS) {
break;
}
delay(1);
}
if (!stream->available()) {
LOG_ERROR("Firmware upload failed - read stalled at %u of %d bytes", (unsigned)total_read, firmware_size);
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD);
return false;
}
size_t want = (size_t)firmware_size - total_read;
if (want > CHUNK_SIZE) {
want = CHUNK_SIZE;
}
size_t n = stream->readBytes(buf, want);
if (n == 0) {
LOG_ERROR("Firmware upload failed - read returned 0 at %u of %d bytes", (unsigned)total_read, firmware_size);
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD);
return false;
}
// verify the ESP image magic byte the very first time so we fail fast with a
// clear message if the URL points at the wrong asset (HTML, archive, ...)
if (!magic_ok) {
if (buf[0] != 0xE9) {
LOG_ERROR("Firmware upload failed - bad magic byte 0x%02X (expected 0xE9, not an ESP32 firmware image?)", buf[0]);
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD);
return false;
}
magic_ok = true;
}
if (Update.write(buf, n) != n) {
LOG_ERROR("Firmware upload failed - flash write error at %u of %d bytes: %s", (unsigned)total_read, firmware_size, Update.errorString());
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD);
return false;
}
total_read += n;
// update the WebUI status, but only when the percentage actually changes
int pct = (int)(total_read * 100 / (size_t)firmware_size);
if (pct != last_pct) {
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_UPLOADING + pct);
last_pct = pct;
}
yield();
}
if (!Update.end(true)) {
LOG_ERROR("Firmware upload failed - general error");
http.end();
LOG_ERROR("Firmware upload failed - %s", Update.errorString());
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_ERROR_UPLOAD);
return false; // error
}
// finished with upload
http.end();
saved_url.clear(); // prevent from downloading again
LOG_INFO("Firmware uploaded successfully. Restarting...");
EMSESP::system_.systemStatus(SYSTEM_STATUS::SYSTEM_STATUS_PENDING_RESTART);

View File

@@ -1 +1 @@
#define EMSESP_APP_VERSION "3.9.0-dev.9"
#define EMSESP_APP_VERSION "3.9.0-dev.10"

View File

@@ -478,7 +478,7 @@ void WebDataService::dashboard_data(AsyncWebServerRequest * request) {
}
}
// show scheduler, with name, on/off
// show scheduler, with name, on/off, unless it's of type SCHEDULE_IMMEDIATE
if (EMSESP::webSchedulerService.count_entities(true)) {
JsonObject obj = nodes.add<JsonObject>();
obj["id"] = EMSdevice::DeviceTypeUniqueID::SCHEDULER_UID; // it's unique id
@@ -488,8 +488,8 @@ void WebDataService::dashboard_data(AsyncWebServerRequest * request) {
EMSESP::webSchedulerService.read([&](const WebScheduler & webScheduler) {
for (const ScheduleItem & scheduleItem : webScheduler.scheduleItems) {
// only add if we have a name - we don't need a u (UOM) for this
if (scheduleItem.name[0] != '\0') {
// only add if we have a name and it's not of type SCHEDULE_IMMEDIATE - we don't need a u (UOM) for this
if (scheduleItem.name[0] != '\0' && scheduleItem.flags != SCHEDULEFLAG_SCHEDULE_IMMEDIATE) {
JsonObject node = nodes.add<JsonObject>();
node["id"] = (EMSdevice::DeviceTypeUniqueID::SCHEDULER_UID * 100) + count++;

View File

@@ -82,7 +82,7 @@ StateUpdateResult WebScheduler::update(JsonObject root, WebScheduler & webSchedu
EMSESP::webSchedulerService.ha_reset();
// build up the list of schedule items
auto scheduleItems = root["schedule"].as<JsonArray>();
auto scheduleItems = root["schedule"].as<JsonArray>();
for (const JsonObject schedule : scheduleItems) {
// create each schedule item, overwriting any previous settings
// ignore the id (as this is only used in the web for table rendering)
@@ -137,6 +137,7 @@ bool WebSchedulerService::command_setvalue(const char * value, const int8_t id,
if (EMSESP::mqtt_.get_publish_onchange(0)) {
publish();
}
// save new state to nvs #2946
if (scheduleItem.flags != SCHEDULEFLAG_SCHEDULE_IMMEDIATE) {
char key[sizeof(scheduleItem.name) + 2];
@@ -343,6 +344,7 @@ uint8_t WebSchedulerService::count_entities(bool cmd_only) {
}
// execute scheduled command
// return true if successful, false if not
bool WebSchedulerService::command(const char * name, const std::string & command, const std::string & data) {
std::string cmd = Helpers::toLower(command);
@@ -365,130 +367,10 @@ bool WebSchedulerService::command(const char * name, const std::string & command
std::string value = doc["value"] | data; // extract value if its in the command, or take the data
std::string method = doc["method"] | "GET"; // default GET
commands(value, false);
if (value.length()) {
method = "POST";
}
std::string result;
int httpResult = 0;
#ifndef NO_TLS_SUPPORT
if (Helpers::toLower(url.c_str()).starts_with("https://")) {
WiFiClient * basic_client = new WiFiClient;
ESP_SSLClient * ssl_client = new ESP_SSLClient;
ssl_client->setInsecure(); // with root CA we should set here: ssl_client->setCACert(rootCACert);
ssl_client->setBufferSizes(1024, 1024);
ssl_client->setSessionTimeout(120); // Set the timeout in seconds (>=120 seconds)
url.replace(0, 8, "");
std::string host = url;
auto index = url.find_first_of('/');
if (index != std::string::npos) {
host = url.substr(0, index);
url.replace(0, index, "");
}
// EMSESP::logger().debug("Host: %s, URL: %s", host.c_str(), url.c_str());
ssl_client->setClient(basic_client);
if (ssl_client->connect(host.c_str(), 443)) {
if (value.length() || Helpers::toLower(method) == "post") {
// EMSESP::logger().debug("POST %s HTTP/1.1", url.c_str());
ssl_client->print("POST ");
ssl_client->print(url.c_str());
ssl_client->println(" HTTP/1.1");
ssl_client->print("Host: ");
ssl_client->println(host.c_str());
bool content_set = false;
for (JsonPair p : doc["header"].as<JsonObject>()) {
content_set |= (emsesp::Helpers::toLower(p.key().c_str()) == "content-type");
ssl_client->print(p.key().c_str());
ssl_client->print(": ");
ssl_client->println(p.value().as<std::string>().c_str());
}
if (!content_set) {
ssl_client->print("Content-Type: ");
if (value.starts_with('{')) {
ssl_client->println(asyncsrv::T_application_json);
} else {
ssl_client->println(asyncsrv::T_text_plain);
}
}
ssl_client->print("Content-Length: ");
ssl_client->println(value.length());
ssl_client->println("Connection: close");
ssl_client->print("\r\n");
ssl_client->print(value.c_str());
} else {
// EMSESP::logger().debug("GET %s HTTP/1.1", url.c_str());
ssl_client->print("GET ");
ssl_client->print(url.c_str());
ssl_client->println(" HTTP/1.1");
ssl_client->print("Host: ");
ssl_client->println(host.c_str());
for (JsonPair p : doc["header"].as<JsonObject>()) {
ssl_client->print(p.key().c_str());
ssl_client->print(": ");
ssl_client->println(p.value().as<std::string>().c_str());
}
ssl_client->println("Connection: close");
}
auto ms = millis();
while (ssl_client->connected() && !ssl_client->available() && millis() - ms < 3000) {
delay(0);
}
while (ssl_client->available()) {
result += (char)ssl_client->read();
}
ssl_client->stop();
// EMSESP::logger().debug("HTTPS response: %s", result.c_str());
index = result.find_first_of(' ');
if (index != std::string::npos) {
httpResult = stoi(result.substr(index + 1, 3));
// EMSESP::logger().debug("HTTPS code: %i", httpResult);
}
index = result.find("\r\n\r\n");
if (index != std::string::npos) {
result.replace(0, index + 4, "");
// EMSESP::logger().debug("HTTPS response: %s", result.c_str());
}
} else {
EMSESP::logger().warning("HTTPS connection failed");
}
delete ssl_client;
delete basic_client;
// check HTTP return code
if (httpResult != 200) {
EMSESP::logger().warning("Schedule '%s': URL command failed with http code %d", name, httpResult);
return false;
}
return true;
} else
#endif
if (Helpers::toLower(url.c_str()).starts_with("http://")) {
HTTPClient * http = new HTTPClient;
if (http->begin(url.c_str())) {
bool content_set = false;
for (JsonPair p : doc["header"].as<JsonObject>()) {
http->addHeader(p.key().c_str(), p.value().as<std::string>().c_str());
content_set |= p.key() == "content-type";
}
// if there is data, force a POST
if (Helpers::toLower(method) == "post") { // we have all lowercase
if (!content_set) {
// http->addHeader("Content-Type", value.find_first_of('{') != std::string::npos ? "application/json" : "text/plain");
if (value.starts_with('{')) {
http->addHeader(asyncsrv::T_Content_Type, asyncsrv::T_application_json, false); // auto-set to JSON
} else {
http->addHeader(asyncsrv::T_Content_Type, asyncsrv::T_text_plain, false); // auto-set to JSON
}
}
httpResult = http->POST(value.c_str());
} else {
httpResult = http->GET(); // normal GET
if (httpResult > 0) {
result = http->getString().c_str();
}
}
}
http->end();
delete http;
// check HTTP return code
auto lower_url = Helpers::toLower(url.c_str());
if (lower_url.starts_with("http://") || lower_url.starts_with("https://")) {
std::string result;
int httpResult = http_request(url, method, value, doc["header"].as<JsonObjectConst>(), result);
if (httpResult != 200) {
EMSESP::logger().warning("Schedule '%s': URL command failed with http code %d", name, httpResult);
return false;
@@ -598,7 +480,7 @@ void WebSchedulerService::loop() {
for (ScheduleItem & scheduleItem : *scheduleItems_) {
if (scheduleItem.active && scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_IMMEDIATE) {
command(scheduleItem.name, scheduleItem.cmd.c_str(), compute(scheduleItem.value.c_str()));
scheduleItem.active = false;
// scheduleItem.active = false;
publish_single(scheduleItem.name, false);
if (EMSESP::mqtt_.get_publish_onchange(0)) {
publish();
@@ -659,6 +541,18 @@ void WebSchedulerService::loop() {
}
}
// execute a schedule item immediately
bool WebSchedulerService::executeSchedule(const char * name) {
for (ScheduleItem & scheduleItem : *scheduleItems_) {
if (scheduleItem.flags == SCHEDULEFLAG_SCHEDULE_IMMEDIATE && strcmp(scheduleItem.name, name) == 0) {
EMSESP::logger().info("Executing schedule '%s'", name);
return command(scheduleItem.name, scheduleItem.cmd.c_str(), compute(scheduleItem.value.c_str()));
}
}
EMSESP::logger().warning("Schedule '%s' not found", name);
return false; // not found
}
// process schedules async
void WebSchedulerService::scheduler_task(void * pvParameters) {
while (1) {
@@ -681,11 +575,11 @@ void WebSchedulerService::load_test_data() {
// test 1
auto si = ScheduleItem();
si.active = true;
si.flags = 1;
si.flags = 1; // day schedule
si.time = "12:00";
si.cmd = "system/fetch";
si.value = "10";
strcpy(si.name, "test_scheduler");
strcpy(si.name, "test_scheduler1");
si.elapsed_min = 0;
si.retry_cnt = 0xFF; // no startup retries
@@ -694,11 +588,11 @@ void WebSchedulerService::load_test_data() {
// test 2
si = ScheduleItem();
si.active = false;
si.flags = 1;
si.flags = SCHEDULEFLAG_SCHEDULE_IMMEDIATE; // immediate
si.time = "13:00";
si.cmd = "system/message";
si.value = "20";
strcpy(si.name, ""); // to make sure its excluded from Dashboard
strcpy(si.name, "test_scheduler2"); // to make sure its excluded from Dashboard
si.elapsed_min = 0;
si.retry_cnt = 0xFF; // no startup retries

View File

@@ -38,14 +38,14 @@
// bit flags for the schedule items. Matches those in interface/src/app/main/SchedulerDialog.tsx
// 0-127 (0->0x7F) is day schedule
// 128/0x80 is timer
// 129/0x81 is on change
// 130/0x82 is on condition
// 132/0x84 is immediate
// 128 (0x80) is timer
// 129 (0x81) is on change
// 130 (0x82) is on condition
// 132 (0x84) is immediate
#define SCHEDULEFLAG_SCHEDULE_TIMER 0x80 // 7th bit for Timer
#define SCHEDULEFLAG_SCHEDULE_ONCHANGE 0x81 // 7th+1st bit for OnChange
#define SCHEDULEFLAG_SCHEDULE_CONDITION 0x82 // 7th+2nd bit for Condition
#define SCHEDULEFLAG_SCHEDULE_IMMEDIATE 0x84 // 7th+3rd bit for Condition
#define SCHEDULEFLAG_SCHEDULE_IMMEDIATE 0x84 // 7th+3rd bit for Immediate
#define MAX_STARTUP_RETRIES 3 // retry the start-up commands x times
@@ -54,7 +54,7 @@ namespace emsesp {
class ScheduleItem {
public:
boolean active;
uint8_t flags;
uint8_t flags; // bit flags, see SCHEDULEFLAG_* defines
uint16_t elapsed_min; // total mins from 00:00
stringPSRAM time; // HH:MM
stringPSRAM cmd;
@@ -88,6 +88,8 @@ class WebSchedulerService : public StatefulService<WebScheduler> {
uint8_t count_entities(bool cmd_only = false);
bool onChange(const char * cmd);
bool executeSchedule(const char * name);
std::string get_metrics_prometheus();
std::string raw_value;

View File

@@ -83,19 +83,15 @@ void WebSettings::read(WebSettings & settings, JsonObject root) {
root["modbus_max_clients"] = settings.modbus_max_clients;
root["modbus_timeout"] = settings.modbus_timeout;
root["developer_mode"] = settings.developer_mode;
#ifndef NO_TLS_SUPPORT
root["email_enabled"] = settings.email_enabled;
#else
root["email_enabled"] = false;
#endif
root["email_security"] = settings.email_security;
root["email_server"] = settings.email_server;
root["email_port"] = settings.email_port;
root["email_login"] = settings.email_login;
root["email_pass"] = settings.email_pass;
root["email_sender"] = settings.email_sender;
root["email_recp"] = settings.email_recp;
root["email_subject"] = settings.email_subject;
root["email_enabled"] = settings.email_enabled;
root["email_security"] = settings.email_security;
root["email_server"] = settings.email_server;
root["email_port"] = settings.email_port;
root["email_login"] = settings.email_login;
root["email_pass"] = settings.email_pass;
root["email_sender"] = settings.email_sender;
root["email_recp"] = settings.email_recp;
root["email_subject"] = settings.email_subject;
}
// call on initialization and also when settings are updated/saved via web or console

View File

@@ -20,7 +20,8 @@
#ifndef EMSESP_STANDALONE
#include <esp_ota_ops.h>
#include <HTTPClient.h>
#include <WiFiClient.h>
#include <ESP_SSLClient.h>
#endif
namespace emsesp {
@@ -227,6 +228,8 @@ void WebStatusService::action(AsyncWebServerRequest * request, JsonVariant json)
EMSESP::mqtt_.reset_mqtt();
} else if (action == "upgradeImportantMessages") {
root["upgradeImportantMessageType"] = upgradeImportantMessages(param);
} else if (action == "executeSchedule") {
ok = EMSESP::webSchedulerService.executeSchedule(param.c_str());
}
#if defined(EMSESP_STANDALONE) && !defined(EMSESP_UNITY)
@@ -412,30 +415,91 @@ bool WebStatusService::refresh_versions_cache() {
#ifdef EMSESP_STANDALONE
return false;
#else
HTTPClient http;
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
http.setTimeout(5000);
http.useHTTP10(true);
if (!http.begin(EMSESP_VERSIONS_URL)) {
// detect scheme from EMSESP_VERSIONS_URL (case-insensitive). One code path for HTTP and HTTPS,
// using ESP_SSLClient as a plain TCP passthrough when SSL is disabled.
String url = EMSESP_VERSIONS_URL;
String lower = url;
lower.toLowerCase();
const bool is_https = lower.startsWith("https://");
if (!is_https && !lower.startsWith("http://")) {
#if defined(EMSESP_DEBUG)
EMSESP::logger().debug("versions.json: failed to start HTTPS request");
EMSESP::logger().debug("versions.json: unsupported scheme");
#endif
return false;
}
const int scheme_len = is_https ? 8 : 7;
WiFiClient basic_client;
ESP_SSLClient ssl_client;
if (is_https) {
ssl_client.setInsecure();
ssl_client.setBufferSizes(16384, 1024); // versions.json fits easily but TLS records may be full-size
ssl_client.setSessionTimeout(120);
}
basic_client.setTimeout(5000);
ssl_client.setTimeout(5000);
ssl_client.setClient(&basic_client, is_https);
// split into host and path
String rest = url.substring(scheme_len);
String host;
String path;
int s = rest.indexOf('/');
if (s < 0) {
host = rest;
path = "/";
} else {
host = rest.substring(0, s);
path = rest.substring(s);
}
if (!ssl_client.connect(host.c_str(), is_https ? 443 : 80)) {
#if defined(EMSESP_DEBUG)
EMSESP::logger().debug("versions.json: connection failed");
#endif
return false;
}
int httpCode = http.GET();
if (httpCode != HTTP_CODE_OK) {
// minimal HTTP/1.0 GET so we don't have to handle chunked encoding
ssl_client.print("GET ");
ssl_client.print(path);
ssl_client.println(" HTTP/1.0");
ssl_client.print("Host: ");
ssl_client.println(host);
ssl_client.println("User-Agent: EMS-ESP");
ssl_client.println("Connection: close");
ssl_client.print("\r\n");
// wait for the first byte
uint32_t ms = millis();
while (ssl_client.connected() && !ssl_client.available() && millis() - ms < 5000) {
delay(1);
}
// parse status line
String status_line = ssl_client.readStringUntil('\n');
int sp = status_line.indexOf(' ');
int http_code = (sp >= 0) ? status_line.substring(sp + 1, sp + 4).toInt() : 0;
if (http_code != 200) {
ssl_client.stop();
#if defined(EMSESP_DEBUG)
EMSESP::logger().debug("versions.json: HTTP error code %d", httpCode);
EMSESP::logger().debug("versions.json: HTTP error code %d", http_code);
#endif
http.end();
return false;
}
// skip headers
while (ssl_client.connected() || ssl_client.available()) {
String line = ssl_client.readStringUntil('\n');
line.trim();
if (line.isEmpty()) {
break;
}
}
JsonDocument doc(PSRAM_DOC);
DeserializationError err = deserializeJson(doc, http.getStream());
http.end();
DeserializationError err = deserializeJson(doc, ssl_client);
ssl_client.stop();
if (err) {
#if defined(EMSESP_DEBUG)
EMSESP::logger().debug("versions.json: parse error");