mirror of
https://github.com/esphome/esphome.git
synced 2026-10-24 06:55:37 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2cbc1bc02d | ||
|
|
701e5746b0 | ||
|
|
9d30bcc721 |
No files matched your search
@@ -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`)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in new issue
Block a user