[web_server_idf] Send events straight from the caller's buffers instead of a std::string (#19376)

This commit is contained in:
J. Nick Koston
2026-09-30 11:10:40 +13:00
committed by GitHub
parent 99c7f9e625
commit 5502f620d4
9 changed files with 850 additions and 503 deletions
+20 -12
View File
@@ -1,4 +1,6 @@
#include "json_util.h"
#include <cstring>
#include "esphome/core/log.h"
// ArduinoJson::Allocator is included via ArduinoJson.h in json_util.h
@@ -68,6 +70,22 @@ JsonDocument parse_json(const uint8_t *data, size_t len) {
JsonBuilder::JsonBuilder() = default;
size_t JsonBuilder::serialize_to(char *buf, size_t cap) {
if (doc_.overflowed()) {
ESP_LOGE(TAG, "JSON document overflow");
// Same contract as serializeJson; written by hand so no "{}" literal lives in RAM on ESP8266
size_t n = 0;
if (n < cap)
buf[n++] = '{';
if (n < cap)
buf[n++] = '}';
if (n < cap)
buf[n] = '\0';
return n;
}
return serializeJson(doc_, buf, cap);
}
SerializationBuffer<> JsonBuilder::serialize() {
// ===========================================================================================
// CRITICAL: NRVO (Named Return Value Optimization) - DO NOT REFACTOR WITHOUT UNDERSTANDING
@@ -109,17 +127,7 @@ SerializationBuffer<> JsonBuilder::serialize() {
constexpr size_t buf_size = SerializationBuffer<>::BUFFER_SIZE;
SerializationBuffer<> result(buf_size - 1); // Max content size (reserve 1 for null)
if (doc_.overflowed()) {
ESP_LOGE(TAG, "JSON document overflow");
auto *buf = result.data_writable_();
buf[0] = '{';
buf[1] = '}';
buf[2] = '\0';
result.set_size_(2);
return result;
}
size_t size = serializeJson(doc_, result.data_writable_(), buf_size);
size_t size = this->serialize_to(result.data_writable_(), buf_size);
if (size < buf_size) {
// Fits in stack buffer - update size to actual length
result.set_size_(size);
@@ -134,7 +142,7 @@ SerializationBuffer<> JsonBuilder::serialize() {
size_t heap_size = buf_size * 2;
while (heap_size <= max_heap_size) {
result.reallocate_heap_(heap_size - 1);
size = serializeJson(doc_, result.data_writable_(), heap_size);
size = this->serialize_to(result.data_writable_(), heap_size);
if (size < heap_size) {
result.set_size_(size);
return result;
+4
View File
@@ -179,6 +179,10 @@ class JsonBuilder {
return root_;
}
/// Serialize into a caller owned buffer. Returns the length; cap or more means it did not fit
/// and buf holds a truncated, unterminated copy. An overflowed document serializes as "{}".
size_t serialize_to(char *buf, size_t cap);
/// Serialize the JSON document to a SerializationBuffer (stack-first allocation)
/// Uses 512-byte stack buffer by default, falls back to heap for larger JSON
SerializationBuffer<> serialize();
+215 -239
View File
@@ -1,4 +1,6 @@
#include "web_server.h"
#include <algorithm>
#ifdef USE_WEBSERVER
#include "esphome/components/json/json_util.h"
#include "esphome/core/progmem.h"
@@ -58,6 +60,12 @@ static const char *const TAG = "web_server";
// View a state LogString as a ProgmemStr so ArduinoJson serializes it PROGMEM-aware on ESP8266.
[[maybe_unused]] static ProgmemStr json_state_str(const LogString *s) { return reinterpret_cast<ProgmemStr>(s); }
// Out of line: every GET handler ends with this
[[maybe_unused]] static void send_json(AsyncWebServerRequest *request, json::JsonBuilder &builder) {
auto data = builder.serialize();
request->send(200, ESPHOME_F("application/json"), data.c_str());
}
// Parse URL and return match info
// URL formats (disambiguated by HTTP method for 3-segment case):
// GET /{domain}/{entity_name} - main device state
@@ -193,13 +201,15 @@ DeferredUpdateEventSource::deq_push_back_with_dedup_(void *source, message_gener
void DeferredUpdateEventSource::process_deferred_queue_() {
while (!deferred_queue_.empty()) {
DeferredEvent &de = deferred_queue_.front();
auto message = de.message_generator_(web_server_, de.source_);
json::JsonBuilder builder;
de.message_generator_(web_server_, de.source_, builder);
auto message = builder.serialize();
if (this->send(message.c_str(), "state") != DISCARDED) {
// O(n) but memory efficiency is more important than speed here which is why std::vector was chosen
deferred_queue_.erase(deferred_queue_.begin());
this->consecutive_send_failures_ = 0; // Reset failure count on successful send
} else {
// NOTE: Similar logic exists in web_server_idf/web_server_idf.cpp in AsyncEventSourceResponse::process_buffer_().
// NOTE: Similar logic exists in web_server_idf/web_server_idf.cpp in AsyncEventSourceResponse::drain_tail_().
// The close mechanisms are platform-specific (this path calls close() directly; the IDF path is time-based and
// closes through HTTPD to preserve session ownership), but both drop a client after roughly 20 seconds without
// send progress. Keep that stall policy in sync when changing either side.
@@ -250,7 +260,9 @@ void DeferredUpdateEventSource::deferrable_send_state(void *source, const char *
// deferred queue still not empty which means downstream event queue full, no point trying to send first
deq_push_back_with_dedup_(source, message_generator);
} else {
auto message = message_generator(web_server_, source);
json::JsonBuilder builder;
message_generator(web_server_, source, builder);
auto message = builder.serialize();
if (this->send(message.c_str(), "state") == DISCARDED) {
deq_push_back_with_dedup_(source, message_generator);
} else {
@@ -405,10 +417,11 @@ void WebServer::loop() {
}
#ifdef USE_LOGGER
void WebServer::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) {
(void) level;
(void) tag;
this->events_.try_send_nodefer(message, message_len, "log", millis());
this->events_.try_send_nodefer(message, std::min(message_len, LOG_EVENT_MAX_LEN), "log", millis());
}
#endif
@@ -634,21 +647,21 @@ void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlM
// Note: request->method() is always HTTP_GET here (canHandle ensures this)
if (entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->sensor_json_(obj, obj->state, detail);
request->send(200, "application/json", data.c_str());
json::JsonBuilder builder;
this->sensor_json_(obj, obj->state, detail, builder);
send_json(request, builder);
return;
}
}
request->send(404);
}
json::SerializationBuffer<> WebServer::sensor_state_json_generator(WebServer *web_server, void *source) {
return web_server->sensor_json_((sensor::Sensor *) (source), ((sensor::Sensor *) (source))->state, DETAIL_STATE);
void WebServer::sensor_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
web_server->sensor_json_((sensor::Sensor *) (source), ((sensor::Sensor *) (source))->state, DETAIL_STATE, builder);
}
json::SerializationBuffer<> WebServer::sensor_all_json_generator(WebServer *web_server, void *source) {
return web_server->sensor_json_((sensor::Sensor *) (source), ((sensor::Sensor *) (source))->state, DETAIL_ALL);
void WebServer::sensor_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
web_server->sensor_json_((sensor::Sensor *) (source), ((sensor::Sensor *) (source))->state, DETAIL_ALL, builder);
}
json::SerializationBuffer<> WebServer::sensor_json_(sensor::Sensor *obj, float value, JsonDetail start_config) {
json::JsonBuilder builder;
void WebServer::sensor_json_(sensor::Sensor *obj, float value, JsonDetail start_config, json::JsonBuilder &builder) {
JsonObject root = builder.root();
const auto uom_ref = obj->get_unit_of_measurement_ref();
@@ -662,8 +675,6 @@ json::SerializationBuffer<> WebServer::sensor_json_(sensor::Sensor *obj, float v
if (!uom_ref.empty())
root[ESPHOME_F("uom")] = uom_ref.c_str();
}
return builder.serialize();
}
#endif
@@ -681,32 +692,30 @@ void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const
// Note: request->method() is always HTTP_GET here (canHandle ensures this)
if (entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->text_sensor_json_(obj, obj->state, detail);
request->send(200, "application/json", data.c_str());
json::JsonBuilder builder;
this->text_sensor_json_(obj, obj->state, detail, builder);
send_json(request, builder);
return;
}
}
request->send(404);
}
json::SerializationBuffer<> WebServer::text_sensor_state_json_generator(WebServer *web_server, void *source) {
return web_server->text_sensor_json_((text_sensor::TextSensor *) (source),
((text_sensor::TextSensor *) (source))->state, DETAIL_STATE);
void WebServer::text_sensor_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
web_server->text_sensor_json_((text_sensor::TextSensor *) (source), ((text_sensor::TextSensor *) (source))->state,
DETAIL_STATE, builder);
}
json::SerializationBuffer<> WebServer::text_sensor_all_json_generator(WebServer *web_server, void *source) {
return web_server->text_sensor_json_((text_sensor::TextSensor *) (source),
((text_sensor::TextSensor *) (source))->state, DETAIL_ALL);
void WebServer::text_sensor_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
web_server->text_sensor_json_((text_sensor::TextSensor *) (source), ((text_sensor::TextSensor *) (source))->state,
DETAIL_ALL, builder);
}
json::SerializationBuffer<> WebServer::text_sensor_json_(text_sensor::TextSensor *obj, const std::string &value,
JsonDetail start_config) {
json::JsonBuilder builder;
void WebServer::text_sensor_json_(text_sensor::TextSensor *obj, const std::string &value, JsonDetail start_config,
json::JsonBuilder &builder) {
JsonObject root = builder.root();
set_json_icon_state_value(root, obj, "text_sensor", value.c_str(), value.c_str(), start_config);
if (start_config == DETAIL_ALL) {
this->add_sorting_info_(root, obj);
}
return builder.serialize();
}
#endif
@@ -742,8 +751,9 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->switch_json_(obj, obj->state, detail);
request->send(200, "application/json", data.c_str());
json::JsonBuilder builder;
this->switch_json_(obj, obj->state, detail, builder);
send_json(request, builder);
return;
}
@@ -767,14 +777,13 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM
}
request->send(404);
}
json::SerializationBuffer<> WebServer::switch_state_json_generator(WebServer *web_server, void *source) {
return web_server->switch_json_((switch_::Switch *) (source), ((switch_::Switch *) (source))->state, DETAIL_STATE);
void WebServer::switch_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
web_server->switch_json_((switch_::Switch *) (source), ((switch_::Switch *) (source))->state, DETAIL_STATE, builder);
}
json::SerializationBuffer<> WebServer::switch_all_json_generator(WebServer *web_server, void *source) {
return web_server->switch_json_((switch_::Switch *) (source), ((switch_::Switch *) (source))->state, DETAIL_ALL);
void WebServer::switch_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
web_server->switch_json_((switch_::Switch *) (source), ((switch_::Switch *) (source))->state, DETAIL_ALL, builder);
}
json::SerializationBuffer<> WebServer::switch_json_(switch_::Switch *obj, bool value, JsonDetail start_config) {
json::JsonBuilder builder;
void WebServer::switch_json_(switch_::Switch *obj, bool value, JsonDetail start_config, json::JsonBuilder &builder) {
JsonObject root = builder.root();
set_json_icon_state_value(root, obj, "switch", value ? "ON" : "OFF", value, start_config);
@@ -782,8 +791,6 @@ json::SerializationBuffer<> WebServer::switch_json_(switch_::Switch *obj, bool v
root[ESPHOME_F("assumed_state")] = obj->assumed_state();
this->add_sorting_info_(root, obj);
}
return builder.serialize();
}
#endif
@@ -795,8 +802,9 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM
continue;
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->button_json_(obj, detail);
request->send(200, "application/json", data.c_str());
json::JsonBuilder builder;
this->button_json_(obj, detail, builder);
send_json(request, builder);
} else if (match.method_equals(ESPHOME_F("press"))) {
DEFER_ACTION(obj, obj->press());
request->send(200);
@@ -808,19 +816,16 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM
}
request->send(404);
}
json::SerializationBuffer<> WebServer::button_all_json_generator(WebServer *web_server, void *source) {
return web_server->button_json_((button::Button *) (source), DETAIL_ALL);
void WebServer::button_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
web_server->button_json_((button::Button *) (source), DETAIL_ALL, builder);
}
json::SerializationBuffer<> WebServer::button_json_(button::Button *obj, JsonDetail start_config) {
json::JsonBuilder builder;
void WebServer::button_json_(button::Button *obj, JsonDetail start_config, json::JsonBuilder &builder) {
JsonObject root = builder.root();
set_json_id(root, obj, "button", start_config);
if (start_config == DETAIL_ALL) {
this->add_sorting_info_(root, obj);
}
return builder.serialize();
}
#endif
@@ -838,32 +843,30 @@ void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, con
// Note: request->method() is always HTTP_GET here (canHandle ensures this)
if (entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->binary_sensor_json_(obj, obj->state, detail);
request->send(200, "application/json", data.c_str());
json::JsonBuilder builder;
this->binary_sensor_json_(obj, obj->state, detail, builder);
send_json(request, builder);
return;
}
}
request->send(404);
}
json::SerializationBuffer<> WebServer::binary_sensor_state_json_generator(WebServer *web_server, void *source) {
return web_server->binary_sensor_json_((binary_sensor::BinarySensor *) (source),
((binary_sensor::BinarySensor *) (source))->state, DETAIL_STATE);
void WebServer::binary_sensor_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
web_server->binary_sensor_json_((binary_sensor::BinarySensor *) (source),
((binary_sensor::BinarySensor *) (source))->state, DETAIL_STATE, builder);
}
json::SerializationBuffer<> WebServer::binary_sensor_all_json_generator(WebServer *web_server, void *source) {
return web_server->binary_sensor_json_((binary_sensor::BinarySensor *) (source),
((binary_sensor::BinarySensor *) (source))->state, DETAIL_ALL);
void WebServer::binary_sensor_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
web_server->binary_sensor_json_((binary_sensor::BinarySensor *) (source),
((binary_sensor::BinarySensor *) (source))->state, DETAIL_ALL, builder);
}
json::SerializationBuffer<> WebServer::binary_sensor_json_(binary_sensor::BinarySensor *obj, bool value,
JsonDetail start_config) {
json::JsonBuilder builder;
void WebServer::binary_sensor_json_(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config,
json::JsonBuilder &builder) {
JsonObject root = builder.root();
set_json_icon_state_value(root, obj, "binary_sensor", value ? "ON" : "OFF", value, start_config);
if (start_config == DETAIL_ALL) {
this->add_sorting_info_(root, obj);
}
return builder.serialize();
}
#endif
@@ -881,8 +884,9 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->fan_json_(obj, detail);
request->send(200, "application/json", data.c_str());
json::JsonBuilder builder;
this->fan_json_(obj, detail, builder);
send_json(request, builder);
} else if (match.method_equals(ESPHOME_F("toggle"))) {
DEFER_ACTION(obj, obj->toggle().perform());
request->send(200);
@@ -922,14 +926,13 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
}
request->send(404);
}
json::SerializationBuffer<> WebServer::fan_state_json_generator(WebServer *web_server, void *source) {
return web_server->fan_json_((fan::Fan *) (source), DETAIL_STATE);
void WebServer::fan_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
web_server->fan_json_((fan::Fan *) (source), DETAIL_STATE, builder);
}
json::SerializationBuffer<> WebServer::fan_all_json_generator(WebServer *web_server, void *source) {
return web_server->fan_json_((fan::Fan *) (source), DETAIL_ALL);
void WebServer::fan_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
web_server->fan_json_((fan::Fan *) (source), DETAIL_ALL, builder);
}
json::SerializationBuffer<> WebServer::fan_json_(fan::Fan *obj, JsonDetail start_config) {
json::JsonBuilder builder;
void WebServer::fan_json_(fan::Fan *obj, JsonDetail start_config, json::JsonBuilder &builder) {
JsonObject root = builder.root();
set_json_icon_state_value(root, obj, "fan", obj->state ? "ON" : "OFF", obj->state, start_config);
@@ -943,8 +946,6 @@ json::SerializationBuffer<> WebServer::fan_json_(fan::Fan *obj, JsonDetail start
if (start_config == DETAIL_ALL) {
this->add_sorting_info_(root, obj);
}
return builder.serialize();
}
#endif
@@ -962,8 +963,9 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->light_json_(obj, detail);
request->send(200, "application/json", data.c_str());
json::JsonBuilder builder;
this->light_json_(obj, detail, builder);
send_json(request, builder);
} else if (match.method_equals(ESPHOME_F("toggle"))) {
DEFER_ACTION(obj, obj->toggle().perform());
request->send(200);
@@ -1003,14 +1005,13 @@ void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMa
}
request->send(404);
}
json::SerializationBuffer<> WebServer::light_state_json_generator(WebServer *web_server, void *source) {
return web_server->light_json_((light::LightState *) (source), DETAIL_STATE);
void WebServer::light_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
web_server->light_json_((light::LightState *) (source), DETAIL_STATE, builder);
}
json::SerializationBuffer<> WebServer::light_all_json_generator(WebServer *web_server, void *source) {
return web_server->light_json_((light::LightState *) (source), DETAIL_ALL);
void WebServer::light_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
web_server->light_json_((light::LightState *) (source), DETAIL_ALL, builder);
}
json::SerializationBuffer<> WebServer::light_json_(light::LightState *obj, JsonDetail start_config) {
json::JsonBuilder builder;
void WebServer::light_json_(light::LightState *obj, JsonDetail start_config, json::JsonBuilder &builder) {
JsonObject root = builder.root();
set_json_value(root, obj, "light", obj->get_reported_values().is_on() ? "ON" : "OFF", start_config);
@@ -1024,8 +1025,6 @@ json::SerializationBuffer<> WebServer::light_json_(light::LightState *obj, JsonD
}
this->add_sorting_info_(root, obj);
}
return builder.serialize();
}
#endif
@@ -1043,8 +1042,9 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->cover_json_(obj, detail);
request->send(200, "application/json", data.c_str());
json::JsonBuilder builder;
this->cover_json_(obj, detail, builder);
send_json(request, builder);
return;
}
@@ -1091,14 +1091,13 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa
}
request->send(404);
}
json::SerializationBuffer<> WebServer::cover_state_json_generator(WebServer *web_server, void *source) {
return web_server->cover_json_((cover::Cover *) (source), DETAIL_STATE);
void WebServer::cover_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
web_server->cover_json_((cover::Cover *) (source), DETAIL_STATE, builder);
}
json::SerializationBuffer<> WebServer::cover_all_json_generator(WebServer *web_server, void *source) {
return web_server->cover_json_((cover::Cover *) (source), DETAIL_ALL);
void WebServer::cover_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
web_server->cover_json_((cover::Cover *) (source), DETAIL_ALL, builder);
}
json::SerializationBuffer<> WebServer::cover_json_(cover::Cover *obj, JsonDetail start_config) {
json::JsonBuilder builder;
void WebServer::cover_json_(cover::Cover *obj, JsonDetail start_config, json::JsonBuilder &builder) {
JsonObject root = builder.root();
set_json_icon_state_value(root, obj, "cover", obj->is_fully_closed() ? "CLOSED" : "OPEN", obj->position,
@@ -1113,8 +1112,6 @@ json::SerializationBuffer<> WebServer::cover_json_(cover::Cover *obj, JsonDetail
root[ESPHOME_F("assumed_state")] = obj->get_traits().get_is_assumed_state();
this->add_sorting_info_(root, obj);
}
return builder.serialize();
}
#endif
@@ -1132,8 +1129,9 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->number_json_(obj, obj->state, detail);
request->send(200, "application/json", data.c_str());
json::JsonBuilder builder;
this->number_json_(obj, obj->state, detail, builder);
send_json(request, builder);
return;
}
if (!match.method_equals(ESPHOME_F("set"))) {
@@ -1151,14 +1149,13 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM
request->send(404);
}
json::SerializationBuffer<> WebServer::number_state_json_generator(WebServer *web_server, void *source) {
return web_server->number_json_((number::Number *) (source), ((number::Number *) (source))->state, DETAIL_STATE);
void WebServer::number_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
web_server->number_json_((number::Number *) (source), ((number::Number *) (source))->state, DETAIL_STATE, builder);
}
json::SerializationBuffer<> WebServer::number_all_json_generator(WebServer *web_server, void *source) {
return web_server->number_json_((number::Number *) (source), ((number::Number *) (source))->state, DETAIL_ALL);
void WebServer::number_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
web_server->number_json_((number::Number *) (source), ((number::Number *) (source))->state, DETAIL_ALL, builder);
}
json::SerializationBuffer<> WebServer::number_json_(number::Number *obj, float value, JsonDetail start_config) {
json::JsonBuilder builder;
void WebServer::number_json_(number::Number *obj, float value, JsonDetail start_config, json::JsonBuilder &builder) {
JsonObject root = builder.root();
const auto uom_ref = obj->get_unit_of_measurement_ref();
@@ -1181,8 +1178,6 @@ json::SerializationBuffer<> WebServer::number_json_(number::Number *obj, float v
root[ESPHOME_F("uom")] = uom_ref.c_str();
this->add_sorting_info_(root, obj);
}
return builder.serialize();
}
#endif
@@ -1199,8 +1194,9 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat
continue;
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->date_json_(obj, detail);
request->send(200, "application/json", data.c_str());
json::JsonBuilder builder;
this->date_json_(obj, detail, builder);
send_json(request, builder);
return;
}
if (!match.method_equals(ESPHOME_F("set"))) {
@@ -1225,14 +1221,13 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat
request->send(404);
}
json::SerializationBuffer<> WebServer::date_state_json_generator(WebServer *web_server, void *source) {
return web_server->date_json_((datetime::DateEntity *) (source), DETAIL_STATE);
void WebServer::date_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
web_server->date_json_((datetime::DateEntity *) (source), DETAIL_STATE, builder);
}
json::SerializationBuffer<> WebServer::date_all_json_generator(WebServer *web_server, void *source) {
return web_server->date_json_((datetime::DateEntity *) (source), DETAIL_ALL);
void WebServer::date_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
web_server->date_json_((datetime::DateEntity *) (source), DETAIL_ALL, builder);
}
json::SerializationBuffer<> WebServer::date_json_(datetime::DateEntity *obj, JsonDetail start_config) {
json::JsonBuilder builder;
void WebServer::date_json_(datetime::DateEntity *obj, JsonDetail start_config, json::JsonBuilder &builder) {
JsonObject root = builder.root();
// Format: YYYY-MM-DD (max 10 chars + null)
@@ -1242,8 +1237,6 @@ json::SerializationBuffer<> WebServer::date_json_(datetime::DateEntity *obj, Jso
if (start_config == DETAIL_ALL) {
this->add_sorting_info_(root, obj);
}
return builder.serialize();
}
#endif // USE_DATETIME_DATE
@@ -1260,8 +1253,9 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat
continue;
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->time_json_(obj, detail);
request->send(200, "application/json", data.c_str());
json::JsonBuilder builder;
this->time_json_(obj, detail, builder);
send_json(request, builder);
return;
}
if (!match.method_equals(ESPHOME_F("set"))) {
@@ -1285,14 +1279,13 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat
}
request->send(404);
}
json::SerializationBuffer<> WebServer::time_state_json_generator(WebServer *web_server, void *source) {
return web_server->time_json_((datetime::TimeEntity *) (source), DETAIL_STATE);
void WebServer::time_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
web_server->time_json_((datetime::TimeEntity *) (source), DETAIL_STATE, builder);
}
json::SerializationBuffer<> WebServer::time_all_json_generator(WebServer *web_server, void *source) {
return web_server->time_json_((datetime::TimeEntity *) (source), DETAIL_ALL);
void WebServer::time_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
web_server->time_json_((datetime::TimeEntity *) (source), DETAIL_ALL, builder);
}
json::SerializationBuffer<> WebServer::time_json_(datetime::TimeEntity *obj, JsonDetail start_config) {
json::JsonBuilder builder;
void WebServer::time_json_(datetime::TimeEntity *obj, JsonDetail start_config, json::JsonBuilder &builder) {
JsonObject root = builder.root();
// Format: HH:MM:SS (8 chars + null)
@@ -1302,8 +1295,6 @@ json::SerializationBuffer<> WebServer::time_json_(datetime::TimeEntity *obj, Jso
if (start_config == DETAIL_ALL) {
this->add_sorting_info_(root, obj);
}
return builder.serialize();
}
#endif // USE_DATETIME_TIME
@@ -1320,8 +1311,9 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur
continue;
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->datetime_json_(obj, detail);
request->send(200, "application/json", data.c_str());
json::JsonBuilder builder;
this->datetime_json_(obj, detail, builder);
send_json(request, builder);
return;
}
if (!match.method_equals(ESPHOME_F("set"))) {
@@ -1345,14 +1337,13 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur
}
request->send(404);
}
json::SerializationBuffer<> WebServer::datetime_state_json_generator(WebServer *web_server, void *source) {
return web_server->datetime_json_((datetime::DateTimeEntity *) (source), DETAIL_STATE);
void WebServer::datetime_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
web_server->datetime_json_((datetime::DateTimeEntity *) (source), DETAIL_STATE, builder);
}
json::SerializationBuffer<> WebServer::datetime_all_json_generator(WebServer *web_server, void *source) {
return web_server->datetime_json_((datetime::DateTimeEntity *) (source), DETAIL_ALL);
void WebServer::datetime_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
web_server->datetime_json_((datetime::DateTimeEntity *) (source), DETAIL_ALL, builder);
}
json::SerializationBuffer<> WebServer::datetime_json_(datetime::DateTimeEntity *obj, JsonDetail start_config) {
json::JsonBuilder builder;
void WebServer::datetime_json_(datetime::DateTimeEntity *obj, JsonDetail start_config, json::JsonBuilder &builder) {
JsonObject root = builder.root();
// Format: YYYY-MM-DD HH:MM:SS (max 19 chars + null)
@@ -1363,8 +1354,6 @@ json::SerializationBuffer<> WebServer::datetime_json_(datetime::DateTimeEntity *
if (start_config == DETAIL_ALL) {
this->add_sorting_info_(root, obj);
}
return builder.serialize();
}
#endif // USE_DATETIME_DATETIME
@@ -1382,8 +1371,9 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->text_json_(obj, obj->state, detail);
request->send(200, "application/json", data.c_str());
json::JsonBuilder builder;
this->text_json_(obj, obj->state, detail, builder);
send_json(request, builder);
return;
}
if (!match.method_equals(ESPHOME_F("set"))) {
@@ -1403,14 +1393,14 @@ void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMat
request->send(404);
}
json::SerializationBuffer<> WebServer::text_state_json_generator(WebServer *web_server, void *source) {
return web_server->text_json_((text::Text *) (source), ((text::Text *) (source))->state, DETAIL_STATE);
void WebServer::text_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
web_server->text_json_((text::Text *) (source), ((text::Text *) (source))->state, DETAIL_STATE, builder);
}
json::SerializationBuffer<> WebServer::text_all_json_generator(WebServer *web_server, void *source) {
return web_server->text_json_((text::Text *) (source), ((text::Text *) (source))->state, DETAIL_ALL);
void WebServer::text_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
web_server->text_json_((text::Text *) (source), ((text::Text *) (source))->state, DETAIL_ALL, builder);
}
json::SerializationBuffer<> WebServer::text_json_(text::Text *obj, const std::string &value, JsonDetail start_config) {
json::JsonBuilder builder;
void WebServer::text_json_(text::Text *obj, const std::string &value, JsonDetail start_config,
json::JsonBuilder &builder) {
JsonObject root = builder.root();
// A password entity shows the mask and prefills the input with nothing, so the secret never
@@ -1425,8 +1415,6 @@ json::SerializationBuffer<> WebServer::text_json_(text::Text *obj, const std::st
root[ESPHOME_F("mode")] = (int) obj->traits.get_mode();
this->add_sorting_info_(root, obj);
}
return builder.serialize();
}
#endif
@@ -1444,8 +1432,9 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->select_json_(obj, obj->has_state() ? obj->current_option() : StringRef(), detail);
request->send(200, "application/json", data.c_str());
json::JsonBuilder builder;
this->select_json_(obj, obj->has_state() ? obj->current_option() : StringRef(), detail, builder);
send_json(request, builder);
return;
}
@@ -1465,16 +1454,16 @@ void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlM
}
request->send(404);
}
json::SerializationBuffer<> WebServer::select_state_json_generator(WebServer *web_server, void *source) {
void WebServer::select_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
auto *obj = (select::Select *) (source);
return web_server->select_json_(obj, obj->has_state() ? obj->current_option() : StringRef(), DETAIL_STATE);
web_server->select_json_(obj, obj->has_state() ? obj->current_option() : StringRef(), DETAIL_STATE, builder);
}
json::SerializationBuffer<> WebServer::select_all_json_generator(WebServer *web_server, void *source) {
void WebServer::select_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
auto *obj = (select::Select *) (source);
return web_server->select_json_(obj, obj->has_state() ? obj->current_option() : StringRef(), DETAIL_ALL);
web_server->select_json_(obj, obj->has_state() ? obj->current_option() : StringRef(), DETAIL_ALL, builder);
}
json::SerializationBuffer<> WebServer::select_json_(select::Select *obj, StringRef value, JsonDetail start_config) {
json::JsonBuilder builder;
void WebServer::select_json_(select::Select *obj, StringRef value, JsonDetail start_config,
json::JsonBuilder &builder) {
JsonObject root = builder.root();
// value points to null-terminated string literals from codegen (via current_option())
@@ -1486,8 +1475,6 @@ json::SerializationBuffer<> WebServer::select_json_(select::Select *obj, StringR
}
this->add_sorting_info_(root, obj);
}
return builder.serialize();
}
#endif
@@ -1505,8 +1492,9 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->climate_json_(obj, detail);
request->send(200, "application/json", data.c_str());
json::JsonBuilder builder;
this->climate_json_(obj, detail, builder);
send_json(request, builder);
return;
}
@@ -1547,17 +1535,16 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url
}
request->send(404);
}
json::SerializationBuffer<> WebServer::climate_state_json_generator(WebServer *web_server, void *source) {
void WebServer::climate_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return web_server->climate_json_((climate::Climate *) (source), DETAIL_STATE);
web_server->climate_json_((climate::Climate *) (source), DETAIL_STATE, builder);
}
json::SerializationBuffer<> WebServer::climate_all_json_generator(WebServer *web_server, void *source) {
void WebServer::climate_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return web_server->climate_json_((climate::Climate *) (source), DETAIL_ALL);
web_server->climate_json_((climate::Climate *) (source), DETAIL_ALL, builder);
}
json::SerializationBuffer<> WebServer::climate_json_(climate::Climate *obj, JsonDetail start_config) {
void WebServer::climate_json_(climate::Climate *obj, JsonDetail start_config, json::JsonBuilder &builder) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
json::JsonBuilder builder;
JsonObject root = builder.root();
set_json_id(root, obj, "climate", start_config);
const auto traits = obj->get_traits();
@@ -1656,7 +1643,6 @@ json::SerializationBuffer<> WebServer::climate_json_(climate::Climate *obj, Json
root[ESPHOME_F("state")] = root[ESPHOME_F("target_temperature")];
}
return builder.serialize();
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
}
#endif
@@ -1693,8 +1679,9 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->lock_json_(obj, obj->state, detail);
request->send(200, "application/json", data.c_str());
json::JsonBuilder builder;
this->lock_json_(obj, obj->state, detail, builder);
send_json(request, builder);
return;
}
@@ -1718,22 +1705,20 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat
}
request->send(404);
}
json::SerializationBuffer<> WebServer::lock_state_json_generator(WebServer *web_server, void *source) {
return web_server->lock_json_((lock::Lock *) (source), ((lock::Lock *) (source))->state, DETAIL_STATE);
void WebServer::lock_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
web_server->lock_json_((lock::Lock *) (source), ((lock::Lock *) (source))->state, DETAIL_STATE, builder);
}
json::SerializationBuffer<> WebServer::lock_all_json_generator(WebServer *web_server, void *source) {
return web_server->lock_json_((lock::Lock *) (source), ((lock::Lock *) (source))->state, DETAIL_ALL);
void WebServer::lock_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
web_server->lock_json_((lock::Lock *) (source), ((lock::Lock *) (source))->state, DETAIL_ALL, builder);
}
json::SerializationBuffer<> WebServer::lock_json_(lock::Lock *obj, lock::LockState value, JsonDetail start_config) {
json::JsonBuilder builder;
void WebServer::lock_json_(lock::Lock *obj, lock::LockState value, JsonDetail start_config,
json::JsonBuilder &builder) {
JsonObject root = builder.root();
set_json_icon_state_value(root, obj, "lock", json_state_str(lock::lock_state_to_string(value)), value, start_config);
if (start_config == DETAIL_ALL) {
this->add_sorting_info_(root, obj);
}
return builder.serialize();
}
#endif
@@ -1751,8 +1736,9 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->valve_json_(obj, detail);
request->send(200, "application/json", data.c_str());
json::JsonBuilder builder;
this->valve_json_(obj, detail, builder);
send_json(request, builder);
return;
}
@@ -1797,14 +1783,13 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa
}
request->send(404);
}
json::SerializationBuffer<> WebServer::valve_state_json_generator(WebServer *web_server, void *source) {
return web_server->valve_json_((valve::Valve *) (source), DETAIL_STATE);
void WebServer::valve_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
web_server->valve_json_((valve::Valve *) (source), DETAIL_STATE, builder);
}
json::SerializationBuffer<> WebServer::valve_all_json_generator(WebServer *web_server, void *source) {
return web_server->valve_json_((valve::Valve *) (source), DETAIL_ALL);
void WebServer::valve_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
web_server->valve_json_((valve::Valve *) (source), DETAIL_ALL, builder);
}
json::SerializationBuffer<> WebServer::valve_json_(valve::Valve *obj, JsonDetail start_config) {
json::JsonBuilder builder;
void WebServer::valve_json_(valve::Valve *obj, JsonDetail start_config, json::JsonBuilder &builder) {
JsonObject root = builder.root();
set_json_icon_state_value(root, obj, "valve", obj->is_fully_closed() ? "CLOSED" : "OPEN", obj->position,
@@ -1816,8 +1801,6 @@ json::SerializationBuffer<> WebServer::valve_json_(valve::Valve *obj, JsonDetail
if (start_config == DETAIL_ALL) {
this->add_sorting_info_(root, obj);
}
return builder.serialize();
}
#endif
@@ -1835,8 +1818,9 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->alarm_control_panel_json_(obj, obj->get_state(), detail);
request->send(200, "application/json", data.c_str());
json::JsonBuilder builder;
this->alarm_control_panel_json_(obj, obj->get_state(), detail, builder);
send_json(request, builder);
return;
}
@@ -1878,20 +1862,21 @@ void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *reques
}
request->send(404);
}
json::SerializationBuffer<> WebServer::alarm_control_panel_state_json_generator(WebServer *web_server, void *source) {
return web_server->alarm_control_panel_json_((alarm_control_panel::AlarmControlPanel *) (source),
((alarm_control_panel::AlarmControlPanel *) (source))->get_state(),
DETAIL_STATE);
void WebServer::alarm_control_panel_state_json_generator(WebServer *web_server, void *source,
json::JsonBuilder &builder) {
web_server->alarm_control_panel_json_((alarm_control_panel::AlarmControlPanel *) (source),
((alarm_control_panel::AlarmControlPanel *) (source))->get_state(),
DETAIL_STATE, builder);
}
json::SerializationBuffer<> WebServer::alarm_control_panel_all_json_generator(WebServer *web_server, void *source) {
return web_server->alarm_control_panel_json_((alarm_control_panel::AlarmControlPanel *) (source),
((alarm_control_panel::AlarmControlPanel *) (source))->get_state(),
DETAIL_ALL);
void WebServer::alarm_control_panel_all_json_generator(WebServer *web_server, void *source,
json::JsonBuilder &builder) {
web_server->alarm_control_panel_json_((alarm_control_panel::AlarmControlPanel *) (source),
((alarm_control_panel::AlarmControlPanel *) (source))->get_state(), DETAIL_ALL,
builder);
}
json::SerializationBuffer<> WebServer::alarm_control_panel_json_(alarm_control_panel::AlarmControlPanel *obj,
alarm_control_panel::AlarmControlPanelState value,
JsonDetail start_config) {
json::JsonBuilder builder;
void WebServer::alarm_control_panel_json_(alarm_control_panel::AlarmControlPanel *obj,
alarm_control_panel::AlarmControlPanelState value, JsonDetail start_config,
json::JsonBuilder &builder) {
JsonObject root = builder.root();
set_json_icon_state_value(root, obj, "alarm_control_panel",
@@ -1899,8 +1884,6 @@ json::SerializationBuffer<> WebServer::alarm_control_panel_json_(alarm_control_p
if (start_config == DETAIL_ALL) {
this->add_sorting_info_(root, obj);
}
return builder.serialize();
}
#endif
@@ -1918,8 +1901,9 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->water_heater_json_(obj, detail);
request->send(200, "application/json", data.c_str());
json::JsonBuilder builder;
this->water_heater_json_(obj, detail, builder);
send_json(request, builder);
return;
}
if (!match.method_equals(ESPHOME_F("set"))) {
@@ -1957,15 +1941,15 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons
request->send(404);
}
json::SerializationBuffer<> WebServer::water_heater_state_json_generator(WebServer *web_server, void *source) {
return web_server->water_heater_json_(static_cast<water_heater::WaterHeater *>(source), DETAIL_STATE);
void WebServer::water_heater_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
web_server->water_heater_json_(static_cast<water_heater::WaterHeater *>(source), DETAIL_STATE, builder);
}
json::SerializationBuffer<> WebServer::water_heater_all_json_generator(WebServer *web_server, void *source) {
void WebServer::water_heater_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return web_server->water_heater_json_(static_cast<water_heater::WaterHeater *>(source), DETAIL_ALL);
web_server->water_heater_json_(static_cast<water_heater::WaterHeater *>(source), DETAIL_ALL, builder);
}
json::SerializationBuffer<> WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config) {
json::JsonBuilder builder;
void WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config,
json::JsonBuilder &builder) {
JsonObject root = builder.root();
const auto mode = obj->get_mode();
@@ -2012,8 +1996,6 @@ json::SerializationBuffer<> WebServer::water_heater_json_(water_heater::WaterHea
if (traits.has_feature_flags(water_heater::WATER_HEATER_SUPPORTS_ON_OFF)) {
root[ESPHOME_F("is_on")] = obj->is_on();
}
return builder.serialize();
}
#endif
@@ -2026,8 +2008,9 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->infrared_json_(obj, detail);
request->send(200, ESPHOME_F("application/json"), data.c_str());
json::JsonBuilder builder;
this->infrared_json_(obj, detail, builder);
send_json(request, builder);
return;
}
if (!match.method_equals(ESPHOME_F("transmit"))) {
@@ -2086,13 +2069,12 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur
request->send(404);
}
json::SerializationBuffer<> WebServer::infrared_all_json_generator(WebServer *web_server, void *source) {
void WebServer::infrared_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return web_server->infrared_json_(static_cast<infrared::Infrared *>(source), DETAIL_ALL);
web_server->infrared_json_(static_cast<infrared::Infrared *>(source), DETAIL_ALL, builder);
}
json::SerializationBuffer<> WebServer::infrared_json_(infrared::Infrared *obj, JsonDetail start_config) {
json::JsonBuilder builder;
void WebServer::infrared_json_(infrared::Infrared *obj, JsonDetail start_config, json::JsonBuilder &builder) {
JsonObject root = builder.root();
set_json_icon_state_value(root, obj, "infrared", "", 0, start_config);
@@ -2105,8 +2087,6 @@ json::SerializationBuffer<> WebServer::infrared_json_(infrared::Infrared *obj, J
if (start_config == DETAIL_ALL) {
this->add_sorting_info_(root, obj);
}
return builder.serialize();
}
#endif
@@ -2119,8 +2099,9 @@ void WebServer::handle_radio_frequency_request(AsyncWebServerRequest *request, c
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->radio_frequency_json_(obj, detail);
request->send(200, ESPHOME_F("application/json"), data.c_str());
json::JsonBuilder builder;
this->radio_frequency_json_(obj, detail, builder);
send_json(request, builder);
return;
}
if (!match.method_equals(ESPHOME_F("transmit"))) {
@@ -2178,14 +2159,13 @@ void WebServer::handle_radio_frequency_request(AsyncWebServerRequest *request, c
request->send(404);
}
json::SerializationBuffer<> WebServer::radio_frequency_all_json_generator(WebServer *web_server, void *source) {
void WebServer::radio_frequency_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return web_server->radio_frequency_json_(static_cast<radio_frequency::RadioFrequency *>(source), DETAIL_ALL);
web_server->radio_frequency_json_(static_cast<radio_frequency::RadioFrequency *>(source), DETAIL_ALL, builder);
}
json::SerializationBuffer<> WebServer::radio_frequency_json_(radio_frequency::RadioFrequency *obj,
JsonDetail start_config) {
json::JsonBuilder builder;
void WebServer::radio_frequency_json_(radio_frequency::RadioFrequency *obj, JsonDetail start_config,
json::JsonBuilder &builder) {
JsonObject root = builder.root();
set_json_icon_state_value(root, obj, "radio_frequency", "", 0, start_config);
@@ -2203,8 +2183,6 @@ json::SerializationBuffer<> WebServer::radio_frequency_json_(radio_frequency::Ra
if (start_config == DETAIL_ALL) {
this->add_sorting_info_(root, obj);
}
return builder.serialize();
}
#endif
@@ -2224,8 +2202,9 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa
// Note: request->method() is always HTTP_GET here (canHandle ensures this)
if (entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->event_json_(obj, StringRef(), detail);
request->send(200, "application/json", data.c_str());
json::JsonBuilder builder;
this->event_json_(obj, StringRef(), detail, builder);
send_json(request, builder);
return;
}
}
@@ -2234,17 +2213,17 @@ void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMa
static StringRef get_event_type(event::Event *event) { return event ? event->get_last_event_type() : StringRef(); }
json::SerializationBuffer<> WebServer::event_state_json_generator(WebServer *web_server, void *source) {
void WebServer::event_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
auto *event = static_cast<event::Event *>(source);
return web_server->event_json_(event, get_event_type(event), DETAIL_STATE);
web_server->event_json_(event, get_event_type(event), DETAIL_STATE, builder);
}
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
json::SerializationBuffer<> WebServer::event_all_json_generator(WebServer *web_server, void *source) {
void WebServer::event_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
auto *event = static_cast<event::Event *>(source);
return web_server->event_json_(event, get_event_type(event), DETAIL_ALL);
web_server->event_json_(event, get_event_type(event), DETAIL_ALL, builder);
}
json::SerializationBuffer<> WebServer::event_json_(event::Event *obj, StringRef event_type, JsonDetail start_config) {
json::JsonBuilder builder;
void WebServer::event_json_(event::Event *obj, StringRef event_type, JsonDetail start_config,
json::JsonBuilder &builder) {
JsonObject root = builder.root();
set_json_id(root, obj, "event", start_config);
@@ -2260,8 +2239,6 @@ json::SerializationBuffer<> WebServer::event_json_(event::Event *obj, StringRef
root[ESPHOME_F("device_class")] = obj->get_device_class_to(dc_buf);
this->add_sorting_info_(root, obj);
}
return builder.serialize();
}
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
#endif
@@ -2278,8 +2255,9 @@ void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlM
if (request->method() == HTTP_GET && entity_match.action_is_empty) {
auto detail = get_request_detail(request);
auto data = this->update_json_(obj, detail);
request->send(200, "application/json", data.c_str());
json::JsonBuilder builder;
this->update_json_(obj, detail, builder);
send_json(request, builder);
return;
}
@@ -2294,17 +2272,16 @@ void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlM
}
request->send(404);
}
json::SerializationBuffer<> WebServer::update_state_json_generator(WebServer *web_server, void *source) {
void WebServer::update_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return web_server->update_json_((update::UpdateEntity *) (source), DETAIL_STATE);
web_server->update_json_((update::UpdateEntity *) (source), DETAIL_STATE, builder);
}
json::SerializationBuffer<> WebServer::update_all_json_generator(WebServer *web_server, void *source) {
void WebServer::update_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
return web_server->update_json_((update::UpdateEntity *) (source), DETAIL_ALL);
web_server->update_json_((update::UpdateEntity *) (source), DETAIL_ALL, builder);
}
json::SerializationBuffer<> WebServer::update_json_(update::UpdateEntity *obj, JsonDetail start_config) {
void WebServer::update_json_(update::UpdateEntity *obj, JsonDetail start_config, json::JsonBuilder &builder) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
json::JsonBuilder builder;
JsonObject root = builder.root();
set_json_icon_state_value(root, obj, "update", json_state_str(update::update_state_to_string(obj->state)),
@@ -2321,7 +2298,6 @@ json::SerializationBuffer<> WebServer::update_json_(update::UpdateEntity *obj, J
this->add_sorting_info_(root, obj);
}
return builder.serialize();
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
}
#endif
+75 -71
View File
@@ -37,6 +37,10 @@ extern const size_t ESPHOME_WEBSERVER_JS_INCLUDE_SIZE;
namespace esphome::web_server {
// ESP-IDF cuts a log event here: nothing a browser log view needs is longer, and it bounds the
// tail a stalled client keeps. The Arduino backend takes a C string and sends the whole line.
constexpr size_t LOG_EVENT_MAX_LEN = 512;
// Parameter names live in flash on ESP8266
using ParamNameType = ProgmemStr;
@@ -101,7 +105,7 @@ enum JsonDetail { DETAIL_ALL, DETAIL_STATE };
can be forgotten.
*/
#if !defined(USE_ESP32) && defined(USE_ARDUINO)
using message_generator_t = json::SerializationBuffer<>(WebServer *, void *);
using message_generator_t = void(WebServer *, void *, json::JsonBuilder &);
class DeferredUpdateEventSourceList;
class DeferredUpdateEventSource final : public AsyncEventSource {
@@ -296,8 +300,8 @@ class WebServer final : public Component, public AsyncWebHandler {
/// Handle a sensor request under '/sensor/<id>'.
void handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> sensor_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> sensor_all_json_generator(WebServer *web_server, void *source);
static void sensor_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
static void sensor_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
#endif
#ifdef USE_SWITCH
@@ -306,8 +310,8 @@ class WebServer final : public Component, public AsyncWebHandler {
/// Handle a switch request under '/switch/<id>/</turn_on/turn_off/toggle>'.
void handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> switch_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> switch_all_json_generator(WebServer *web_server, void *source);
static void switch_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
static void switch_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
#endif
#ifdef USE_BUTTON
@@ -315,7 +319,7 @@ class WebServer final : public Component, public AsyncWebHandler {
void handle_button_request(AsyncWebServerRequest *request, const UrlMatch &match);
// Buttons are stateless, so there is no button_state_json_generator
static json::SerializationBuffer<> button_all_json_generator(WebServer *web_server, void *source);
static void button_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
#endif
#ifdef USE_BINARY_SENSOR
@@ -324,8 +328,8 @@ class WebServer final : public Component, public AsyncWebHandler {
/// Handle a binary sensor request under '/binary_sensor/<id>'.
void handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> binary_sensor_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> binary_sensor_all_json_generator(WebServer *web_server, void *source);
static void binary_sensor_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
static void binary_sensor_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
#endif
#ifdef USE_FAN
@@ -334,8 +338,8 @@ class WebServer final : public Component, public AsyncWebHandler {
/// Handle a fan request under '/fan/<id>/</turn_on/turn_off/toggle>'.
void handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> fan_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> fan_all_json_generator(WebServer *web_server, void *source);
static void fan_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
static void fan_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
#endif
#ifdef USE_LIGHT
@@ -344,8 +348,8 @@ class WebServer final : public Component, public AsyncWebHandler {
/// Handle a light request under '/light/<id>/</turn_on/turn_off/toggle>'.
void handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> light_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> light_all_json_generator(WebServer *web_server, void *source);
static void light_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
static void light_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
#endif
#ifdef USE_TEXT_SENSOR
@@ -354,8 +358,8 @@ class WebServer final : public Component, public AsyncWebHandler {
/// Handle a text sensor request under '/text_sensor/<id>'.
void handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> text_sensor_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> text_sensor_all_json_generator(WebServer *web_server, void *source);
static void text_sensor_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
static void text_sensor_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
#endif
#ifdef USE_COVER
@@ -364,8 +368,8 @@ class WebServer final : public Component, public AsyncWebHandler {
/// Handle a cover request under '/cover/<id>/<open/close/stop/set>'.
void handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> cover_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> cover_all_json_generator(WebServer *web_server, void *source);
static void cover_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
static void cover_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
#endif
#ifdef USE_NUMBER
@@ -373,8 +377,8 @@ class WebServer final : public Component, public AsyncWebHandler {
/// Handle a number request under '/number/<id>'.
void handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> number_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> number_all_json_generator(WebServer *web_server, void *source);
static void number_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
static void number_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
#endif
#ifdef USE_DATETIME_DATE
@@ -382,8 +386,8 @@ class WebServer final : public Component, public AsyncWebHandler {
/// Handle a date request under '/date/<id>'.
void handle_date_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> date_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> date_all_json_generator(WebServer *web_server, void *source);
static void date_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
static void date_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
#endif
#ifdef USE_DATETIME_TIME
@@ -391,8 +395,8 @@ class WebServer final : public Component, public AsyncWebHandler {
/// Handle a time request under '/time/<id>'.
void handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> time_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> time_all_json_generator(WebServer *web_server, void *source);
static void time_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
static void time_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
#endif
#ifdef USE_DATETIME_DATETIME
@@ -400,8 +404,8 @@ class WebServer final : public Component, public AsyncWebHandler {
/// Handle a datetime request under '/datetime/<id>'.
void handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> datetime_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> datetime_all_json_generator(WebServer *web_server, void *source);
static void datetime_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
static void datetime_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
#endif
#ifdef USE_TEXT
@@ -409,8 +413,8 @@ class WebServer final : public Component, public AsyncWebHandler {
/// Handle a text input request under '/text/<id>'.
void handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> text_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> text_all_json_generator(WebServer *web_server, void *source);
static void text_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
static void text_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
#endif
#ifdef USE_SELECT
@@ -418,8 +422,8 @@ class WebServer final : public Component, public AsyncWebHandler {
/// Handle a select request under '/select/<id>'.
void handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> select_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> select_all_json_generator(WebServer *web_server, void *source);
static void select_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
static void select_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
#endif
#ifdef USE_CLIMATE
@@ -427,8 +431,8 @@ class WebServer final : public Component, public AsyncWebHandler {
/// Handle a climate request under '/climate/<id>'.
void handle_climate_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> climate_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> climate_all_json_generator(WebServer *web_server, void *source);
static void climate_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
static void climate_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
#endif
#ifdef USE_LOCK
@@ -437,8 +441,8 @@ class WebServer final : public Component, public AsyncWebHandler {
/// Handle a lock request under '/lock/<id>/</lock/unlock/open>'.
void handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> lock_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> lock_all_json_generator(WebServer *web_server, void *source);
static void lock_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
static void lock_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
#endif
#ifdef USE_VALVE
@@ -447,8 +451,8 @@ class WebServer final : public Component, public AsyncWebHandler {
/// Handle a valve request under '/valve/<id>/<open/close/stop/set>'.
void handle_valve_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> valve_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> valve_all_json_generator(WebServer *web_server, void *source);
static void valve_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
static void valve_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
#endif
#ifdef USE_ALARM_CONTROL_PANEL
@@ -457,8 +461,8 @@ class WebServer final : public Component, public AsyncWebHandler {
/// Handle a alarm_control_panel request under '/alarm_control_panel/<id>'.
void handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> alarm_control_panel_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> alarm_control_panel_all_json_generator(WebServer *web_server, void *source);
static void alarm_control_panel_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
static void alarm_control_panel_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
#endif
#ifdef USE_WATER_HEATER
@@ -467,21 +471,21 @@ class WebServer final : public Component, public AsyncWebHandler {
/// Handle a water_heater request under '/water_heater/<id>/<mode/set>'.
void handle_water_heater_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> water_heater_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> water_heater_all_json_generator(WebServer *web_server, void *source);
static void water_heater_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
static void water_heater_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
#endif
#ifdef USE_INFRARED
/// Handle an infrared request under '/infrared/<id>/transmit'.
void handle_infrared_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> infrared_all_json_generator(WebServer *web_server, void *source);
static void infrared_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
#endif
#ifdef USE_RADIO_FREQUENCY
/// Handle a radio frequency request under '/radio_frequency/<id>/transmit'.
void handle_radio_frequency_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> radio_frequency_all_json_generator(WebServer *web_server, void *source);
static void radio_frequency_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
#endif
#ifdef USE_MEDIA_PLAYER
@@ -492,8 +496,8 @@ class WebServer final : public Component, public AsyncWebHandler {
#ifdef USE_EVENT
void on_event(event::Event *obj);
static json::SerializationBuffer<> event_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> event_all_json_generator(WebServer *web_server, void *source);
static void event_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
static void event_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
/// Handle a event request under '/event<id>'.
void handle_event_request(AsyncWebServerRequest *request, const UrlMatch &match);
@@ -505,8 +509,8 @@ class WebServer final : public Component, public AsyncWebHandler {
/// Handle a update request under '/update/<id>'.
void handle_update_request(AsyncWebServerRequest *request, const UrlMatch &match);
static json::SerializationBuffer<> update_state_json_generator(WebServer *web_server, void *source);
static json::SerializationBuffer<> update_all_json_generator(WebServer *web_server, void *source);
static void update_state_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
static void update_all_json_generator(WebServer *web_server, void *source, json::JsonBuilder &builder);
#endif
/// Override the web handler's canHandle method.
@@ -624,77 +628,77 @@ class WebServer final : public Component, public AsyncWebHandler {
private:
#ifdef USE_SENSOR
json::SerializationBuffer<> sensor_json_(sensor::Sensor *obj, float value, JsonDetail start_config);
void sensor_json_(sensor::Sensor *obj, float value, JsonDetail start_config, json::JsonBuilder &builder);
#endif
#ifdef USE_SWITCH
json::SerializationBuffer<> switch_json_(switch_::Switch *obj, bool value, JsonDetail start_config);
void switch_json_(switch_::Switch *obj, bool value, JsonDetail start_config, json::JsonBuilder &builder);
#endif
#ifdef USE_BUTTON
json::SerializationBuffer<> button_json_(button::Button *obj, JsonDetail start_config);
void button_json_(button::Button *obj, JsonDetail start_config, json::JsonBuilder &builder);
#endif
#ifdef USE_BINARY_SENSOR
json::SerializationBuffer<> binary_sensor_json_(binary_sensor::BinarySensor *obj, bool value,
JsonDetail start_config);
void binary_sensor_json_(binary_sensor::BinarySensor *obj, bool value, JsonDetail start_config,
json::JsonBuilder &builder);
#endif
#ifdef USE_FAN
json::SerializationBuffer<> fan_json_(fan::Fan *obj, JsonDetail start_config);
void fan_json_(fan::Fan *obj, JsonDetail start_config, json::JsonBuilder &builder);
#endif
#ifdef USE_LIGHT
json::SerializationBuffer<> light_json_(light::LightState *obj, JsonDetail start_config);
void light_json_(light::LightState *obj, JsonDetail start_config, json::JsonBuilder &builder);
#endif
#ifdef USE_TEXT_SENSOR
json::SerializationBuffer<> text_sensor_json_(text_sensor::TextSensor *obj, const std::string &value,
JsonDetail start_config);
void text_sensor_json_(text_sensor::TextSensor *obj, const std::string &value, JsonDetail start_config,
json::JsonBuilder &builder);
#endif
#ifdef USE_COVER
json::SerializationBuffer<> cover_json_(cover::Cover *obj, JsonDetail start_config);
void cover_json_(cover::Cover *obj, JsonDetail start_config, json::JsonBuilder &builder);
#endif
#ifdef USE_NUMBER
json::SerializationBuffer<> number_json_(number::Number *obj, float value, JsonDetail start_config);
void number_json_(number::Number *obj, float value, JsonDetail start_config, json::JsonBuilder &builder);
#endif
#ifdef USE_DATETIME_DATE
json::SerializationBuffer<> date_json_(datetime::DateEntity *obj, JsonDetail start_config);
void date_json_(datetime::DateEntity *obj, JsonDetail start_config, json::JsonBuilder &builder);
#endif
#ifdef USE_DATETIME_TIME
json::SerializationBuffer<> time_json_(datetime::TimeEntity *obj, JsonDetail start_config);
void time_json_(datetime::TimeEntity *obj, JsonDetail start_config, json::JsonBuilder &builder);
#endif
#ifdef USE_DATETIME_DATETIME
json::SerializationBuffer<> datetime_json_(datetime::DateTimeEntity *obj, JsonDetail start_config);
void datetime_json_(datetime::DateTimeEntity *obj, JsonDetail start_config, json::JsonBuilder &builder);
#endif
#ifdef USE_TEXT
json::SerializationBuffer<> text_json_(text::Text *obj, const std::string &value, JsonDetail start_config);
void text_json_(text::Text *obj, const std::string &value, JsonDetail start_config, json::JsonBuilder &builder);
#endif
#ifdef USE_SELECT
json::SerializationBuffer<> select_json_(select::Select *obj, StringRef value, JsonDetail start_config);
void select_json_(select::Select *obj, StringRef value, JsonDetail start_config, json::JsonBuilder &builder);
#endif
#ifdef USE_CLIMATE
json::SerializationBuffer<> climate_json_(climate::Climate *obj, JsonDetail start_config);
void climate_json_(climate::Climate *obj, JsonDetail start_config, json::JsonBuilder &builder);
#endif
#ifdef USE_LOCK
json::SerializationBuffer<> lock_json_(lock::Lock *obj, lock::LockState value, JsonDetail start_config);
void lock_json_(lock::Lock *obj, lock::LockState value, JsonDetail start_config, json::JsonBuilder &builder);
#endif
#ifdef USE_VALVE
json::SerializationBuffer<> valve_json_(valve::Valve *obj, JsonDetail start_config);
void valve_json_(valve::Valve *obj, JsonDetail start_config, json::JsonBuilder &builder);
#endif
#ifdef USE_ALARM_CONTROL_PANEL
json::SerializationBuffer<> alarm_control_panel_json_(alarm_control_panel::AlarmControlPanel *obj,
alarm_control_panel::AlarmControlPanelState value,
JsonDetail start_config);
void alarm_control_panel_json_(alarm_control_panel::AlarmControlPanel *obj,
alarm_control_panel::AlarmControlPanelState value, JsonDetail start_config,
json::JsonBuilder &builder);
#endif
#ifdef USE_EVENT
json::SerializationBuffer<> event_json_(event::Event *obj, StringRef event_type, JsonDetail start_config);
void event_json_(event::Event *obj, StringRef event_type, JsonDetail start_config, json::JsonBuilder &builder);
#endif
#ifdef USE_WATER_HEATER
json::SerializationBuffer<> water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config);
void water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config, json::JsonBuilder &builder);
#endif
#ifdef USE_INFRARED
json::SerializationBuffer<> infrared_json_(infrared::Infrared *obj, JsonDetail start_config);
void infrared_json_(infrared::Infrared *obj, JsonDetail start_config, json::JsonBuilder &builder);
#endif
#ifdef USE_RADIO_FREQUENCY
json::SerializationBuffer<> radio_frequency_json_(radio_frequency::RadioFrequency *obj, JsonDetail start_config);
void radio_frequency_json_(radio_frequency::RadioFrequency *obj, JsonDetail start_config, json::JsonBuilder &builder);
#endif
#ifdef USE_UPDATE
json::SerializationBuffer<> update_json_(update::UpdateEntity *obj, JsonDetail start_config);
void update_json_(update::UpdateEntity *obj, JsonDetail start_config, json::JsonBuilder &builder);
#endif
};
@@ -0,0 +1,66 @@
#include "sse_chunk.h"
#include <cinttypes>
#include <cstring>
#include "esphome/core/helpers.h"
namespace esphome::web_server_idf {
void for_each_chunk_piece(const char *message, size_t message_len, ChunkPieceSink sink, void *ctx) {
if (message == nullptr) {
sink(ctx, CHUNK_END, CHUNK_END_LEN);
return;
}
const char *pos = message;
const char *end = message + message_len;
for (;;) {
const size_t remaining = end - pos;
const auto *n = static_cast<const char *>(memchr(pos, '\n', remaining));
// Only a \r before the next \n can end this line, so the search stops there instead of
// rescanning the rest of the message for every line
const auto *r = static_cast<const char *>(memchr(pos, '\r', n != nullptr ? n - pos : remaining));
if (n == nullptr && r == nullptr) {
sink(ctx, pos, remaining);
break;
}
const char *brk = (r != nullptr && (n == nullptr || r < n)) ? r : n;
sink(ctx, pos, brk - pos);
pos = brk + ((brk == r && brk + 1 == n) ? 2 : 1);
if (pos >= end) {
break;
}
sink(ctx, SSE_SEP, SSE_SEP_LEN);
}
sink(ctx, SSE_SUFFIX, SSE_SUFFIX_LEN);
}
size_t build_chunk_prefix(char *buf, size_t size, const char *event, uint32_t id, uint32_t reconnect, bool with_data) {
size_t len = CHUNK_HDR_LEN;
if (reconnect) {
len = buf_append_printf(buf, size, len, "retry: %" PRIu32 "\r\n", reconnect);
}
if (id) {
len = buf_append_printf(buf, size, len, "id: %" PRIu32 "\r\n", id);
}
if (event && *event) {
len = buf_append_str(buf, size, len, "event: ");
len = buf_append_str(buf, size, len, event);
len = buf_append_str(buf, size, len, "\r\n");
}
if (with_data) {
len = buf_append_str(buf, size, len, "data: ");
}
return len;
}
void write_chunk_header(char *buf, size_t chunk_len) {
// Eight lowercase hex digits; the temp keeps the terminator format_hex_to writes out of buf
char digits[9];
format_hex_to(digits, static_cast<uint32_t>(chunk_len));
std::memcpy(buf, digits, 8);
buf[8] = '\r';
buf[9] = '\n';
}
} // namespace esphome::web_server_idf
@@ -0,0 +1,40 @@
#pragma once
#include <cstddef>
#include <cstdint>
namespace esphome::web_server_idf {
// Wire framing of one Server-Sent-Events event inside the chunked /events response:
// "%08x\r\n" + fields + "data: " line ["\r\ndata: " line]... "\r\n" + "\r\n" + "\r\n"
// The chunk header and the final CRLF (the chunk terminator) do not count toward the
// chunk length; everything between them does.
// HTTP chunk header "%08x\r\n"
constexpr size_t CHUNK_HDR_LEN = 10;
// Chunk terminator, also the only piece of a null message (no data line, no blank line)
constexpr char CHUNK_END[] = "\r\n";
constexpr size_t CHUNK_END_LEN = sizeof(CHUNK_END) - 1;
// Between two data lines: the end of one and the prefix of the next
constexpr char SSE_SEP[] = "\r\ndata: ";
constexpr size_t SSE_SEP_LEN = sizeof(SSE_SEP) - 1;
// End of the last data line, the blank line ending the event, and the chunk terminator
constexpr char SSE_SUFFIX[] = "\r\n\r\n\r\n";
constexpr size_t SSE_SUFFIX_LEN = sizeof(SSE_SUFFIX) - 1;
// Receives one piece of the chunk; ctx is whatever the caller passed to for_each_chunk_piece()
using ChunkPieceSink = void (*)(void *ctx, const char *piece, size_t len);
// Calls sink for each piece after the prefix: data lines split on \n, \r or \r\n with SSE_SEP
// between them and SSE_SUFFIX after the last (as ESPAsyncWebServer: a trailing break adds no
// empty line, an inner one is kept). A null message yields only the chunk terminator.
void for_each_chunk_piece(const char *message, size_t message_len, ChunkPieceSink sink, void *ctx);
// Writes the chunk header placeholder, the retry/id/event lines and, with_data, the first
// "data: " into buf. Returns the prefix length; size - 1 or more means the event name did not fit.
size_t build_chunk_prefix(char *buf, size_t size, const char *event, uint32_t id, uint32_t reconnect, bool with_data);
// Fills the header placeholder at the start of buf once the chunk length is known
void write_chunk_header(char *buf, size_t chunk_len);
} // namespace esphome::web_server_idf
@@ -1,7 +1,9 @@
#ifdef USE_ESP32
#include <algorithm>
#include <cstdarg>
#include <memory>
#include <cstdio>
#include <cstring>
#include <cctype>
#include <cinttypes>
@@ -15,6 +17,7 @@
#include <freertos/task.h>
#include "utils.h"
#include "sse_chunk.h"
#include "web_server_idf.h"
#ifdef USE_WEBSERVER_AUTH_DIGEST
@@ -740,7 +743,8 @@ bool AsyncEventSource::loop() {
++i;
}
}
return !this->sessions_.empty();
// A session still waiting for httpd to commit its context keeps the loop alive too
return !this->sessions_.empty() || this->has_pending_sessions_.load(std::memory_order_acquire);
}
void AsyncEventSource::adopt_pending_sessions_main_loop_() {
@@ -756,6 +760,14 @@ void AsyncEventSource::adopt_pending_sessions_main_loop_() {
delete rsp; // NOLINT(cppcoreguidelines-owning-memory)
continue;
}
// httpd commits the session context only after the creating handler returns, so stay
// pending until then; httpd_req_cleanup() always commits it or calls destroy()
if (httpd_sess_get_ctx(rsp->hd_, rsp->fd_.load()) != rsp) {
LockGuard guard{this->pending_mutex_};
this->pending_sessions_.push_back(rsp);
this->has_pending_sessions_.store(true, std::memory_order_release);
continue;
}
this->sessions_.push_back(rsp);
// Prime first so on_connect_ observes a session that has already sent its
// initial ping/config/sorting_groups, matching the pre-refactor ordering.
@@ -792,7 +804,7 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest *
esphome::web_server_idf::AsyncEventSource *server,
esphome::web_server::WebServer *ws)
: server_(server), web_server_(ws), entities_iterator_(ws, server) {
// Httpd task only. start_session_main_loop_() handles event_buffer_ / iterator setup.
// Httpd task only. start_session_main_loop_() sends the greeting and starts the iterator.
httpd_req_t *req = *request;
httpd_resp_set_status(req, HTTPD_200);
@@ -820,9 +832,14 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest *
void AsyncEventSourceResponse::start_session_main_loop_() {
auto *ws = this->web_server_;
// tcp send buffer is empty on connect, so these should always go through
// The tcp send buffer is empty on connect. A refusal is a closing session or a failed tail
// allocation; nothing retries the greeting, so close and let the client reconnect.
auto message = ws->get_config_json();
this->try_send_nodefer(message.c_str(), message.size(), "ping", millis(), 30000);
if (!this->try_send_nodefer(message.c_str(), message.size(), "ping", millis(), 30000)) {
ESP_LOGW(TAG, "Config not sent to fd %d", this->fd_.load());
this->request_close_();
return;
}
#ifdef USE_WEBSERVER_SORTING
for (auto &group : ws->sorting_groups_) {
@@ -833,8 +850,13 @@ void AsyncEventSourceResponse::start_session_main_loop_() {
message = builder.serialize();
// a (very) large number of these should be able to be queued initially without defer
// since the only thing in the send buffer at this point is the initial ping/config
this->try_send_nodefer(message.c_str(), message.size(), "sorting_group");
// since the only thing in the send buffer at this point is the initial ping/config.
// A refusal means the socket is full or closing; the remaining groups are not sent. The
// session stays up on purpose: partial grouping beats a reconnect loop on a slow link.
if (!this->try_send_nodefer(message.c_str(), message.size(), "sorting_group")) {
ESP_LOGW(TAG, "Sorting groups not sent to fd %d", this->fd_.load());
break;
}
}
#endif
@@ -871,8 +893,9 @@ void AsyncEventSourceResponse::process_deferred_queue_() {
}
while (!deferred_queue_.empty()) {
DeferredEvent &de = deferred_queue_.front();
auto message = de.message_generator_(web_server_, de.source_);
if (this->try_send_nodefer(message.c_str(), message.size(), "state")) {
json::JsonBuilder builder;
de.message_generator_(web_server_, de.source_, builder);
if (this->send_json_(builder)) {
if (this->close_requested_ || deferred_queue_.empty()) {
return;
}
@@ -888,8 +911,10 @@ void AsyncEventSourceResponse::request_close_() {
if (!this->close_requested_) {
this->close_requested_ = true;
this->deferred_queue_.clear();
this->event_buffer_.clear();
this->event_bytes_sent_ = 0;
this->tail_.reset();
this->tail_cap_ = 0;
this->tail_len_ = 0;
this->tail_sent_ = 0;
this->next_close_attempt_ms_ = App.get_loop_component_start_time();
}
@@ -940,21 +965,43 @@ void AsyncEventSourceResponse::close_session_work(void *arg) {
response->close_work_queued_.store(false, std::memory_order_release);
}
void AsyncEventSourceResponse::process_buffer_() {
if (this->close_requested_ || event_buffer_.empty()) {
return;
ssize_t AsyncEventSourceResponse::send_(struct iovec *iov, int iovcnt) {
// httpd frees a session before closing its socket, so the fd may already be a new client's.
// Treated as would-block; the stall timer ends a session that never becomes ours again.
const int fd = this->fd_.load();
if (httpd_sess_get_ctx(this->hd_, fd) != this) {
return 0;
}
if (event_bytes_sent_ == event_buffer_.size()) {
event_buffer_.resize(0);
event_bytes_sent_ = 0;
return;
struct msghdr msg {};
msg.msg_iov = iov;
msg.msg_iovlen = iovcnt;
const ssize_t sent = sendmsg(fd, &msg, MSG_DONTWAIT);
if (sent >= 0) {
return sent;
}
const int err = errno;
if (err == EAGAIN || err == EWOULDBLOCK) {
return 0;
}
ESP_LOGD(TAG, "send error: errno %d", err);
this->request_close_();
return -1;
}
size_t remaining = event_buffer_.size() - event_bytes_sent_;
int bytes_sent =
httpd_socket_send(this->hd_, this->fd_.load(), event_buffer_.c_str() + event_bytes_sent_, remaining, 0);
if (bytes_sent == HTTPD_SOCK_ERR_TIMEOUT) {
// EAGAIN/EWOULDBLOCK - socket buffer full, try again later
void AsyncEventSourceResponse::drain_tail_() {
if (this->sending_ || this->close_requested_ || this->tail_len_ == 0) {
return;
}
SendGuard guard{*this};
const size_t remaining = this->tail_len_ - this->tail_sent_;
struct iovec iov = {this->tail_.get() + this->tail_sent_, remaining};
const ssize_t sent = this->send_(&iov, 1);
if (sent < 0) {
return;
}
if (sent == 0) {
// Socket buffer full, try again later
// NOTE: Similar logic exists in web_server/web_server.cpp in DeferredUpdateEventSource::process_deferred_queue_().
// The IDF path is intentionally time-based and closes through HTTPD to preserve session ownership.
const uint32_t now = App.get_loop_component_start_time();
@@ -968,32 +1015,62 @@ void AsyncEventSourceResponse::process_buffer_() {
}
return;
}
if (bytes_sent == HTTPD_SOCK_ERR_FAIL) {
// Low-level asynchronous sends do not make HTTPD close the session automatically.
this->request_close_();
return;
}
if (bytes_sent <= 0) {
// Unexpected error or zero bytes sent
ESP_LOGW(TAG, "Unexpected send result: %d", bytes_sent);
this->request_close_();
return;
}
// Successful send - reset stall tracking
this->send_failure_started_ms_ = 0;
event_bytes_sent_ += bytes_sent;
// Log partial sends for debugging
if (event_bytes_sent_ < event_buffer_.size()) {
ESP_LOGV(TAG, "Partial send: %d/%zu bytes (total: %zu/%zu)", bytes_sent, remaining, event_bytes_sent_,
event_buffer_.size());
this->tail_sent_ += sent;
if (this->tail_sent_ < this->tail_len_) {
ESP_LOGV(TAG, "Partial send: %zd/%zu bytes (total: %u/%u)", sent, remaining, this->tail_sent_, this->tail_len_);
return;
}
// Fully sent; the storage stays for the next stall
this->tail_len_ = 0;
}
if (event_bytes_sent_ == event_buffer_.size()) {
event_buffer_.resize(0);
event_bytes_sent_ = 0;
bool AsyncEventSourceResponse::reserve_tail_(size_t len) {
if (this->tail_cap_ >= len) {
return true;
}
if (len > TAIL_MAX_SIZE) {
return false;
}
// Nothing is pending while the tail grows, so free the old block first. PREFER_INTERNAL keeps
// the tail where plain new put it.
this->tail_.reset();
this->tail_ = RAMAllocator<uint8_t>(RAMAllocator<uint8_t>::PREFER_INTERNAL).make_unique_array_for_overwrite(len);
this->tail_cap_ = this->tail_ ? len : 0;
return this->tail_cap_ != 0;
}
bool AsyncEventSourceResponse::stash_chunk_(const char *prefix, size_t prefix_len, const char *message,
size_t message_len, size_t total, size_t sent) {
// A log event of nothing but line breaks is the largest chunk; reserve_tail_() must never
// refuse it for size, only for memory
static_assert(SSE_SEP_LEN * web_server::LOG_EVENT_MAX_LEN + PREFIX_BUF_SIZE + SSE_SUFFIX_LEN <= TAIL_MAX_SIZE,
"the log cut in web_server.h must keep a worst case log event inside the tail ceiling");
if (!this->reserve_tail_(total)) {
if (sent != 0) {
// Part of the chunk is on the wire, so the stream is broken and the client has to go
ESP_LOGW(TAG, "Cannot buffer a %zu byte chunk, closing", total);
this->request_close_();
} else {
this->tail_alloc_failed_(total); // nothing on the wire, the caller retries on the stall clock
}
return false;
}
uint8_t *dst = this->tail_.get();
std::memcpy(dst, prefix, prefix_len);
dst += prefix_len;
for_each_chunk_piece(
message, message_len,
[](void *ctx, const char *piece, size_t len) {
auto &out = *static_cast<uint8_t **>(ctx);
std::memcpy(out, piece, len);
out += len;
},
&dst);
this->tail_len_ = total;
this->tail_sent_ = sent;
return true;
}
void AsyncEventSourceResponse::loop() {
@@ -1001,7 +1078,7 @@ void AsyncEventSourceResponse::loop() {
this->process_close_();
return;
}
process_buffer_();
drain_tail_();
process_deferred_queue_();
if (this->close_requested_)
return;
@@ -1009,142 +1086,138 @@ void AsyncEventSourceResponse::loop() {
this->entities_iterator_.try_advance(1);
}
bool AsyncEventSourceResponse::send_json_(json::JsonBuilder &builder) {
char buf[JSON_BUF_SIZE];
const size_t len = builder.serialize_to(buf, sizeof(buf));
if (len < sizeof(buf)) {
return this->try_send_nodefer(buf, len, "state");
}
// Too large for the stack: the tail holds the whole chunk and loop() drains it. Serialized
// JSON has no raw line break, so the body is one data line.
if (!this->ready_to_send_()) {
return false;
}
{
SendGuard guard{*this};
char prefix[PREFIX_BUF_SIZE];
const size_t prefix_len = build_chunk_prefix(prefix, sizeof(prefix), "state", 0, 0, true);
// Grow the tail until the document fits. Nothing has reached the wire, so a document that
// cannot be held costs only this event, and the tail grown for it is released.
size_t json_len = 0;
size_t cap = std::max<size_t>(JSON_BUF_SIZE * 2, this->tail_cap_);
for (;;) {
if (!this->reserve_tail_(cap)) {
this->tail_alloc_failed_(cap);
return false; // stays deferred, retried on a later pass
}
const size_t room = cap - prefix_len - SSE_SUFFIX_LEN;
json_len = builder.serialize_to(reinterpret_cast<char *>(this->tail_.get()) + prefix_len, room);
if (json_len < room) {
break;
}
if (cap >= TAIL_MAX_SIZE) {
ESP_LOGW(TAG, "State event does not fit %zu bytes, dropped", room);
this->tail_.reset();
this->tail_cap_ = 0;
this->send_failure_started_ms_ = 0;
return true; // would never fit, reported as sent
}
cap = std::min<size_t>(cap * 2, TAIL_MAX_SIZE);
}
const size_t total = prefix_len + json_len + SSE_SUFFIX_LEN;
write_chunk_header(prefix, total - CHUNK_HDR_LEN - CHUNK_END_LEN);
uint8_t *dst = this->tail_.get();
std::memcpy(dst, prefix, prefix_len);
std::memcpy(dst + prefix_len + json_len, SSE_SUFFIX, SSE_SUFFIX_LEN);
this->tail_len_ = total;
this->tail_sent_ = 0;
}
drain_tail_();
return true;
}
void AsyncEventSourceResponse::tail_alloc_failed_(size_t cap) {
// Same stall clock as a socket that stops draining, so a session cannot retry forever
const uint32_t now = App.get_loop_component_start_time();
if (this->send_failure_started_ms_ == 0) {
this->send_failure_started_ms_ = now != 0 ? now : 1; // Reserve zero for no stall.
ESP_LOGW(TAG, "No memory for a %zu byte chunk", cap);
return;
}
if (static_cast<int32_t>(now - (this->send_failure_started_ms_ + SEND_STALL_TIMEOUT_MS)) >= 0) {
ESP_LOGW(TAG, "Closing EventSource after %" PRIu32 " ms without memory", now - this->send_failure_started_ms_);
this->request_close_();
}
}
bool AsyncEventSourceResponse::ready_to_send_() {
if (this->sending_ || this->fd_.load() == 0 || this->close_requested_) {
return false;
}
drain_tail_();
return !this->close_requested_ && this->tail_len_ == 0;
}
bool AsyncEventSourceResponse::try_send_nodefer(const char *message, size_t message_len, const char *event, uint32_t id,
uint32_t reconnect) {
if (this->fd_.load() == 0 || this->close_requested_) {
if (!this->ready_to_send_()) {
return false;
}
SendGuard guard{*this};
process_buffer_();
if (this->close_requested_ || !event_buffer_.empty()) {
// there is still pending event data to send first
return false;
// Everything after the prefix goes out straight from the caller's buffer
char prefix[PREFIX_BUF_SIZE];
const size_t prefix_len = build_chunk_prefix(prefix, sizeof(prefix), event, id, reconnect, message != nullptr);
if (message == nullptr && prefix_len == CHUNK_HDR_LEN) {
return true; // Match ESPAsyncWebServer: nothing to send
}
// 8 spaces are standing in for the hexidecimal chunk length to print later
const char chunk_len_header[] = " " CRLF_STR;
const int chunk_len_header_len = sizeof(chunk_len_header) - 1;
event_buffer_.append(chunk_len_header);
// Use stack buffer for formatting numeric fields to avoid temporary string allocations
// Size: "retry: " (7) + max uint32 (10 digits) + CRLF (2) + null (1) = 20 bytes, use 32 for safety
constexpr size_t num_buf_size = 32;
char num_buf[num_buf_size];
if (reconnect) {
int len = snprintf(num_buf, num_buf_size, "retry: %" PRIu32 CRLF_STR, reconnect);
event_buffer_.append(num_buf, len);
}
if (id) {
int len = snprintf(num_buf, num_buf_size, "id: %" PRIu32 CRLF_STR, id);
event_buffer_.append(num_buf, len);
}
if (event && *event) {
event_buffer_.append("event: ", sizeof("event: ") - 1);
event_buffer_.append(event);
event_buffer_.append(CRLF_STR, CRLF_LEN);
}
// Match ESPAsyncWebServer: null message means no data lines and no terminating blank line
if (message) {
// SSE spec requires each line of a multi-line message to have its own "data:" prefix
// Handle \n, \r, and \r\n line endings (matching ESPAsyncWebServer behavior)
// Fast path: check if message contains any newlines at all
// Most SSE messages (JSON state updates) have no newlines
const char *first_n = static_cast<const char *>(memchr(message, '\n', message_len));
const char *first_r = static_cast<const char *>(memchr(message, '\r', message_len));
if (first_n == nullptr && first_r == nullptr) {
// No newlines - fast path (most common case)
event_buffer_.append("data: ", sizeof("data: ") - 1);
event_buffer_.append(message, message_len);
event_buffer_.append(CRLF_STR CRLF_STR, CRLF_LEN * 2); // data line + blank line terminator
} else {
// Has newlines - handle multi-line message
const char *line_start = message;
const char *msg_end = message + message_len;
// Reuse the first search results
const char *next_n = first_n;
const char *next_r = first_r;
while (line_start <= msg_end) {
const char *line_end;
const char *next_line;
if (next_n == nullptr && next_r == nullptr) {
// No more line breaks - output remaining text as final line
event_buffer_.append("data: ", sizeof("data: ") - 1);
event_buffer_.append(line_start, msg_end - line_start);
event_buffer_.append(CRLF_STR, CRLF_LEN);
break;
}
// Determine line ending type and next line start
if (next_n != nullptr && next_r != nullptr) {
if (next_r + 1 == next_n) {
// \r\n sequence
line_end = next_r;
next_line = next_n + 1;
} else {
// Mixed \n and \r - use whichever comes first
line_end = (next_r < next_n) ? next_r : next_n;
next_line = line_end + 1;
}
} else if (next_n != nullptr) {
// Unix LF
line_end = next_n;
next_line = next_n + 1;
} else {
// Old Mac CR
line_end = next_r;
next_line = next_r + 1;
}
// Output this line
event_buffer_.append("data: ", sizeof("data: ") - 1);
event_buffer_.append(line_start, line_end - line_start);
event_buffer_.append(CRLF_STR, CRLF_LEN);
line_start = next_line;
// Check if we've consumed all content
if (line_start >= msg_end) {
break;
}
// Search for next newlines only in remaining string
next_n = static_cast<const char *>(memchr(line_start, '\n', msg_end - line_start));
next_r = static_cast<const char *>(memchr(line_start, '\r', msg_end - line_start));
}
// Terminate message with blank line
event_buffer_.append(CRLF_STR, CRLF_LEN);
}
}
if (event_buffer_.size() == static_cast<size_t>(chunk_len_header_len)) {
// Nothing was added, reset buffer
event_buffer_.resize(0);
if (prefix_len >= PREFIX_BUF_SIZE - 1) {
// The appenders truncate silently, which would put a malformed event on the wire
ESP_LOGW(TAG, "Event name too long, dropped");
return true;
}
event_buffer_.append(CRLF_STR, CRLF_LEN);
// Gather list: the prefix, then the data lines and their separators from the message
struct Gather {
struct iovec iov[MAX_SEND_IOV]; // left uninitialized on purpose
int iovcnt{1};
size_t total{0};
bool fits{true};
} g;
g.total = prefix_len;
for_each_chunk_piece(
message, message_len,
[](void *ctx, const char *piece, size_t len) {
auto &g = *static_cast<Gather *>(ctx);
g.total += len;
if (len == 0) {
return;
}
if (g.iovcnt == MAX_SEND_IOV) {
g.fits = false;
return;
}
g.iov[g.iovcnt++] = {const_cast<char *>(piece), len};
},
&g);
// The header and the terminator are not part of the chunk length
write_chunk_header(prefix, g.total - CHUNK_HDR_LEN - CHUNK_END_LEN);
g.iov[0] = {prefix, prefix_len};
// chunk length header itself and the final chunk terminating CRLF are not counted as part of the chunk
int chunk_len = event_buffer_.size() - CRLF_LEN - chunk_len_header_len;
char chunk_len_str[9];
snprintf(chunk_len_str, 9, "%08x", chunk_len);
std::memcpy(&event_buffer_[0], chunk_len_str, 8);
event_bytes_sent_ = 0;
process_buffer_();
return true;
// A message with more lines than the list holds skips straight to the tail
const ssize_t sent = g.fits ? this->send_(g.iov, g.iovcnt) : 0;
if (sent < 0) {
return false;
}
if (static_cast<size_t>(sent) == g.total) {
this->send_failure_started_ms_ = 0; // progress, whichever stall clock was running
return true;
}
// The caller's buffers do not outlive this call, so keep the chunk and continue from loop()
return this->stash_chunk_(prefix, prefix_len, message, message_len, g.total, sent);
}
void AsyncEventSourceResponse::deferrable_send_state(void *source, const char *event_type,
@@ -1165,20 +1238,22 @@ void AsyncEventSourceResponse::deferrable_send_state(void *source, const char *e
ESP_LOGE(TAG, "Can't defer non-state event");
}
process_buffer_();
drain_tail_();
process_deferred_queue_();
if (this->close_requested_) {
return;
}
if (!event_buffer_.empty() || !deferred_queue_.empty()) {
if (this->tail_len_ != 0 || !deferred_queue_.empty()) {
// outgoing event buffer or deferred queue still not empty which means downstream tcp send buffer full, no point
// trying to send first
deq_push_back_with_dedup_(source, message_generator);
} else {
auto message = message_generator(web_server_, source);
if (!this->try_send_nodefer(message.c_str(), message.size(), "state")) {
json::JsonBuilder builder;
message_generator(web_server_, source, builder);
// A send error closes the session and clears the queue; nothing is queued after that
if (!this->send_json_(builder) && !this->close_requested_) {
deq_push_back_with_dedup_(source, message_generator);
}
}
@@ -18,8 +18,11 @@
#ifdef USE_WEBSERVER
#include "esphome/components/json/json_util.h"
#include "esphome/components/web_server/list_entities.h"
#include "sse_chunk.h"
#endif
struct iovec; // NOLINT(readability-identifier-naming) - forward decl of lwip's gather list entry
namespace esphome {
#ifdef USE_WEBSERVER
namespace web_server {
@@ -255,7 +258,7 @@ class AsyncWebHandler {
class AsyncEventSource;
class AsyncEventSourceResponse;
using message_generator_t = json::SerializationBuffer<>(esphome::web_server::WebServer *, void *);
using message_generator_t = void(esphome::web_server::WebServer *, void *, json::JsonBuilder &);
/*
This class holds a pointer to the source component that wants to publish a state event, and a pointer to a function
@@ -300,8 +303,30 @@ class AsyncEventSourceResponse {
void deq_push_back_with_dedup_(void *source, message_generator_t *message_generator);
void process_deferred_queue_();
void process_buffer_();
// A new chunk may go out: not re-entered from a log line, session alive, tail empty
bool ready_to_send_();
// Non-blocking gather write. Returns bytes written, 0 on would-block or a socket that is not
// ours, -1 after requesting the close on any other error.
ssize_t send_(struct iovec *iov, int iovcnt);
// Push what is left of the chunk in tail_ to the socket; owns the stall timer.
void drain_tail_();
// Grow tail_ to hold len bytes, kept at its high-water mark. False on OOM.
bool reserve_tail_(size_t len);
// Keep the whole chunk in tail_ and continue from sent; false when the tail cannot be allocated.
bool stash_chunk_(const char *prefix, size_t prefix_len, const char *message, size_t message_len, size_t total,
size_t sent);
// Send a state event; JSON too large for the stack buffer is serialized into tail_ instead
bool send_json_(json::JsonBuilder &builder);
// Warn once, and close the session once the stall timeout passes with no memory for the tail
void tail_alloc_failed_(size_t cap);
void request_close_();
// A log line emitted inside a send re-enters try_send_nodefer on this session; refuse it
struct SendGuard {
AsyncEventSourceResponse &owner;
explicit SendGuard(AsyncEventSourceResponse &owner) : owner(owner) { owner.sending_ = true; }
~SendGuard() { this->owner.sending_ = false; }
};
void process_close_();
static void close_session_work(void *arg);
@@ -317,15 +342,33 @@ class AsyncEventSourceResponse {
std::vector<DeferredEvent> deferred_queue_;
esphome::web_server::WebServer *web_server_;
esphome::web_server::ListEntitiesIterator entities_iterator_;
std::string event_buffer_;
size_t event_bytes_sent_;
// One chunk the socket did not take whole, allocated on the first stall; the only heap use
// on the send path
RAMUniquePtr<uint8_t[]> tail_;
uint32_t send_failure_started_ms_{0}; // Zero means no send stall in progress.
uint32_t next_close_attempt_ms_{0};
// Main-loop only; the HTTPD task never reads or writes this flag.
bool close_requested_{false};
bool close_retry_warning_logged_{false};
uint16_t tail_cap_{0};
uint16_t tail_len_{0}; // Zero means nothing pending
uint16_t tail_sent_{0};
// Set on the main loop before queueing close work, cleared by the HTTPD-task callback when done.
std::atomic<bool> close_work_queued_{false};
// Main-loop only; the HTTPD task never reads or writes these flags.
bool close_requested_{false};
bool close_retry_warning_logged_{false};
bool sending_{false};
// The longest multi line log message in the tree (a climate dump_config) has 22 lines; a
// longer one goes through the tail
static constexpr size_t MAX_SEND_LINES = 22;
static constexpr size_t MAX_SEND_IOV = 1 + 2 * MAX_SEND_LINES;
// Chunk header, retry/id/event lines and the first "data: "
static constexpr size_t PREFIX_BUF_SIZE = 128;
// Stack buffer for a state event's JSON; a larger document is serialized into the tail
static constexpr size_t JSON_BUF_SIZE = 1024;
// Same ceiling JsonBuilder::serialize() applies (max_heap_size in json_util.cpp); a larger
// document is dropped before anything is on the wire
static constexpr size_t JSON_MAX_SIZE = 5120;
// Most RAM a stalled session keeps: the largest state document plus any accepted framing
static constexpr size_t TAIL_MAX_SIZE = JSON_MAX_SIZE + PREFIX_BUF_SIZE + SSE_SUFFIX_LEN;
static constexpr uint32_t SEND_STALL_TIMEOUT_MS = 20000;
static constexpr uint32_t CLOSE_RETRY_INTERVAL_MS = 250;
static constexpr uint32_t CLOSE_CONFIRM_INTERVAL_MS = 1000;
@@ -0,0 +1,131 @@
#include <gtest/gtest.h>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <random>
#include <string>
#include "esphome/components/web_server_idf/sse_chunk.h"
namespace esphome::web_server_idf::testing {
// The chunk as try_send_nodefer lays it out: header, fields, first "data: ", then the pieces
static std::string build_chunk(const char *message, size_t message_len, const char *event, uint32_t id,
uint32_t reconnect) {
char prefix[128];
const size_t prefix_len = build_chunk_prefix(prefix, sizeof(prefix), event, id, reconnect, message != nullptr);
if (message == nullptr && prefix_len == CHUNK_HDR_LEN) {
return "";
}
std::string out(prefix, prefix_len);
for_each_chunk_piece(
message, message_len,
[](void *ctx, const char *piece, size_t len) { static_cast<std::string *>(ctx)->append(piece, len); }, &out);
write_chunk_header(prefix, out.size() - CHUNK_HDR_LEN - CHUNK_END_LEN);
out.replace(0, CHUNK_HDR_LEN, prefix, CHUNK_HDR_LEN);
return out;
}
// The std::string builder this framing replaced, kept as the reference for the wire format
static std::string reference_chunk(const char *message, size_t message_len, const char *event, uint32_t id,
uint32_t reconnect) {
std::string buf = " \r\n";
char num[32];
if (reconnect)
buf.append(num, snprintf(num, sizeof(num), "retry: %u\r\n", reconnect));
if (id)
buf.append(num, snprintf(num, sizeof(num), "id: %u\r\n", id));
if (event && *event)
buf.append("event: ").append(event).append("\r\n");
if (message) {
const char *line_start = message;
const char *msg_end = message + message_len;
const char *next_n = static_cast<const char *>(memchr(message, '\n', message_len));
const char *next_r = static_cast<const char *>(memchr(message, '\r', message_len));
if (next_n == nullptr && next_r == nullptr) {
buf.append("data: ").append(message, message_len).append("\r\n\r\n");
} else {
while (line_start <= msg_end) {
const char *line_end;
const char *next_line;
if (next_n == nullptr && next_r == nullptr) {
buf.append("data: ").append(line_start, msg_end - line_start).append("\r\n");
break;
}
if (next_n != nullptr && next_r != nullptr) {
if (next_r + 1 == next_n) {
line_end = next_r;
next_line = next_n + 1;
} else {
line_end = (next_r < next_n) ? next_r : next_n;
next_line = line_end + 1;
}
} else if (next_n != nullptr) {
line_end = next_n;
next_line = next_n + 1;
} else {
line_end = next_r;
next_line = next_r + 1;
}
buf.append("data: ").append(line_start, line_end - line_start).append("\r\n");
line_start = next_line;
if (line_start >= msg_end)
break;
next_n = static_cast<const char *>(memchr(line_start, '\n', msg_end - line_start));
next_r = static_cast<const char *>(memchr(line_start, '\r', msg_end - line_start));
}
buf.append("\r\n");
}
}
if (buf.size() == 10)
return "";
buf.append("\r\n");
char len[9];
snprintf(len, sizeof(len), "%08x", static_cast<unsigned>(buf.size() - 2 - 10));
buf.replace(0, 8, len, 8);
return buf;
}
static void expect_same(const std::string &message, const char *event, uint32_t id, uint32_t reconnect) {
const std::string got = build_chunk(message.data(), message.size(), event, id, reconnect);
const std::string want = reference_chunk(message.data(), message.size(), event, id, reconnect);
EXPECT_EQ(got, want) << "message=[" << message << "] event=" << (event ? event : "null") << " id=" << id
<< " retry=" << reconnect;
}
TEST(SseChunk, NullMessageHasNoDataLineAndNoBlankLine) {
EXPECT_EQ(build_chunk(nullptr, 0, "ping", 7, 30000), reference_chunk(nullptr, 0, "ping", 7, 30000));
EXPECT_EQ(build_chunk(nullptr, 0, "ping", 7, 30000), "00000022\r\nretry: 30000\r\nid: 7\r\nevent: ping\r\n\r\n");
EXPECT_EQ(build_chunk(nullptr, 0, nullptr, 0, 0), "");
}
TEST(SseChunk, SingleLine) {
EXPECT_EQ(build_chunk("{}", 2, "state", 0, 0), "0000001a\r\nevent: state\r\ndata: {}\r\n\r\n\r\n");
expect_same("", "state", 0, 0);
expect_same(R"({"id":"light-x"})", "state_detail_all", 0, 0);
}
TEST(SseChunk, LineBreaks) {
for (const char *m : {"a\n", "a\r", "a\r\n", "\n", "\r\n", "\r", "a\n\nb", "a\r\rb", "a\n\rb", "a\r\n\r\nb", "\n\n",
"x\r\n", "\r\nx", "one\ntwo\nthree", "tail\r\n\r\n"}) {
expect_same(m, "log", 1234, 0);
}
EXPECT_EQ(build_chunk("a\r\nb", 4, nullptr, 0, 0), "00000014\r\ndata: a\r\ndata: b\r\n\r\n\r\n");
EXPECT_EQ(build_chunk("a\n\rb", 4, nullptr, 0, 0), "0000001c\r\ndata: a\r\ndata: \r\ndata: b\r\n\r\n\r\n");
}
TEST(SseChunk, MatchesReferenceOnRandomMessages) {
std::mt19937 rng(1234); // NOLINT(cert-msc32-c,cert-msc51-cpp,bugprone-random-generator-seed) reproducible
const char *events[] = {nullptr, "", "ping", "state", "log", "state_detail_all", "sorting_group"};
const char alphabet[] = "ab{}\":,\n\r ";
for (int i = 0; i < 20000; i++) {
std::string m;
const size_t len = rng() % 120;
for (size_t k = 0; k < len; k++)
m += alphabet[rng() % (sizeof(alphabet) - 1)];
expect_same(m, events[rng() % 7], (rng() % 3 == 0) ? 0 : rng(), (rng() % 4 == 0) ? 30000 : 0);
}
}
} // namespace esphome::web_server_idf::testing