mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 02:02:51 +00:00
[http_request] Keep action request headers in a shared flash table (#20225)
This commit is contained in:
@@ -39,6 +39,7 @@ HttpRequestIDF = http_request_ns.class_("HttpRequestIDF", HttpRequestComponent)
|
||||
HttpRequestHost = http_request_ns.class_("HttpRequestHost", HttpRequestComponent)
|
||||
|
||||
HttpContainer = http_request_ns.class_("HttpContainer")
|
||||
RequestHeader = http_request_ns.struct("RequestHeader")
|
||||
|
||||
HttpRequestSendAction = http_request_ns.class_(
|
||||
"HttpRequestSendAction", automation.Action
|
||||
@@ -360,12 +361,27 @@ async def http_request_action_to_code(
|
||||
for key in json_:
|
||||
template_ = await cg.templatable(json_[key], args, cg.std_string)
|
||||
cg.add(var.add_json(key, template_))
|
||||
request_headers = config.get(CONF_REQUEST_HEADERS, {})
|
||||
if request_headers:
|
||||
cg.add(var.init_request_headers(len(request_headers)))
|
||||
for key, value in request_headers.items():
|
||||
template_ = await cg.templatable(value, args, cg.const_char_ptr)
|
||||
cg.add(var.add_request_header(key, template_))
|
||||
if request_headers := config.get(CONF_REQUEST_HEADERS):
|
||||
# A user lambda may keep static state, so only all-constant lists are shared
|
||||
share = not any(map(cg.is_template, request_headers.values()))
|
||||
# Constant headers ignore their arguments, so drop the names: identical lists
|
||||
# from scripts with different parameter names then render the same table.
|
||||
header_args = [(arg_type, "") for arg_type, _ in args] if share else args
|
||||
headers = [
|
||||
cg.StructInitializer(
|
||||
"",
|
||||
("name", key),
|
||||
("value", await cg.templatable(value, header_args, cg.const_char_ptr)),
|
||||
)
|
||||
for key, value in request_headers.items()
|
||||
]
|
||||
table = cg.shared_progmem_array(
|
||||
"http_request_headers",
|
||||
RequestHeader.template(*[arg_type for arg_type, _ in args]),
|
||||
headers,
|
||||
share=share,
|
||||
)
|
||||
cg.add(var.set_request_headers(table, len(headers)))
|
||||
|
||||
for value in config.get(CONF_COLLECT_HEADERS, []):
|
||||
cg.add(var.add_collect_header(value.lower()))
|
||||
|
||||
@@ -22,6 +22,12 @@ struct Header {
|
||||
std::string value;
|
||||
};
|
||||
|
||||
/// One request header of an action; codegen puts these in a shared flash table.
|
||||
template<typename... Ts> struct RequestHeader {
|
||||
const char *name;
|
||||
const char *(*value)(Ts...);
|
||||
};
|
||||
|
||||
// Some common HTTP status codes
|
||||
enum HttpStatus {
|
||||
HTTP_STATUS_OK = 200,
|
||||
@@ -492,9 +498,12 @@ template<typename... Ts> class HttpRequestSendAction final : public Action<Ts...
|
||||
TEMPLATABLE_VALUE(bool, capture_response)
|
||||
#endif
|
||||
|
||||
void init_request_headers(size_t count) { this->request_headers_.init(count); }
|
||||
void add_request_header(const char *key, TemplatableFn<const char *, Ts...> value) {
|
||||
this->request_headers_.push_back({key, value});
|
||||
static_assert(std::is_trivially_copyable_v<RequestHeader<Ts...>> &&
|
||||
sizeof(RequestHeader<Ts...>) % sizeof(uint32_t) == 0,
|
||||
"ESP8266 reads request headers from flash in whole words");
|
||||
/// Codegen only: the table must outlive the action.
|
||||
void set_request_headers(const RequestHeader<Ts...> *headers, size_t count) {
|
||||
this->request_headers_ = ConstVector<RequestHeader<Ts...>>(headers, count);
|
||||
}
|
||||
|
||||
void add_collect_header(const char *value) { this->lower_case_collect_headers_.emplace_back(value); }
|
||||
@@ -530,8 +539,8 @@ template<typename... Ts> class HttpRequestSendAction final : public Action<Ts...
|
||||
}
|
||||
std::vector<Header> request_headers;
|
||||
request_headers.reserve(this->request_headers_.size());
|
||||
for (const auto &[key, val] : this->request_headers_) {
|
||||
request_headers.push_back({key, val.value(x...)});
|
||||
for (const auto &header : this->request_headers_) {
|
||||
request_headers.push_back({header.name, header.value(x...)});
|
||||
}
|
||||
|
||||
auto container = this->parent_->start(this->url_.value(x...).c_str(), this->method_.value(x...), body,
|
||||
@@ -596,7 +605,7 @@ template<typename... Ts> class HttpRequestSendAction final : public Action<Ts...
|
||||
}
|
||||
}
|
||||
HttpRequestComponent *parent_;
|
||||
FixedVector<std::pair<const char *, TemplatableFn<const char *, Ts...>>> request_headers_{};
|
||||
ConstVector<RequestHeader<Ts...>> request_headers_{};
|
||||
std::vector<std::string> lower_case_collect_headers_{"content-type", "content-length"};
|
||||
FixedVector<std::pair<const char *, TemplatableValue<std::string, Ts...>>> json_{};
|
||||
std::function<void(Ts..., JsonObject)> json_func_{nullptr};
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
wifi:
|
||||
ssid: test
|
||||
password: testtest
|
||||
|
||||
http_request:
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
id: shared_a
|
||||
on_press:
|
||||
- http_request.get:
|
||||
url: http://example.com
|
||||
request_headers:
|
||||
Accept: application/json
|
||||
X-Mode: a
|
||||
- platform: template
|
||||
id: shared_b
|
||||
on_press:
|
||||
- http_request.get:
|
||||
url: http://example.com
|
||||
request_headers:
|
||||
Accept: application/json
|
||||
X-Mode: a
|
||||
- platform: template
|
||||
id: other
|
||||
on_press:
|
||||
- http_request.get:
|
||||
url: http://example.com
|
||||
request_headers:
|
||||
Accept: text/plain
|
||||
- platform: template
|
||||
id: lambda_a
|
||||
on_press:
|
||||
- http_request.get:
|
||||
url: http://example.com
|
||||
request_headers:
|
||||
Accept: application/json
|
||||
X-Mode: !lambda return "a";
|
||||
- platform: template
|
||||
id: lambda_b
|
||||
on_press:
|
||||
- http_request.get:
|
||||
url: http://example.com
|
||||
request_headers:
|
||||
Accept: application/json
|
||||
X-Mode: !lambda return "a";
|
||||
|
||||
script:
|
||||
- id: param_a
|
||||
parameters:
|
||||
level: int
|
||||
then:
|
||||
- http_request.get:
|
||||
url: http://example.com
|
||||
request_headers:
|
||||
Accept: application/json
|
||||
X-Mode: a
|
||||
- id: param_b
|
||||
parameters:
|
||||
value: int
|
||||
then:
|
||||
- http_request.get:
|
||||
url: http://example.com
|
||||
request_headers:
|
||||
Accept: application/json
|
||||
X-Mode: a
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Request headers are PROGMEM tables, shared only between all-constant lists."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
|
||||
def test_request_headers_share_constant_tables(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
main_cpp = generate_main(component_config_path("request_headers.yaml"))
|
||||
|
||||
# Actions in config order: the five buttons, then the two scripts
|
||||
tables = [
|
||||
(table, int(count))
|
||||
for table, count in re.findall(
|
||||
r"->set_request_headers\((\w+), (\d+)\);", main_cpp
|
||||
)
|
||||
]
|
||||
assert len(tables) == 7
|
||||
shared_a, shared_b, other, lambda_a, lambda_b, param_a, param_b = tables
|
||||
assert shared_a == shared_b
|
||||
assert shared_a[1] == 2
|
||||
assert other[1] == 1
|
||||
# A list with a lambda may keep static state, so it never shares even with an identical one
|
||||
assert lambda_a[0] != lambda_b[0]
|
||||
assert len({shared_a[0], other[0], lambda_a[0], lambda_b[0]}) == 4
|
||||
for table, _ in tables:
|
||||
assert re.search(rf"\b{table}\[\] PROGMEM = \{{", main_cpp)
|
||||
# Constant headers ignore their arguments, so scripts whose parameters differ
|
||||
# only by name share a table, and it is the only one of its argument type
|
||||
assert param_a == param_b
|
||||
assert param_a[0] not in {shared_a[0], other[0], lambda_a[0], lambda_b[0]}
|
||||
assert re.findall(r"RequestHeader<[^>]+> (\w+)\[\] PROGMEM = \{", main_cpp) == [
|
||||
param_a[0]
|
||||
]
|
||||
Reference in New Issue
Block a user