Compare commits

..
21 changed files with 1357 additions and 177 deletions

No files matched your search

-2
View File
@@ -449,8 +449,6 @@ file does, and it is the authority when they disagree. The most useful starting
```
Restore an owned or standalone switch in `setup()` with `sw->restore()`, or `restore(default)`
to still drive a state when the restore mode is disabled.
A hub that applies its switches' restored states itself reads `sw->get_initial_write_state()`, which
already applies `inverted`.
- **Other codegen helpers:** `uart.new_uart_component(config, *ctor_args)` (and `i2c.new_i2c_component`,
`spi.new_spi_component`, `modbus.new_modbus_client_component`, `ble_client.new_ble_node_component`)
+1
View File
@@ -19,6 +19,7 @@ CONF_BITRATE = "bitrate"
CONF_BLUE_CHANNEL = "blue_channel"
CONF_BORDER_ROUTER = "border_router"
CONF_BREATH_VOC_EQUIVALENT = "breath_voc_equivalent"
CONF_BUTTONS = "buttons"
CONF_BYTE_ORDER = "byte_order"
CONF_CHANNEL_COLORS = "channel_colors"
CONF_CLEAR_CHANNEL = "clear_channel"
+1 -1
View File
@@ -18,6 +18,7 @@ from esphome.components import (
text_sensor,
valve,
)
from esphome.components.const import CONF_BUTTONS
import esphome.config_validation as cv
from esphome.const import (
CONF_ACCURACY_DECIMALS,
@@ -167,7 +168,6 @@ VALVE_TYPES = {
CONF_ALARM_CONTROL_PANELS = "alarm_control_panels"
CONF_BUTTONS = "buttons"
CONF_CLIMATES = "climates"
CONF_COVERS = "covers"
CONF_DATETIMES = "datetimes"
+10 -3
View File
@@ -56,6 +56,7 @@ CONF_HTTP_REQUEST_ID = "http_request_id"
CONF_USERAGENT = "useragent"
CONF_FOLLOW_REDIRECTS = "follow_redirects"
CONF_REDIRECT_LIMIT = "redirect_limit"
CONF_MAX_CONCURRENT_REQUESTS = "max_concurrent_requests"
CONF_BUFFER_SIZE_RX = "buffer_size_rx"
CONF_BUFFER_SIZE_TX = "buffer_size_tx"
CONF_TLS_BUFFER_SIZE_RX = "tls_buffer_size_rx"
@@ -144,6 +145,10 @@ CONFIG_SCHEMA = cv.All(
): cv.string,
cv.Optional(CONF_FOLLOW_REDIRECTS, True): cv.boolean,
cv.Optional(CONF_REDIRECT_LIMIT, 3): cv.int_,
# Each request holds a client, and a TLS client about 40 kB
cv.Optional(CONF_MAX_CONCURRENT_REQUESTS, default=1): cv.int_range(
min=1, max=4
),
cv.Optional(
CONF_TIMEOUT, default="4.5s"
): cv.positive_time_period_milliseconds,
@@ -193,6 +198,8 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_useragent(config[CONF_USERAGENT]))
cg.add(var.set_follow_redirects(config[CONF_FOLLOW_REDIRECTS]))
cg.add(var.set_redirect_limit(config[CONF_REDIRECT_LIMIT]))
if (max_concurrent := config[CONF_MAX_CONCURRENT_REQUESTS]) != 1:
cg.add(var.set_max_concurrent_requests(max_concurrent))
if CORE.is_esp8266 and not config[CONF_ESP8266_DISABLE_SSL_SUPPORT]:
cg.add_define("USE_HTTP_REQUEST_ESP8266_HTTPS")
@@ -312,19 +319,19 @@ HTTP_REQUEST_SEND_ACTION_SCHEMA = HTTP_REQUEST_ACTION_SCHEMA.extend(
"http_request.get",
HttpRequestSendAction,
HTTP_REQUEST_GET_ACTION_SCHEMA,
synchronous=True,
synchronous=False,
)
@automation.register_action(
"http_request.post",
HttpRequestSendAction,
HTTP_REQUEST_POST_ACTION_SCHEMA,
synchronous=True,
synchronous=False,
)
@automation.register_action(
"http_request.send",
HttpRequestSendAction,
HTTP_REQUEST_SEND_ACTION_SCHEMA,
synchronous=True,
synchronous=False,
)
async def http_request_action_to_code(
config: ConfigType,
@@ -1,5 +1,7 @@
#include "http_request.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include <cinttypes>
@@ -14,8 +16,10 @@ void HttpRequestComponent::dump_config() {
" Timeout: %" PRIu32 "ms\n"
" User-Agent: %s\n"
" Follow redirects: %s\n"
" Redirect limit: %d",
this->timeout_, this->useragent_, YESNO(this->follow_redirects_), this->redirect_limit_);
" Redirect limit: %d\n"
" Max concurrent requests: %u",
this->timeout_, this->useragent_, YESNO(this->follow_redirects_), this->redirect_limit_,
this->max_concurrent_);
if (this->watchdog_timeout_ > 0) {
ESP_LOGCONFIG(TAG, " Watchdog Timeout: %" PRIu32 "ms", this->watchdog_timeout_);
}
@@ -32,4 +36,209 @@ std::string HttpContainer::get_response_header(const std::string &header_name) {
return "";
}
void PendingRequest::free_buffer() {
if (this->buf != nullptr)
RAMAllocator<uint8_t>().deallocate(this->buf, this->buf_size);
this->buf = nullptr;
}
void HttpRequestComponent::setup() {
// An on_boot automation may already have queued a request
if (this->requests_ == nullptr)
this->disable_loop();
}
void HttpRequestComponent::loop() {
if (!this->process_())
this->disable_loop();
}
void HttpRequestComponent::on_safe_shutdown() {
// Called before any on_shutdown(). Drop the requests already made, so that they do not hold up the reboot;
// only the ones that on_shutdown automations make are sent.
this->shutting_down_ = true;
this->busy_++;
unsigned dropped = 0;
for (PendingRequest *request = this->requests_; request != nullptr; request = request->next) {
if (request->finished || request->cancelled)
continue;
dropped++;
request->cancelled = true;
// As script.stop would, so that a script that was waiting for it can run again from on_shutdown
request->handler(request, RequestEvent::REQUEST_EVENT_ABANDON);
}
this->busy_--;
if (dropped != 0) {
ESP_LOGW(TAG, "Shutting down; %u requests dropped", dropped);
}
if (this->busy_ == 0)
this->remove_dead_();
}
void HttpRequestComponent::on_shutdown() {
// Shutdown runs no loop(), so finish the requests here, blocking as they did before. A forced reboot calls
// only this, so it also finishes the requests made before it.
this->shutting_down_ = true;
this->drain_();
}
void HttpRequestComponent::submit(PendingRequest *request) {
this->busy_++;
uint8_t running = 0;
uint8_t waiting = 0;
PendingRequest **link = &this->requests_;
for (; *link != nullptr; link = &(*link)->next) {
const PendingRequest *other = *link;
if (other->finished || other->cancelled)
continue;
if (other->container == nullptr) {
waiting++;
} else {
running++;
}
}
*link = request;
this->enable_loop();
if (waiting >= MAX_QUEUED) {
ESP_LOGW(TAG, "Too many requests waiting; request dropped");
request->failed = true;
this->finish_(request);
} else if (waiting == 0 && running < this->max_concurrent_) {
// Start at once; one that cannot start is reported at once
this->begin_request_(request);
if (request->failed)
this->finish_(request);
}
this->busy_--;
if (this->busy_ != 0)
return;
if (this->shutting_down_) {
this->drain_();
} else {
this->remove_dead_();
}
}
void HttpRequestComponent::cancel(void *owner) {
for (PendingRequest *request = this->requests_; request != nullptr; request = request->next) {
if (request->owner == owner)
request->cancelled = true;
}
if (this->busy_ == 0)
this->remove_dead_();
}
bool HttpRequestComponent::process_() {
this->busy_++;
// The first max_concurrent_ live requests run; a request finished here frees its slot on the next pass
uint8_t slots = this->max_concurrent_;
for (PendingRequest *request = this->requests_; request != nullptr && slots != 0; request = request->next) {
if (request->finished || request->cancelled)
continue;
slots--;
if (request->container == nullptr)
this->begin_request_(request);
if (!request->cancelled && (request->failed || this->advance_(request)))
this->finish_(request);
}
this->busy_--;
if (this->busy_ == 0)
this->remove_dead_();
return this->requests_ != nullptr;
}
void HttpRequestComponent::drain_() {
// A call further up holds request pointers, and runs the new requests itself
if (this->busy_ != 0)
return;
while (this->process_()) {
App.feed_wdt();
delay(1);
}
}
void HttpRequestComponent::begin_request_(PendingRequest *request) {
request->start_time = millis();
request->handler(request, RequestEvent::REQUEST_EVENT_START);
// Stopped from the request's own lambdas: it is removed without being polled
if (!request->cancelled && request->container == nullptr)
request->failed = true;
}
bool HttpRequestComponent::advance_(PendingRequest *request) {
if (!request->reading) {
auto progress = request->container->poll();
if (progress == HttpRequestProgress::HTTP_REQUEST_PROGRESS_PENDING)
return false;
if (progress == HttpRequestProgress::HTTP_REQUEST_PROGRESS_FAILED) {
request->failed = true;
return true;
}
if (!request->capture)
return true;
request->buf = RAMAllocator<uint8_t>().allocate(request->buf_size);
if (request->buf == nullptr) {
// As before, on_response still fires, with an empty body
ESP_LOGW(TAG, "Could not allocate %zu bytes for the response body", request->buf_size);
return true;
}
request->reading = true;
// millis(), not the loop time: poll() may have blocked earlier in this pass
request->last_data_time = millis();
}
// NOTE: HttpContainer::read() has non-BSD socket semantics - see http_request.h
// millis(), not the loop time: the budget is shorter than one pass of the loop
const uint32_t start = millis();
while (request->read_index < request->buf_size) {
int read_or_error =
request->container->read_available(request->buf + request->read_index, request->buf_size - request->read_index);
const uint32_t now = millis();
// As before, a body that is cut off still fires on_response with what was read
if (read_or_error < 0) {
ESP_LOGW(TAG, "Response body read failed after %zu bytes", request->read_index);
return true;
}
if (read_or_error == 0) {
if (request->container->is_read_complete())
return true;
if (now - request->last_data_time < request->timeout)
return false;
ESP_LOGW(TAG, "Response body timed out after %zu bytes", request->read_index);
return true;
}
request->read_index += read_or_error;
request->last_data_time = now;
if (now - start >= READ_BUDGET_MS)
return false;
}
// As before, on_response still fires, with the part that fitted
if (!request->container->is_read_complete()) {
ESP_LOGW(TAG, "Response body is larger than max_response_buffer_size; kept the first %zu bytes", request->buf_size);
}
return true;
}
void HttpRequestComponent::finish_(PendingRequest *request) {
request->finished = true;
// The body was read over several passes, so time it from the start, as a blocking read would
if (request->reading && request->container != nullptr)
request->container->duration_ms = millis() - request->start_time;
request->handler(request, RequestEvent::REQUEST_EVENT_FINISH);
}
void HttpRequestComponent::remove_dead_() {
PendingRequest **link = &this->requests_;
while (PendingRequest *request = *link) {
if (!request->finished && !request->cancelled) {
link = &request->next;
continue;
}
*link = request->next;
if (request->container != nullptr)
request->container->end();
request->free_buffer();
request->handler(request, RequestEvent::REQUEST_EVENT_DESTROY);
}
}
} // namespace esphome::http_request
+162 -52
View File
@@ -12,6 +12,7 @@
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/alloc_helpers.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -186,6 +187,13 @@ enum class HttpReadLoopResult : uint8_t {
TIMEOUT, ///< Timeout waiting for data, caller should exit loop
};
/// Progress of a request started with HttpRequestComponent::begin()
enum class HttpRequestProgress : uint8_t {
HTTP_REQUEST_PROGRESS_PENDING, ///< Still connecting, sending, or waiting for the response headers
HTTP_REQUEST_PROGRESS_DONE, ///< Response headers received; status_code is set and the body can be read
HTTP_REQUEST_PROGRESS_FAILED, ///< No response; the container must still be ended
};
/// Process a read result with timeout tracking and delay handling
/// @param bytes_read_or_error Return value from read() - positive for bytes read, negative for error
/// @param last_data_time Time of last successful read, updated when data received
@@ -249,8 +257,15 @@ class HttpContainer : public Parented<HttpRequestComponent> {
* - http_read_fully(): for simple "read N bytes" operations
*/
virtual int read(uint8_t *buf, size_t max_len) = 0;
/// Like read(), but returns 0 at once when no data has arrived yet, where read() may wait for it.
virtual int read_available(uint8_t *buf, size_t max_len) { return this->read(buf, max_len); }
virtual void end() = 0;
/// Advance a request started with HttpRequestComponent::begin(); call from the main loop until it is no
/// longer pending. Containers from start() are already done. Most calls return quickly, but on ESP-IDF the
/// first call for each host waits for the name lookup and the TCP connection, up to the whole timeout.
virtual HttpRequestProgress poll() { return HttpRequestProgress::HTTP_REQUEST_PROGRESS_DONE; }
void set_secure(bool secure) { this->secure_ = secure; }
void set_watchdog_timeout(uint32_t watchdog_timeout) { this->watchdog_timeout_ = watchdog_timeout; }
uint32_t get_watchdog_timeout() const { return this->watchdog_timeout_; }
@@ -332,10 +347,56 @@ class HttpRequestResponseTrigger final : public Trigger<std::shared_ptr<HttpCont
}
};
/// What HttpRequestComponent asks of the action that owns a request
enum class RequestEvent : uint8_t {
REQUEST_EVENT_START, ///< Build the request and call begin()
REQUEST_EVENT_FINISH, ///< Fire the triggers and continue the action chain
REQUEST_EVENT_DESTROY, ///< Free the request
REQUEST_EVENT_ABANDON, ///< Dropped at shutdown: stop the action, so that it can run again
};
/// A request from an HttpRequestSendAction, run from HttpRequestComponent::loop().
/// The action derives from it to add the trigger arguments.
struct PendingRequest {
using Handler = void (*)(PendingRequest *request, RequestEvent event);
PendingRequest(Handler handler, void *owner) : handler(handler), owner(owner) {}
PendingRequest(const PendingRequest &) = delete;
PendingRequest &operator=(const PendingRequest &) = delete;
void free_buffer();
PendingRequest *next{nullptr};
Handler handler;
// The action, so that a stop of it can find its requests
void *owner;
// nullptr until the request has started
std::shared_ptr<HttpContainer> container;
uint8_t *buf{nullptr};
size_t buf_size{0};
size_t read_index{0};
uint32_t last_data_time{0};
uint32_t timeout{0};
uint32_t start_time{0};
bool capture{false};
bool reading{false};
bool failed{false};
bool finished{false};
bool cancelled{false};
};
class HttpRequestComponent : public Component {
public:
void dump_config() override;
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
void setup() override;
void loop() override;
void on_safe_shutdown() override;
void on_shutdown() override;
/// For HttpRequestSendAction: queue a request and take ownership of it. Requests start in order, up to
/// max_concurrent_requests at a time; one that can start at once is started here.
void submit(PendingRequest *request);
/// For HttpRequestSendAction: drop the requests of a stopped action without firing their triggers.
void cancel(void *owner);
void set_useragent(const char *useragent) { this->useragent_ = useragent; }
void set_timeout(uint32_t timeout) { this->timeout_ = timeout; }
@@ -349,6 +410,7 @@ class HttpRequestComponent : public Component {
}
void set_follow_redirects(bool follow_redirects) { this->follow_redirects_ = follow_redirects; }
void set_redirect_limit(uint16_t limit) { this->redirect_limit_ = limit; }
void set_max_concurrent_requests(uint8_t max_concurrent) { this->max_concurrent_ = max_concurrent; }
std::shared_ptr<HttpContainer> get(const char *url) { return this->start(url, "GET", "", std::vector<Header>{}); }
std::shared_ptr<HttpContainer> get(const char *url, const std::vector<Header> &request_headers) {
@@ -491,16 +553,44 @@ class HttpRequestComponent : public Component {
return this->start(url.c_str(), method.c_str(), body, request_headers, lower_case_collect_headers);
}
/// Start a request without waiting for the response, then poll() the container until it is not pending.
/// Returns nullptr when the request cannot be started. Platforms without a non-blocking client complete
/// the request here.
virtual std::shared_ptr<HttpContainer> begin(const char *url, const char *method, std::string &&body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers,
uint32_t timeout_ms) {
return this->perform(url, method, body, request_headers, lower_case_collect_headers, timeout_ms);
}
protected:
virtual std::shared_ptr<HttpContainer> perform(const char *url, const char *method, const std::string &body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers,
uint32_t timeout_ms) = 0;
/// Longest time one loop() pass spends reading a response body
static constexpr uint32_t READ_BUDGET_MS = 10;
/// Requests that wait for a free slot; more are dropped with on_error
static constexpr uint8_t MAX_QUEUED = 4;
bool process_();
void drain_();
void begin_request_(PendingRequest *request);
bool advance_(PendingRequest *request);
void finish_(PendingRequest *request);
void remove_dead_();
const char *useragent_{nullptr};
bool follow_redirects_{};
uint16_t redirect_limit_{};
uint32_t timeout_{4500};
uint32_t watchdog_timeout_{0};
// Oldest first; the first max_concurrent_ live requests run
PendingRequest *requests_{nullptr};
// Depth of calls that hold request pointers; finished and cancelled requests are freed when it returns to 0
uint8_t busy_{0};
uint8_t max_concurrent_{1};
bool shutting_down_{false};
};
template<typename... Ts> class HttpRequestSendAction final : public Action<Ts...> {
@@ -542,7 +632,42 @@ template<typename... Ts> class HttpRequestSendAction final : public Action<Ts...
this->max_response_buffer_size_ = max_response_buffer_size;
}
void play(const Ts &...x) override {
void play_complex(const Ts &...x) override {
this->num_running_++;
this->parent_->submit(new Request(this, x...)); // NOLINT(cppcoreguidelines-owning-memory)
}
void play(const Ts &...x) override { /* see play_complex */ }
protected:
struct Request final : PendingRequest {
Request(HttpRequestSendAction *action, const Ts &...x)
: PendingRequest(&HttpRequestSendAction::handle_event, action), args(x...) {}
std::tuple<stored_trigger_arg_t<Ts>...> args;
};
static void handle_event(PendingRequest *request, RequestEvent event) {
auto *self = static_cast<Request *>(request);
auto *action = static_cast<HttpRequestSendAction *>(request->owner);
switch (event) {
case RequestEvent::REQUEST_EVENT_START:
std::apply([action, self](auto &...x) { action->start_request_(*self, x...); }, self->args);
break;
case RequestEvent::REQUEST_EVENT_FINISH:
action->finish_(*self);
break;
case RequestEvent::REQUEST_EVENT_DESTROY:
delete self; // NOLINT(cppcoreguidelines-owning-memory)
break;
case RequestEvent::REQUEST_EVENT_ABANDON:
action->stop_complex();
break;
}
}
void stop() override { this->parent_->cancel(this); }
void start_request_(Request &request, const Ts &...x) {
std::string body;
if (this->body_.has_value()) {
body = this->body_.value(x...);
@@ -559,62 +684,47 @@ template<typename... Ts> class HttpRequestSendAction final : public Action<Ts...
request_headers.push_back({header.name, header.value(x...)});
}
const uint32_t timeout = this->timeout_.has_value() ? this->timeout_.value(x...) : this->parent_->get_timeout();
auto container = this->parent_->start(this->url_.value(x...).c_str(), this->method_.value(x...), body,
request_headers, this->lower_case_collect_headers_, timeout);
auto captured_args = std::make_tuple(x...);
if (container == nullptr) {
std::apply([this](Ts... captured_args_inner) { this->error_trigger_.trigger(captured_args_inner...); },
captured_args);
return;
}
request.timeout = this->timeout_.has_value() ? this->timeout_.value(x...) : this->parent_->get_timeout();
#ifdef USE_HTTP_REQUEST_RESPONSE
if (this->capture_response_.value(x...)) {
size_t max_length = this->max_response_buffer_size_;
std::string response_body;
RAMAllocator<uint8_t> allocator;
uint8_t *buf = allocator.allocate(max_length);
if (buf != nullptr) {
// NOTE: HttpContainer::read() has non-BSD socket semantics - see top of this file
// Use http_read_loop_result() helper instead of checking return values directly
size_t read_index = 0;
uint32_t last_data_time = millis();
const uint32_t read_timeout = timeout;
while (container->get_bytes_read() < max_length) {
int read_or_error = container->read(buf + read_index, std::min<size_t>(max_length - read_index, 512));
App.feed_wdt();
yield();
auto result =
http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete());
if (result == HttpReadLoopResult::RETRY)
continue;
if (result != HttpReadLoopResult::DATA)
break; // COMPLETE, ERROR, or TIMEOUT
read_index += read_or_error;
}
response_body.reserve(read_index);
response_body.assign((char *) buf, read_index);
allocator.deallocate(buf, max_length);
}
std::apply(
[this, &container, &response_body](Ts... captured_args_inner) {
this->success_trigger_with_response_.trigger(container, response_body, captured_args_inner...);
},
captured_args);
} else
request.capture = this->capture_response_.value(x...);
#endif
{
std::apply([this, &container](
Ts... captured_args_inner) { this->success_trigger_.trigger(container, captured_args_inner...); },
captured_args);
request.buf_size = this->max_response_buffer_size_;
request.container = this->parent_->begin(this->url_.value(x...).c_str(), this->method_.value(x...), std::move(body),
request_headers, this->lower_case_collect_headers_, request.timeout);
}
void finish_(Request &request) {
auto &container = request.container;
if (request.failed) {
std::apply([this](auto &...args) { this->error_trigger_.trigger(args...); }, request.args);
}
container->end();
#ifdef USE_HTTP_REQUEST_RESPONSE
else if (request.capture) {
std::string response_body;
if (request.buf != nullptr)
response_body.assign(reinterpret_cast<char *>(request.buf), request.read_index);
request.free_buffer();
std::apply(
[this, &container, &response_body](auto &...args) {
this->success_trigger_with_response_.trigger(container, response_body, args...);
},
request.args);
}
#endif
else {
std::apply([this, &container](auto &...args) { this->success_trigger_.trigger(container, args...); },
request.args);
}
// End it before the chain can start another request, so that one client is held at a time
if (container != nullptr) {
container->end();
container.reset();
}
// A stop from the triggers ended this run; a run started after it continues the chain instead
if (!request.cancelled)
std::apply([this](auto &...args) { this->play_next_(args...); }, request.args);
}
protected:
void encode_json_(Ts... x, JsonObject root) {
for (const auto &item : this->json_) {
auto val = item.second;
@@ -2,8 +2,13 @@
#ifdef USE_ESP32
#include <algorithm>
#include <cerrno>
#include <cstring>
#include <fcntl.h>
#include <sys/select.h>
#include "esphome/components/network/util.h"
#include "esphome/components/watchdog/watchdog.h"
@@ -20,6 +25,10 @@
namespace esphome::http_request {
ESPHOME_LOG_TAG(TAG, "http_request");
// Request body sent per esp_http_client_write() call, and the time one poll() spends sending it
static constexpr size_t SEND_CHUNK_SIZE = 1024;
static constexpr uint32_t SEND_BUDGET_MS = 10;
static constexpr uint32_t ERROR_DURATION_MS = 1000;
void HttpRequestIDF::dump_config() {
@@ -51,10 +60,9 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) {
return ESP_OK;
}
std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const char *url, const char *method, const std::string &body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers,
uint32_t timeout_ms) {
std::shared_ptr<HttpContainerIDF> HttpRequestIDF::create_container_(
const char *url, const char *method, const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers, uint32_t timeout_ms, [[maybe_unused]] bool async) {
if (!network::is_connected()) {
this->status_momentary_error(ERROR_DURATION_MS);
ESP_LOGE(TAG, "HTTP Request failed; Not connected to network");
@@ -106,11 +114,12 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const char *url, const ch
config.buffer_size = this->buffer_size_rx_;
config.buffer_size_tx = this->buffer_size_tx_;
const uint32_t start = millis();
const uint32_t watchdog_timeout = this->get_watchdog_timeout(timeout_ms);
watchdog::WatchdogManager wdm(watchdog_timeout);
config.event_handler = http_event_handler;
#ifdef USE_HTTP_REQUEST_IDF_SOCKET_POLL
// Only TLS has a non-blocking connect worth having: the handshake. A plain connection waits for
// the TCP connection either way.
config.is_async = async && secure;
#endif
esp_http_client_handle_t client = esp_http_client_init(&config);
if (client == nullptr) {
@@ -121,7 +130,9 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const char *url, const ch
std::shared_ptr<HttpContainerIDF> container = std::make_shared<HttpContainerIDF>(client);
container->set_parent(this);
container->set_watchdog_timeout(watchdog_timeout);
container->set_watchdog_timeout(this->get_watchdog_timeout(timeout_ms));
container->timeout_ms_ = timeout_ms;
container->async_ = config.is_async;
container->set_secure(secure);
@@ -132,101 +143,229 @@ std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const char *url, const ch
esp_http_client_set_header(client, header.name.c_str(), header.value.c_str());
}
const int body_len = body.length();
return container;
}
esp_err_t err = esp_http_client_open(client, body_len);
if (err != ESP_OK) {
this->status_momentary_error(ERROR_DURATION_MS);
ESP_LOGE(TAG, "HTTP Request failed: %s", esp_err_to_name(err));
esp_http_client_cleanup(client);
std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const char *url, const char *method, const std::string &body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers,
uint32_t timeout_ms) {
std::shared_ptr<HttpContainerIDF> container = this->start_container_(url, method, std::string(body), request_headers,
lower_case_collect_headers, timeout_ms, false);
if (container == nullptr) {
return nullptr;
}
// The same stages as begin(), run here until the response headers are in
watchdog::WatchdogManager wdm(container->get_watchdog_timeout());
HttpRequestProgress progress;
while ((progress = container->poll()) == HttpRequestProgress::HTTP_REQUEST_PROGRESS_PENDING) {
container->feed_wdt();
delay(1);
}
if (progress == HttpRequestProgress::HTTP_REQUEST_PROGRESS_FAILED) {
container->end();
return nullptr;
}
return container;
}
if (body_len > 0) {
int write_left = body_len;
int write_index = 0;
const char *buf = body.c_str();
while (write_left > 0) {
int written = esp_http_client_write(client, buf + write_index, write_left);
if (written <= 0) {
err = ESP_FAIL;
std::shared_ptr<HttpContainer> HttpRequestIDF::begin(const char *url, const char *method, std::string &&body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers,
uint32_t timeout_ms) {
return this->start_container_(url, method, std::move(body), request_headers, lower_case_collect_headers, timeout_ms,
true);
}
std::shared_ptr<HttpContainerIDF> HttpRequestIDF::start_container_(
const char *url, const char *method, std::string &&body, const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers, uint32_t timeout_ms, bool async) {
std::shared_ptr<HttpContainerIDF> container =
this->create_container_(url, method, request_headers, lower_case_collect_headers, timeout_ms, async);
if (container == nullptr) {
return nullptr;
}
container->body_ = std::move(body);
container->start_time_ = millis();
container->progress_time_ = container->start_time_;
container->redirects_left_ = this->follow_redirects_ ? this->redirect_limit_ : 0;
container->stage_ = HttpContainerIDF::Stage::STAGE_CONNECT;
return container;
}
HttpRequestIDF *HttpContainerIDF::idf_parent_() { return static_cast<HttpRequestIDF *>(this->parent_); }
HttpRequestProgress HttpContainerIDF::fail_(esp_err_t err) {
ESP_LOGE(TAG, "HTTP Request failed: %s", esp_err_to_name(err));
this->parent_->status_momentary_error(ERROR_DURATION_MS);
this->stage_ = Stage::STAGE_FAILED;
return HttpRequestProgress::HTTP_REQUEST_PROGRESS_FAILED;
}
HttpRequestProgress HttpContainerIDF::wait_() {
// millis(), not the loop time: progress_time_ is set after calls that block within this pass
if (millis() - this->progress_time_ >= this->timeout_ms_) {
return this->fail_(ESP_ERR_TIMEOUT);
}
return HttpRequestProgress::HTTP_REQUEST_PROGRESS_PENDING;
}
bool HttpContainerIDF::socket_ready_(bool write) {
#ifdef USE_HTTP_REQUEST_IDF_SOCKET_POLL
const int fd = esp_http_client_get_socket(this->client_);
if (fd < 0) {
return true;
}
fd_set fds;
FD_ZERO(&fds);
FD_SET(fd, &fds);
timeval no_wait{};
// An error also counts as ready, so that the call that follows reports it
// Qualified: esphome::select is a namespace
return ::select(fd + 1, write ? nullptr : &fds, write ? &fds : nullptr, nullptr, &no_wait) != 0;
#else
return true;
#endif
}
// Each stage either finishes, or returns PENDING after a call that did not wait. The waits left are
// the name lookup and TCP connection in the first esp_http_client_open() of each hop: a non-blocking
// esp-tls connect that times out once never completes, as select() clears the sets it keeps.
HttpRequestProgress HttpContainerIDF::poll() {
while (true) {
switch (this->stage_) {
case Stage::STAGE_DONE:
return HttpRequestProgress::HTTP_REQUEST_PROGRESS_DONE;
case Stage::STAGE_FAILED:
return HttpRequestProgress::HTTP_REQUEST_PROGRESS_FAILED;
case Stage::STAGE_CONNECT: {
// Only the first call of a hop waits
watchdog::WatchdogManager wdm(this->connecting_ ? 0 : this->get_watchdog_timeout());
esp_http_client_set_timeout_ms(this->client_, this->connecting_ ? 1 : this->timeout_ms_);
this->feed_wdt();
esp_err_t err = esp_http_client_open(this->client_, this->body_.size());
this->feed_wdt();
if (err == ESP_ERR_HTTP_EAGAIN) {
this->connecting_ = true;
return this->wait_();
}
this->connecting_ = false;
if (err != ESP_OK) {
return this->fail_(err);
}
#ifdef USE_HTTP_REQUEST_IDF_SOCKET_POLL
if (this->async_) {
// Back to a blocking socket, so that mbedtls waits for the rest of a record instead of
// failing; reads only start once the socket is readable
const int fd = esp_http_client_get_socket(this->client_);
const int flags = fd < 0 ? -1 : fcntl(fd, F_GETFL, 0);
if (flags < 0 || fcntl(fd, F_SETFL, flags & ~O_NONBLOCK) < 0) {
ESP_LOGE(TAG, "Could not make the socket blocking: fd %d, errno %d", fd, errno);
return this->fail_(ESP_FAIL);
}
}
#endif
esp_http_client_set_timeout_ms(this->client_, this->timeout_ms_);
this->progress_time_ = millis();
this->stage_ = this->body_sent_ < this->body_.size() ? Stage::STAGE_SEND_BODY : Stage::STAGE_HEADERS;
break;
}
write_left -= written;
write_index += written;
container->feed_wdt();
}
}
if (err != ESP_OK) {
this->status_momentary_error(ERROR_DURATION_MS);
ESP_LOGE(TAG, "HTTP Request failed: %s", esp_err_to_name(err));
esp_http_client_cleanup(client);
return nullptr;
}
container->feed_wdt();
// esp_http_client_fetch_headers() returns 0 for chunked transfer encoding (no Content-Length header).
// The read() method handles content_length == 0 specially to support chunked responses.
container->content_length = esp_http_client_fetch_headers(client);
container->set_chunked(esp_http_client_is_chunked_response(client));
container->feed_wdt();
container->status_code = esp_http_client_get_status_code(client);
container->feed_wdt();
container->duration_ms = millis() - start;
if (is_success(container->status_code)) {
return container;
}
if (this->follow_redirects_) {
auto num_redirects = this->redirect_limit_;
while (is_redirect(container->status_code) && num_redirects > 0) {
err = esp_http_client_set_redirection(client);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_http_client_set_redirection failed: %s", esp_err_to_name(err));
this->status_momentary_error(ERROR_DURATION_MS);
esp_http_client_cleanup(client);
return nullptr;
case Stage::STAGE_SEND_BODY: {
// esp_http_client_write() returns only once all it was given is sent, so give it a piece at a time
watchdog::WatchdogManager wdm(this->get_watchdog_timeout());
const uint32_t start = millis();
while (this->body_sent_ < this->body_.size()) {
if (!this->socket_ready_(true)) {
return this->wait_();
}
const size_t len = std::min(this->body_.size() - this->body_sent_, SEND_CHUNK_SIZE);
int written = esp_http_client_write(this->client_, this->body_.data() + this->body_sent_, len);
this->feed_wdt();
if (written < 0) {
return this->fail_(ESP_FAIL);
}
if (written == 0) {
return this->wait_();
}
this->body_sent_ += written;
this->progress_time_ = millis();
if (this->progress_time_ - start >= SEND_BUDGET_MS && this->body_sent_ < this->body_.size()) {
return HttpRequestProgress::HTTP_REQUEST_PROGRESS_PENDING;
}
}
this->body_ = std::string();
this->stage_ = Stage::STAGE_HEADERS;
break;
}
case Stage::STAGE_HEADERS: {
if (!this->socket_ready_(false)) {
if (millis() - this->progress_time_ < this->timeout_ms_) {
return HttpRequestProgress::HTTP_REQUEST_PROGRESS_PENDING;
}
// As perform(), where esp_http_client_fetch_headers() times out: no response is status -1
return this->response_failed_(-1);
}
// The response has begun to arrive, so this waits only for the rest of the headers, or over TLS
// the rest of the record that holds them
watchdog::WatchdogManager wdm(this->get_watchdog_timeout());
this->feed_wdt();
// esp_http_client_fetch_headers() returns 0 for chunked transfer encoding (no Content-Length header)
const int64_t length = esp_http_client_fetch_headers(this->client_);
this->feed_wdt();
this->duration_ms = millis() - this->start_time_;
if (length < 0) {
return this->response_failed_(-1);
}
this->content_length = length;
this->set_chunked(esp_http_client_is_chunked_response(this->client_));
this->status_code = esp_http_client_get_status_code(this->client_);
if (is_success(this->status_code)) {
this->stage_ = Stage::STAGE_DONE;
break;
}
if (this->redirects_left_ > 0 && is_redirect(this->status_code)) {
esp_err_t err = esp_http_client_set_redirection(this->client_);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_http_client_set_redirection failed: %s", esp_err_to_name(err));
return this->fail_(err);
}
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char redirect_url[256]{};
if (esp_http_client_get_url(client, redirect_url, sizeof(redirect_url) - 1) == ESP_OK) {
ESP_LOGV(TAG, "redirecting to url: %s", redirect_url);
}
char redirect_url[256]{};
if (esp_http_client_get_url(this->client_, redirect_url, sizeof(redirect_url) - 1) == ESP_OK) {
ESP_LOGV(TAG, "redirecting to url: %s", redirect_url);
}
#endif
err = esp_http_client_open(client, 0);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_http_client_open failed: %s", esp_err_to_name(err));
this->status_momentary_error(ERROR_DURATION_MS);
esp_http_client_cleanup(client);
return nullptr;
this->redirects_left_--;
// Collect only the final response's headers; redirects do not resend the body
this->response_headers_.clear();
this->body_ = std::string();
this->body_sent_ = 0;
this->progress_time_ = millis();
this->stage_ = Stage::STAGE_CONNECT;
break;
}
if (is_redirect(this->status_code) && this->idf_parent_()->follow_redirects_) {
ESP_LOGW(TAG, "Reach redirect limit count=%d", this->idf_parent_()->redirect_limit_);
}
return this->response_failed_(this->status_code);
}
container->feed_wdt();
// IDF is the only backend reusing the container across redirect hops;
// drop the previous hop's headers (Arduino/host collect only the final response)
container->response_headers_.clear();
container->content_length = esp_http_client_fetch_headers(client);
container->set_chunked(esp_http_client_is_chunked_response(client));
container->feed_wdt();
container->status_code = esp_http_client_get_status_code(client);
container->feed_wdt();
container->duration_ms = millis() - start;
if (is_success(container->status_code)) {
return container;
}
num_redirects--;
}
if (num_redirects == 0) {
ESP_LOGW(TAG, "Reach redirect limit count=%d", this->redirect_limit_);
}
}
}
ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url, container->status_code);
this->status_momentary_error(ERROR_DURATION_MS);
return container;
HttpRequestProgress HttpContainerIDF::response_failed_(int status_code) {
this->status_code = status_code;
this->duration_ms = millis() - this->start_time_;
char url[256]{};
esp_http_client_get_url(this->client_, url, sizeof(url) - 1);
ESP_LOGE(TAG, "HTTP Request failed; URL: %s; Code: %d", url, this->status_code);
this->parent_->status_momentary_error(ERROR_DURATION_MS);
this->stage_ = Stage::STAGE_DONE;
return HttpRequestProgress::HTTP_REQUEST_PROGRESS_DONE;
}
bool HttpContainerIDF::is_read_complete() const {
@@ -265,8 +404,21 @@ bool HttpContainerIDF::is_read_complete() const {
// after its internal transport timeout (configured via timeout_ms) expires.
// This is passed through as a negative return value, which callers treat as an error.
int HttpContainerIDF::read(uint8_t *buf, size_t max_len) {
const uint32_t start = millis();
watchdog::WatchdogManager wdm(this->get_watchdog_timeout());
esp_http_client_set_timeout_ms(this->client_, this->timeout_ms_);
return this->read_(buf, max_len);
}
int HttpContainerIDF::read_available(uint8_t *buf, size_t max_len) {
// A zero timeout only polls; TLS still waits for the rest of a record that has begun to arrive
watchdog::WatchdogManager wdm(this->get_watchdog_timeout());
esp_http_client_set_timeout_ms(this->client_, 0);
int read_len_or_error = this->read_(buf, max_len);
return read_len_or_error == -ESP_ERR_HTTP_EAGAIN ? 0 : read_len_or_error;
}
int HttpContainerIDF::read_(uint8_t *buf, size_t max_len) {
const uint32_t start = millis();
// Check if we've already read all expected content (non-chunked and no-body only).
// Use the base class check here, NOT the override: esp_http_client_is_complete_data_received()
@@ -5,30 +5,71 @@
#ifdef USE_ESP32
#include <esp_event.h>
#include <esp_idf_version.h>
#include <esp_http_client.h>
#include <esp_netif.h>
#include <esp_tls.h>
// The socket is needed to wait for the response without blocking; esp_http_client_get_socket() is new in 5.5.3
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 3)
#define USE_HTTP_REQUEST_IDF_SOCKET_POLL
#endif
namespace esphome::http_request {
class HttpRequestIDF;
class HttpContainerIDF : public HttpContainer {
public:
HttpContainerIDF(esp_http_client_handle_t client) : client_(client) {}
int read(uint8_t *buf, size_t max_len) override;
int read_available(uint8_t *buf, size_t max_len) override;
void end() override;
bool is_read_complete() const override;
HttpRequestProgress poll() override;
/// @brief Feeds the watchdog timer if the executing task has one attached
void feed_wdt();
protected:
friend class HttpRequestIDF;
enum class Stage : uint8_t {
STAGE_CONNECT,
STAGE_SEND_BODY,
STAGE_HEADERS,
STAGE_DONE,
STAGE_FAILED,
};
int read_(uint8_t *buf, size_t max_len);
/// True when the socket can be read or written without waiting; always true when it cannot be checked
bool socket_ready_(bool write);
HttpRequestProgress wait_();
HttpRequestProgress fail_(esp_err_t err);
/// Finish with an unsuccessful status, which is reported through on_response like a success
HttpRequestProgress response_failed_(int status_code);
HttpRequestIDF *idf_parent_();
esp_http_client_handle_t client_;
// Owned copy (not a reference): must outlive perform() for the response-header event handler
std::vector<std::string> collect_headers_;
// Request body for begin(), released once sent
std::string body_;
size_t body_sent_{0};
uint32_t timeout_ms_{0};
// Time of the last step forward, for the request timeout of begin()
uint32_t progress_time_{0};
uint32_t start_time_{0};
uint16_t redirects_left_{0};
Stage stage_{Stage::STAGE_DONE};
// An esp_http_client_open() of a non-blocking client returned ESP_ERR_HTTP_EAGAIN
bool connecting_{false};
bool async_{false};
};
class HttpRequestIDF final : public HttpRequestComponent {
friend class HttpContainerIDF;
public:
// User provided, not "= default": `new(p) HttpRequestIDF()` would zero-fill .bss that is already zero.
HttpRequestIDF() {}
@@ -40,7 +81,21 @@ class HttpRequestIDF final : public HttpRequestComponent {
void set_verify_ssl(bool verify_ssl) { this->verify_ssl_ = verify_ssl; }
void set_ca_certificate(const char *ca_certificate) { this->ca_certificate_ = ca_certificate; }
std::shared_ptr<HttpContainer> begin(const char *url, const char *method, std::string &&body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers,
uint32_t timeout_ms) override;
protected:
/// A container that poll() takes from the connect through to the response headers
std::shared_ptr<HttpContainerIDF> start_container_(const char *url, const char *method, std::string &&body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers,
uint32_t timeout_ms, bool async);
std::shared_ptr<HttpContainerIDF> create_container_(const char *url, const char *method,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers,
uint32_t timeout_ms, bool async);
std::shared_ptr<HttpContainer> perform(const char *url, const char *method, const std::string &body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers,
-1
View File
@@ -729,7 +729,6 @@ CONF_DISP_BG_COLOR = "disp_bg_color"
CONF_DISP_BG_IMAGE = "disp_bg_image"
CONF_DISP_BG_OPA = "disp_bg_opa"
CONF_BODY = "body"
CONF_BUTTONS = "buttons"
CONF_CHANGE_RATE = "change_rate"
CONF_CLICKABLE = "clickable"
CONF_CLOSE_BUTTON = "close_button"
@@ -1,6 +1,6 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components.const import CONF_ROWS
from esphome.components.const import CONF_BUTTONS, CONF_ROWS
from esphome.components.key_provider import KeyProvider
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_ITEMS, CONF_TEXT, CONF_WIDTH
@@ -10,7 +10,6 @@ from esphome.types import Expression
from ..automation import action_to_code
from ..defines import (
BUTTONMATRIX_CTRLS,
CONF_BUTTONS,
CONF_CONTROL,
CONF_KEY_CODE,
CONF_MAIN,
+1 -1
View File
@@ -1,4 +1,5 @@
from esphome import codegen as cg, config_validation as cv
from esphome.components.const import CONF_BUTTONS
from esphome.const import CONF_BUTTON, CONF_HIDDEN, CONF_ID, CONF_TEXT
from esphome.core import ID
from esphome.cpp_generator import MockObjClass
@@ -7,7 +8,6 @@ from esphome.cpp_types import nullptr
from ..defines import (
CONF_BODY,
CONF_BUTTON_STYLE,
CONF_BUTTONS,
CONF_CLOSE_BUTTON,
CONF_HEADER_BUTTONS,
CONF_MAIN,
-5
View File
@@ -60,11 +60,6 @@ optional<bool> Switch::get_initial_state_with_restore_mode() {
}
return initial_state;
}
optional<bool> Switch::get_initial_write_state() {
if (auto initial = this->get_initial_state_with_restore_mode())
return *initial != this->inverted_;
return {};
}
void Switch::restore() {
if (auto initial = this->get_initial_state_with_restore_mode())
this->control(*initial);
-2
View File
@@ -114,8 +114,6 @@ class Switch : public EntityBase {
* state.
*/
optional<bool> get_initial_state_with_restore_mode();
/// get_initial_state_with_restore_mode() with inverted applied: the level write_state() takes.
optional<bool> get_initial_write_state();
/// Drive the switch to the state its restore mode yields, through control(), so inversion and
/// write_state() apply; a disabled restore mode drives \p when_disabled, or nothing without one.
+3 -1
View File
@@ -296,7 +296,9 @@ def _external_manifests() -> list[dict[str, Any]]:
found[component_dir] = source
break
else:
if not component_dir.is_relative_to(loader.CORE_COMPONENTS_PATH):
# Built in means anywhere in the package: the esphome domain itself
# is esphome/core, not under esphome/components
if not component_dir.is_relative_to(loader.CORE_COMPONENTS_PATH.parent):
_LOGGER.warning(
"SBOM: component '%s' in %s matches no external component "
"source and is listed as part of ESPHome",
@@ -0,0 +1,12 @@
esphome:
name: test
esp32:
board: nodemcu-32s
wifi:
ssid: test
password: testtest
http_request:
max_concurrent_requests: 3
@@ -0,0 +1,20 @@
"""max_concurrent_requests is set only when it differs from the default of 1."""
from collections.abc import Callable
from pathlib import Path
def test_max_concurrent_requests_default_is_not_set(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
main_cpp = generate_main(component_config_path("test_esp32_stock.yaml"))
assert "set_max_concurrent_requests" not in main_cpp
def test_max_concurrent_requests_is_set(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
main_cpp = generate_main(component_config_path("max_concurrent.yaml"))
assert "->set_max_concurrent_requests(3);" in main_cpp
+11
View File
@@ -0,0 +1,11 @@
import esphome.codegen as cg
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
# The send action tests read the response body, which the real to_code enables for
# configs that capture it.
async def to_code_testing(config):
cg.add_define("USE_HTTP_REQUEST_RESPONSE")
manifest.to_code = to_code_testing
@@ -87,6 +87,7 @@ esphome:
http_request:
useragent: esphome/tagreader
timeout: 10s
max_concurrent_requests: 2
verify_ssl: ${verify_ssl}
output:
@@ -0,0 +1,612 @@
#include <gtest/gtest.h>
#include <algorithm>
#include <cstring>
#include <memory>
#include <string>
#include <vector>
#include "esphome/components/http_request/http_request.h"
#include "esphome/core/base_automation.h"
namespace esphome::http_request::testing {
// A response that stays pending for a number of polls, then serves its body in fixed chunks
class FakeContainer : public HttpContainer {
public:
HttpRequestProgress poll() override {
this->polls++;
if (this->poll_delay_ms > 0)
delay(this->poll_delay_ms);
if (this->pending_polls > 0) {
this->pending_polls--;
return HttpRequestProgress::HTTP_REQUEST_PROGRESS_PENDING;
}
return this->result;
}
int read(uint8_t *buf, size_t max_len) override { return this->read_available(buf, max_len); }
int read_available(uint8_t *buf, size_t max_len) override {
if (this->stall)
return 0;
if (this->bytes_read_ >= this->fail_at)
return -1;
size_t len = std::min({max_len, this->chunk, this->body.size() - this->bytes_read_});
std::memcpy(buf, this->body.data() + this->bytes_read_, len);
this->bytes_read_ += len;
return static_cast<int>(len);
}
void end() override { this->ends++; }
int pending_polls{0};
// Time each poll() blocks, like the first connect on ESP-IDF
uint32_t poll_delay_ms{0};
HttpRequestProgress result{HttpRequestProgress::HTTP_REQUEST_PROGRESS_DONE};
std::string body;
size_t chunk{512};
bool stall{false};
// Reads fail once this many bytes have been read
size_t fail_at{SIZE_MAX};
int polls{0};
int ends{0};
};
class FakeHttpRequest : public HttpRequestComponent {
public:
std::shared_ptr<HttpContainer> begin(const char *url, const char *method, std::string &&body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers,
uint32_t timeout_ms) override {
this->begins++;
if (this->refuse)
return nullptr;
auto container = std::make_shared<FakeContainer>();
container->status_code = 200;
container->pending_polls = this->pending_polls;
container->poll_delay_ms = this->poll_delay_ms;
container->body = this->response_body;
container->content_length = this->response_body.size();
this->containers.push_back(container);
return container;
}
int begins{0};
bool refuse{false};
int pending_polls{2};
uint32_t poll_delay_ms{0};
std::string response_body;
std::vector<std::shared_ptr<FakeContainer>> containers;
protected:
std::shared_ptr<HttpContainer> perform(const char *url, const char *method, const std::string &body,
const std::vector<Header> &request_headers,
const std::vector<std::string> &lower_case_collect_headers,
uint32_t timeout_ms) override {
return nullptr;
}
};
// The URL lambda must be stateless, so the stop it makes is set up through this
struct UrlStop {
Automation<> *target{nullptr};
int calls{0};
int stop_at_call{0};
};
UrlStop &url_stop() {
static UrlStop state;
return state;
}
const char *url_that_can_stop() {
UrlStop &state = url_stop();
if (++state.calls == state.stop_at_call)
state.target->stop();
return "http://example.invalid/";
}
class CountAction : public Action<> {
public:
explicit CountAction(int *count) : count_(count) {}
protected:
void play() override { (*this->count_)++; }
int *count_;
};
class HttpRequestSendActionTest : public ::testing::Test {
protected:
void SetUp() override {
this->action_.set_url([]() -> std::string { return "http://example.invalid/"; });
this->action_.set_method([]() -> const char * { return "GET"; });
this->action_.set_max_response_buffer_size(1024);
this->automation_.add_actions({&this->action_, &this->next_});
url_stop() = UrlStop{&this->automation_};
}
// One pass of the main loop
void loop_() {
LoopBlockingGuard guard{nullptr, nullptr, millis()};
this->http_.loop();
}
void run_until_idle_() {
for (int i = 0; i != 100 && this->automation_.is_running(); i++)
this->loop_();
}
void capture_response_() {
this->action_.set_capture_response([]() { return true; });
this->with_body_.add_action(&this->store_body_);
}
FakeHttpRequest http_;
HttpRequestSendAction<> action_{&this->http_};
int next_count_{0};
CountAction next_{&this->next_count_};
Trigger<> trigger_;
Automation<> automation_{&this->trigger_};
int responses_{0};
int errors_{0};
std::string body_;
Automation<std::shared_ptr<HttpContainer>> on_response_{this->action_.get_success_trigger()};
LambdaAction<std::shared_ptr<HttpContainer>> count_response_{
[this](const std::shared_ptr<HttpContainer> &) { this->responses_++; }};
Automation<> on_error_{this->action_.get_error_trigger()};
CountAction count_error_{&this->errors_};
Automation<std::shared_ptr<HttpContainer>, std::string &> with_body_{
this->action_.get_success_trigger_with_response()};
LambdaAction<std::shared_ptr<HttpContainer>, std::string &> store_body_{
[this](const std::shared_ptr<HttpContainer> &, std::string &body) {
this->responses_++;
this->body_ = body;
}};
public:
HttpRequestSendActionTest() {
this->on_response_.add_action(&this->count_response_);
this->on_error_.add_action(&this->count_error_);
}
};
TEST_F(HttpRequestSendActionTest, ChainWaitsForTheResponse) {
this->trigger_.trigger();
EXPECT_EQ(this->http_.begins, 1);
EXPECT_TRUE(this->automation_.is_running());
EXPECT_EQ(this->next_count_, 0);
this->loop_();
this->loop_();
EXPECT_EQ(this->next_count_, 0);
EXPECT_EQ(this->responses_, 0);
this->loop_();
EXPECT_EQ(this->responses_, 1);
EXPECT_EQ(this->errors_, 0);
EXPECT_EQ(this->next_count_, 1);
EXPECT_EQ(this->http_.containers[0]->ends, 1);
EXPECT_FALSE(this->automation_.is_running());
}
TEST_F(HttpRequestSendActionTest, RequestThatCannotStartFiresOnErrorAtOnce) {
this->http_.refuse = true;
this->trigger_.trigger();
EXPECT_EQ(this->errors_, 1);
EXPECT_EQ(this->responses_, 0);
EXPECT_EQ(this->next_count_, 1);
EXPECT_FALSE(this->automation_.is_running());
}
TEST_F(HttpRequestSendActionTest, FailedRequestFiresOnError) {
this->trigger_.trigger();
this->http_.containers[0]->result = HttpRequestProgress::HTTP_REQUEST_PROGRESS_FAILED;
this->run_until_idle_();
EXPECT_EQ(this->errors_, 1);
EXPECT_EQ(this->responses_, 0);
EXPECT_EQ(this->next_count_, 1);
EXPECT_EQ(this->http_.containers[0]->ends, 1);
}
TEST_F(HttpRequestSendActionTest, SecondTriggerWaitsForTheFirstRequest) {
this->trigger_.trigger();
this->trigger_.trigger();
EXPECT_EQ(this->http_.begins, 1);
this->loop_();
this->loop_();
EXPECT_EQ(this->http_.begins, 1);
// The first finishes, and the second starts on the next pass
this->loop_();
EXPECT_EQ(this->responses_, 1);
EXPECT_EQ(this->http_.begins, 1);
this->loop_();
EXPECT_EQ(this->http_.begins, 2);
this->run_until_idle_();
EXPECT_EQ(this->responses_, 2);
EXPECT_EQ(this->next_count_, 2);
}
TEST_F(HttpRequestSendActionTest, TriggersBeyondTheQueueAreDropped) {
for (int i = 0; i != 7; i++)
this->trigger_.trigger();
// One running, four waiting, two dropped with on_error
EXPECT_EQ(this->http_.begins, 1);
EXPECT_EQ(this->errors_, 2);
EXPECT_EQ(this->next_count_, 2);
this->run_until_idle_();
EXPECT_EQ(this->http_.begins, 5);
EXPECT_EQ(this->responses_, 5);
EXPECT_EQ(this->next_count_, 7);
}
TEST_F(HttpRequestSendActionTest, WaitingRequestThatCannotStartFiresOnError) {
this->trigger_.trigger();
this->trigger_.trigger();
this->http_.refuse = true;
this->run_until_idle_();
EXPECT_EQ(this->responses_, 1);
EXPECT_EQ(this->errors_, 1);
EXPECT_EQ(this->next_count_, 2);
}
TEST_F(HttpRequestSendActionTest, StopEndsRequestsWithoutTriggers) {
this->trigger_.trigger();
this->trigger_.trigger();
this->automation_.stop();
EXPECT_FALSE(this->automation_.is_running());
EXPECT_EQ(this->http_.containers[0]->ends, 1);
this->loop_();
this->loop_();
this->loop_();
EXPECT_EQ(this->http_.begins, 1);
EXPECT_EQ(this->responses_, 0);
EXPECT_EQ(this->errors_, 0);
EXPECT_EQ(this->next_count_, 0);
// The action is usable again after a stop
this->trigger_.trigger();
this->run_until_idle_();
EXPECT_EQ(this->responses_, 1);
EXPECT_EQ(this->next_count_, 1);
}
TEST_F(HttpRequestSendActionTest, StopFromOnResponseDropsTheRest) {
LambdaAction<std::shared_ptr<HttpContainer>> stop{
[this](const std::shared_ptr<HttpContainer> &) { this->automation_.stop(); }};
this->on_response_.add_action(&stop);
this->trigger_.trigger();
this->trigger_.trigger();
this->run_until_idle_();
EXPECT_EQ(this->responses_, 1);
EXPECT_EQ(this->http_.begins, 1);
EXPECT_EQ(this->next_count_, 0);
EXPECT_FALSE(this->automation_.is_running());
}
// A script in mode restart that executes itself from on_response
TEST_F(HttpRequestSendActionTest, RestartFromOnResponseContinuesOnlyTheNewRun) {
int restarts = 0;
LambdaAction<std::shared_ptr<HttpContainer>> restart{[this, &restarts](const std::shared_ptr<HttpContainer> &) {
if (restarts++ == 0) {
this->automation_.stop();
this->trigger_.trigger();
}
}};
this->on_response_.add_action(&restart);
this->trigger_.trigger();
this->loop_();
this->loop_();
this->loop_();
EXPECT_EQ(this->http_.begins, 2);
EXPECT_EQ(this->http_.containers[0]->ends, 1);
EXPECT_EQ(this->next_count_, 0);
EXPECT_TRUE(this->automation_.is_running());
this->run_until_idle_();
EXPECT_EQ(this->responses_, 2);
EXPECT_EQ(this->next_count_, 1);
EXPECT_FALSE(this->automation_.is_running());
}
TEST_F(HttpRequestSendActionTest, RestartFromOnErrorContinuesOnlyTheNewRun) {
LambdaAction<> restart{[this]() {
if (this->http_.refuse) {
this->http_.refuse = false;
this->automation_.stop();
this->trigger_.trigger();
}
}};
this->on_error_.add_action(&restart);
this->http_.refuse = true;
this->trigger_.trigger();
EXPECT_EQ(this->errors_, 1);
EXPECT_EQ(this->http_.begins, 2);
EXPECT_EQ(this->next_count_, 0);
EXPECT_TRUE(this->automation_.is_running());
this->run_until_idle_();
EXPECT_EQ(this->responses_, 1);
EXPECT_EQ(this->next_count_, 1);
EXPECT_FALSE(this->automation_.is_running());
}
TEST_F(HttpRequestSendActionTest, TriggerFromOnResponseStartsAtOnce) {
int retriggers = 0;
LambdaAction<std::shared_ptr<HttpContainer>> again{[this, &retriggers](const std::shared_ptr<HttpContainer> &) {
if (retriggers++ == 0)
this->trigger_.trigger();
}};
this->on_response_.add_action(&again);
this->trigger_.trigger();
this->loop_();
this->loop_();
this->loop_();
EXPECT_EQ(this->http_.begins, 2);
this->run_until_idle_();
EXPECT_EQ(this->responses_, 2);
EXPECT_EQ(this->next_count_, 2);
}
TEST_F(HttpRequestSendActionTest, CapturedBodyIsReadAcrossPasses) {
this->capture_response_();
this->http_.pending_polls = 0;
this->http_.response_body = std::string(900, 'a');
this->trigger_.trigger();
this->http_.containers[0]->chunk = 100;
this->run_until_idle_();
EXPECT_EQ(this->responses_, 1);
EXPECT_EQ(this->body_, std::string(900, 'a'));
EXPECT_EQ(this->next_count_, 1);
}
TEST_F(HttpRequestSendActionTest, CapturedBodyStopsAtTheBufferSize) {
this->capture_response_();
this->action_.set_max_response_buffer_size(300);
this->http_.response_body = std::string(1000, 'b');
this->trigger_.trigger();
this->run_until_idle_();
EXPECT_EQ(this->body_, std::string(300, 'b'));
}
TEST_F(HttpRequestSendActionTest, StalledBodyEndsAtTheTimeout) {
this->capture_response_();
this->action_.set_timeout([]() -> uint32_t { return 20; });
this->http_.response_body = std::string(1000, 'c');
this->trigger_.trigger();
this->http_.containers[0]->stall = true;
this->loop_();
this->loop_();
this->loop_();
EXPECT_TRUE(this->automation_.is_running());
delay(30);
this->loop_();
EXPECT_FALSE(this->automation_.is_running());
EXPECT_EQ(this->responses_, 1);
EXPECT_TRUE(this->body_.empty());
}
// A url lambda that stops the action while the request is being started
TEST_F(HttpRequestSendActionTest, StopFromLambdaWhileStartingEndsTheRequest) {
this->action_.set_url([]() -> std::string { return url_that_can_stop(); });
url_stop().stop_at_call = 1;
this->trigger_.trigger();
EXPECT_FALSE(this->automation_.is_running());
ASSERT_EQ(this->http_.begins, 1);
EXPECT_EQ(this->http_.containers[0]->ends, 1);
this->run_until_idle_();
EXPECT_EQ(this->responses_, 0);
EXPECT_EQ(this->errors_, 0);
EXPECT_EQ(this->next_count_, 0);
this->trigger_.trigger();
this->run_until_idle_();
EXPECT_EQ(this->responses_, 1);
EXPECT_EQ(this->next_count_, 1);
}
TEST_F(HttpRequestSendActionTest, StopFromLambdaOfAWaitingRequestEndsIt) {
this->action_.set_url([]() -> std::string { return url_that_can_stop(); });
url_stop().stop_at_call = 2;
this->trigger_.trigger();
this->trigger_.trigger();
this->run_until_idle_();
EXPECT_FALSE(this->automation_.is_running());
ASSERT_EQ(this->http_.begins, 2);
EXPECT_EQ(this->http_.containers[1]->polls, 0);
EXPECT_EQ(this->http_.containers[1]->ends, 1);
EXPECT_EQ(this->responses_, 1);
EXPECT_EQ(this->next_count_, 1);
// The slot counts survived the stop, so a new trigger starts at once
this->trigger_.trigger();
EXPECT_EQ(this->http_.begins, 3);
this->run_until_idle_();
EXPECT_EQ(this->responses_, 2);
EXPECT_EQ(this->next_count_, 2);
}
TEST_F(HttpRequestSendActionTest, ReadErrorKeepsTheBodyReadSoFar) {
this->capture_response_();
this->http_.response_body = std::string(800, 'd');
this->trigger_.trigger();
this->http_.containers[0]->chunk = 100;
this->http_.containers[0]->fail_at = 300;
this->run_until_idle_();
EXPECT_EQ(this->responses_, 1);
EXPECT_EQ(this->errors_, 0);
EXPECT_EQ(this->body_, std::string(300, 'd'));
EXPECT_EQ(this->next_count_, 1);
}
TEST_F(HttpRequestSendActionTest, BufferThatCannotBeAllocatedGivesAnEmptyBody) {
this->capture_response_();
this->action_.set_max_response_buffer_size(SIZE_MAX);
this->http_.response_body = "unread";
this->body_ = "not set";
this->trigger_.trigger();
this->run_until_idle_();
EXPECT_EQ(this->responses_, 1);
EXPECT_TRUE(this->body_.empty());
EXPECT_EQ(this->next_count_, 1);
}
// A forced reboot calls only on_shutdown(), which finishes the requests in progress
TEST_F(HttpRequestSendActionTest, ShutdownFinishesTheRequestInProgress) {
this->trigger_.trigger();
this->trigger_.trigger();
EXPECT_EQ(this->responses_, 0);
this->http_.on_shutdown();
EXPECT_EQ(this->responses_, 2);
EXPECT_EQ(this->next_count_, 2);
EXPECT_FALSE(this->automation_.is_running());
}
// An on_shutdown automation that runs after the component's on_shutdown()
TEST_F(HttpRequestSendActionTest, RequestAfterShutdownFinishesAtOnce) {
this->http_.on_shutdown();
this->trigger_.trigger();
EXPECT_EQ(this->responses_, 1);
EXPECT_EQ(this->next_count_, 1);
EXPECT_FALSE(this->automation_.is_running());
}
TEST_F(HttpRequestSendActionTest, BodyTimeoutStartsAfterABlockingPoll) {
this->capture_response_();
this->action_.set_timeout([]() -> uint32_t { return 20; });
this->http_.pending_polls = 0;
this->http_.poll_delay_ms = 30;
this->http_.response_body = std::string(100, 'e');
this->trigger_.trigger();
this->http_.containers[0]->stall = true;
this->loop_();
EXPECT_TRUE(this->automation_.is_running());
this->http_.containers[0]->stall = false;
this->run_until_idle_();
EXPECT_EQ(this->body_, std::string(100, 'e'));
}
TEST_F(HttpRequestSendActionTest, DurationCoversTheWholeBody) {
this->capture_response_();
this->http_.pending_polls = 0;
this->http_.response_body = std::string(100, 'f');
this->trigger_.trigger();
auto container = this->http_.containers[0];
container->duration_ms = 1;
container->stall = true;
this->loop_();
delay(30);
container->stall = false;
this->run_until_idle_();
EXPECT_EQ(this->responses_, 1);
EXPECT_GE(container->duration_ms, 30u);
}
// The limit is per http_request component, so that one client is held at a time
TEST_F(HttpRequestSendActionTest, TwoActionsTakeTurns) {
HttpRequestSendAction<> other{&this->http_};
other.set_url([]() -> std::string { return "http://example.invalid/other"; });
other.set_method([]() -> const char * { return "GET"; });
int other_next = 0;
CountAction count_other{&other_next};
Trigger<> other_trigger;
Automation<> other_automation{&other_trigger};
other_automation.add_actions({&other, &count_other});
this->trigger_.trigger();
other_trigger.trigger();
EXPECT_EQ(this->http_.begins, 1);
this->loop_();
this->loop_();
this->loop_();
EXPECT_EQ(this->next_count_, 1);
EXPECT_EQ(other_next, 0);
EXPECT_EQ(this->http_.begins, 1);
for (int i = 0; i != 10 && other_automation.is_running(); i++)
this->loop_();
EXPECT_EQ(this->http_.begins, 2);
EXPECT_EQ(other_next, 1);
}
// Stopping one action leaves the requests of another alone
TEST_F(HttpRequestSendActionTest, StopEndsOnlyThatActionsRequests) {
HttpRequestSendAction<> other{&this->http_};
other.set_url([]() -> std::string { return "http://example.invalid/other"; });
other.set_method([]() -> const char * { return "GET"; });
int other_next = 0;
CountAction count_other{&other_next};
Trigger<> other_trigger;
Automation<> other_automation{&other_trigger};
other_automation.add_actions({&other, &count_other});
other_trigger.trigger();
this->trigger_.trigger();
this->automation_.stop();
for (int i = 0; i != 10 && other_automation.is_running(); i++)
this->loop_();
EXPECT_EQ(other_next, 1);
EXPECT_EQ(this->next_count_, 0);
EXPECT_EQ(this->http_.begins, 1);
}
// A safe shutdown drops the requests already made, so they do not hold up the reboot,
// and still sends the ones that on_shutdown automations make
TEST_F(HttpRequestSendActionTest, SafeShutdownSendsOnlyTheShutdownRequests) {
this->trigger_.trigger();
this->trigger_.trigger();
this->http_.on_safe_shutdown();
EXPECT_EQ(this->http_.containers[0]->ends, 1);
EXPECT_EQ(this->responses_, 0);
EXPECT_EQ(this->errors_, 0);
// Stopped like script.stop, so a mode single script could run it again from on_shutdown
EXPECT_FALSE(this->automation_.is_running());
this->trigger_.trigger();
EXPECT_EQ(this->responses_, 1);
this->http_.on_shutdown();
EXPECT_EQ(this->http_.begins, 2);
EXPECT_EQ(this->responses_, 1);
EXPECT_EQ(this->next_count_, 1);
}
TEST_F(HttpRequestSendActionTest, MaxConcurrentRequestsRunTogether) {
this->http_.set_max_concurrent_requests(2);
this->trigger_.trigger();
this->trigger_.trigger();
this->trigger_.trigger();
EXPECT_EQ(this->http_.begins, 2);
// Both finish on the third pass; the third request takes a free slot on the next
this->loop_();
this->loop_();
this->loop_();
EXPECT_EQ(this->responses_, 2);
EXPECT_EQ(this->http_.begins, 2);
this->loop_();
EXPECT_EQ(this->http_.begins, 3);
this->run_until_idle_();
EXPECT_EQ(this->responses_, 3);
EXPECT_EQ(this->next_count_, 3);
}
TEST_F(HttpRequestSendActionTest, MaxConcurrentRequestsKeepsTheQueueLimit) {
this->http_.set_max_concurrent_requests(2);
for (int i = 0; i != 8; i++)
this->trigger_.trigger();
// Two running, four waiting, two dropped
EXPECT_EQ(this->http_.begins, 2);
EXPECT_EQ(this->errors_, 2);
this->run_until_idle_();
EXPECT_EQ(this->responses_, 6);
EXPECT_EQ(this->next_count_, 8);
}
} // namespace esphome::http_request::testing
-12
View File
@@ -46,16 +46,4 @@ TEST(SwitchRestore, DefaultAppliesOnlyWhenTheModeIsDisabled) {
EXPECT_EQ(on.writes, std::vector<bool>{true});
}
TEST(SwitchRestore, InitialWriteStateAppliesInversion) {
RecordingSwitch on, inverted, disabled;
on.set_restore_mode(SWITCH_ALWAYS_ON);
inverted.set_restore_mode(SWITCH_ALWAYS_ON);
inverted.set_inverted(true);
disabled.set_restore_mode(SWITCH_RESTORE_DISABLED);
disabled.set_inverted(true);
EXPECT_EQ(on.get_initial_write_state(), optional<bool>(true));
EXPECT_EQ(inverted.get_initial_write_state(), optional<bool>(false));
EXPECT_FALSE(disabled.get_initial_write_state().has_value());
}
} // namespace esphome::switch_::testing
+13 -2
View File
@@ -386,7 +386,7 @@ def test_external_manifests_skip_other_components(
components_dir = tmp_path / "components"
get_sources().append(ExternalSource(components_dir=components_dir))
_add_external_component(components_dir, "widget")
_add_external_component(tmp_path / "builtin", "wifi")
_add_external_component(tmp_path / "pkg" / "components", "wifi")
CORE.loaded_integrations.update({"missing", "namespace"})
sbom.loader._COMPONENT_CACHE["namespace"] = SimpleNamespace(
module=SimpleNamespace()
@@ -399,7 +399,9 @@ def test_external_manifests_skip_other_components(
side_effect=sbom.loader._COMPONENT_CACHE.get,
),
patch.object(
sbom.loader, "CORE_COMPONENTS_PATH", (tmp_path / "builtin").resolve()
sbom.loader,
"CORE_COMPONENTS_PATH",
(tmp_path / "pkg" / "components").resolve(),
),
):
manifests = sbom._external_manifests()
@@ -703,3 +705,12 @@ def test_checkout_remote_detached(tmp_path: Path) -> None:
"https://user:token@github.com/someone/esphome.git",
)
assert not any(call.args[0][1] == "config" for call in run.call_args_list)
def test_external_manifests_esphome_core_is_built_in(
caplog: pytest.LogCaptureFixture,
) -> None:
"""The esphome domain resolves to esphome/core, which is still part of ESPHome."""
CORE.loaded_integrations.add("esphome")
assert sbom._external_manifests() == []
assert "SBOM" not in caplog.text