mirror of
https://github.com/esphome/esphome.git
synced 2026-10-01 17:00:21 +00:00
[web_server_idf] Send events straight from the caller's buffers instead of a std::string (#19376)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user