ESPHome 2026.5.1
Loading...
Searching...
No Matches
web_server.cpp
Go to the documentation of this file.
1#include "web_server.h"
2#ifdef USE_WEBSERVER
11#include "esphome/core/log.h"
12#include "esphome/core/util.h"
13
14#if !defined(USE_ESP32) && defined(USE_ARDUINO)
15#include "StreamString.h"
16#endif
17
18#include <cstdlib>
19
20#ifdef USE_LIGHT
22#endif
23
24#ifdef USE_LOGGER
26#endif
27
28#ifdef USE_CLIMATE
30#endif
31
32#ifdef USE_UPDATE
34#endif
35
36#ifdef USE_WATER_HEATER
38#endif
39
40#ifdef USE_INFRARED
42#endif
43#ifdef USE_RADIO_FREQUENCY
45#endif
46
47#ifdef USE_WEBSERVER_LOCAL
48#if USE_WEBSERVER_VERSION == 2
49#include "server_index_v2.h"
50#elif USE_WEBSERVER_VERSION == 3
51#include "server_index_v3.h"
52#endif
53#endif
54
55namespace esphome::web_server {
56
57static const char *const TAG = "web_server";
58
59// Longest: UPDATE AVAILABLE (16 chars + null terminator, rounded up)
60static constexpr size_t PSTR_LOCAL_SIZE = 18;
61#define PSTR_LOCAL(mode_s) ESPHOME_strncpy_P(buf, (ESPHOME_PGM_P) ((mode_s)), PSTR_LOCAL_SIZE - 1)
62
63// Parse URL and return match info
64// URL formats (disambiguated by HTTP method for 3-segment case):
65// GET /{domain}/{entity_name} - main device state
66// POST /{domain}/{entity_name}/{action} - main device action
67// GET /{domain}/{device_name}/{entity_name} - sub-device state (USE_DEVICES only)
68// POST /{domain}/{device_name}/{entity_name}/{action} - sub-device action (USE_DEVICES only)
69static UrlMatch match_url(const char *url_ptr, size_t url_len, bool only_domain, bool is_post = false) {
70 // URL must start with '/' and have content after it
71 if (url_len < 2 || url_ptr[0] != '/')
72 return UrlMatch{};
73
74 const char *p = url_ptr + 1;
75 const char *end = url_ptr + url_len;
76
77 // Helper to find next segment: returns pointer after '/' or nullptr if no more slashes
78 auto next_segment = [&end](const char *start) -> const char * {
79 const char *slash = (const char *) memchr(start, '/', end - start);
80 return slash ? slash + 1 : nullptr;
81 };
82
83 // Helper to make StringRef from segment start to next segment (or end)
84 auto make_ref = [&end](const char *start, const char *next_start) -> StringRef {
85 return StringRef(start, (next_start ? next_start - 1 : end) - start);
86 };
87
88 // Parse domain segment
89 const char *s1 = p;
90 const char *s2 = next_segment(s1);
91
92 // Must have domain with trailing slash
93 if (!s2)
94 return UrlMatch{};
95
96 UrlMatch match{};
97 match.domain = make_ref(s1, s2);
98 match.valid = true;
99
100 if (only_domain || s2 >= end)
101 return match;
102
103 // Parse remaining segments only when needed
104 const char *s3 = next_segment(s2);
105 const char *s4 = s3 ? next_segment(s3) : nullptr;
106
107 StringRef seg2 = make_ref(s2, s3);
108 StringRef seg3 = s3 ? make_ref(s3, s4) : StringRef();
109 StringRef seg4 = s4 ? make_ref(s4, nullptr) : StringRef();
110
111 // Reject empty segments
112 if (seg2.empty() || (s3 && seg3.empty()) || (s4 && seg4.empty()))
113 return UrlMatch{};
114
115 // Interpret based on segment count
116 if (!s3) {
117 // 1 segment after domain: /{domain}/{entity}
118 match.id = seg2;
119 } else if (!s4) {
120 // 2 segments after domain: /{domain}/{X}/{Y}
121 // HTTP method disambiguates: GET = device/entity, POST = entity/action
122 if (is_post) {
123 match.id = seg2;
124 match.method = seg3;
125 return match;
126 }
127#ifdef USE_DEVICES
128 match.device_name = seg2;
129 match.id = seg3;
130#else
131 return UrlMatch{}; // 3-segment GET not supported without USE_DEVICES
132#endif
133 } else {
134 // 3 segments after domain: /{domain}/{device}/{entity}/{action}
135#ifdef USE_DEVICES
136 if (!is_post) {
137 return UrlMatch{}; // 4-segment GET not supported (action requires POST)
138 }
139 match.device_name = seg2;
140 match.id = seg3;
141 match.method = seg4;
142#else
143 return UrlMatch{}; // Not supported without USE_DEVICES
144#endif
145 }
146
147 return match;
148}
149
151 EntityMatchResult result{false, this->method.empty()};
152
153#ifdef USE_DEVICES
154 Device *entity_device = entity->get_device();
155 bool url_has_device = !this->device_name.empty();
156 bool entity_has_device = (entity_device != nullptr);
157
158 // Device matching: URL device segment must match entity's device
159 if (url_has_device != entity_has_device) {
160 return result; // Mismatch: one has device, other doesn't
161 }
162 if (url_has_device && this->device_name != entity_device->get_name()) {
163 return result; // Device name doesn't match
164 }
165#endif
166
167 // Try matching by entity name (new format)
168 if (this->id == entity->get_name()) {
169 result.matched = true;
170 return result;
171 }
172
173 // Fall back to object_id (deprecated format)
174 char object_id_buf[OBJECT_ID_MAX_LEN];
175 StringRef object_id = entity->get_object_id_to(object_id_buf);
176 if (this->id == object_id) {
177 result.matched = true;
178 // Log deprecation warning
179#ifdef USE_DEVICES
180 Device *device = entity->get_device();
181 if (device != nullptr) {
182 ESP_LOGW(TAG,
183 "Deprecated URL format: /%.*s/%.*s/%.*s - use entity name '/%.*s/%s/%s' instead. "
184 "Object ID URLs will be removed in 2026.7.0.",
185 (int) this->domain.size(), this->domain.c_str(), (int) this->device_name.size(),
186 this->device_name.c_str(), (int) this->id.size(), this->id.c_str(), (int) this->domain.size(),
187 this->domain.c_str(), device->get_name(), entity->get_name().c_str());
188 } else
189#endif
190 {
191 ESP_LOGW(TAG,
192 "Deprecated URL format: /%.*s/%.*s - use entity name '/%.*s/%s' instead. "
193 "Object ID URLs will be removed in 2026.7.0.",
194 (int) this->domain.size(), this->domain.c_str(), (int) this->id.size(), this->id.c_str(),
195 (int) this->domain.size(), this->domain.c_str(), entity->get_name().c_str());
196 }
197 }
198
199 return result;
200}
201
202#if !defined(USE_ESP32) && defined(USE_ARDUINO)
203// helper for allowing only unique entries in the queue
204void __attribute__((flatten))
206 DeferredEvent item(source, message_generator);
207
208 // Use range-based for loop instead of std::find_if to reduce template instantiation overhead and binary size
209 for (auto &event : this->deferred_queue_) {
210 if (event == item) {
211 return; // Already in queue, no need to update since items are equal
212 }
213 }
214 this->deferred_queue_.push_back(item);
215}
216
218 while (!deferred_queue_.empty()) {
219 DeferredEvent &de = deferred_queue_.front();
220 auto message = de.message_generator_(web_server_, de.source_);
221 if (this->send(message.c_str(), "state") != DISCARDED) {
222 // O(n) but memory efficiency is more important than speed here which is why std::vector was chosen
223 deferred_queue_.erase(deferred_queue_.begin());
224 this->consecutive_send_failures_ = 0; // Reset failure count on successful send
225 } else {
226 // NOTE: Similar logic exists in web_server_idf/web_server_idf.cpp in AsyncEventSourceResponse::process_buffer_()
227 // The implementations differ due to platform-specific APIs (DISCARDED vs HTTPD_SOCK_ERR_TIMEOUT, close() vs
228 // fd_.store(0)), but the failure counting and timeout logic should be kept in sync. If you change this logic,
229 // also update the ESP-IDF implementation.
232 // Too many failures, connection is likely dead
233 ESP_LOGW(TAG, "Closing stuck EventSource connection after %" PRIu16 " failed sends",
235 this->close();
236 this->deferred_queue_.clear();
237 }
238 break;
239 }
240 }
241}
242
248
249void DeferredUpdateEventSource::deferrable_send_state(void *source, const char *event_type,
250 message_generator_t *message_generator) {
251 // Skip if no connected clients to avoid unnecessary deferred queue processing
252 if (this->count() == 0)
253 return;
254
255 // allow all json "details_all" to go through before publishing bare state events, this avoids unnamed entries showing
256 // up in the web GUI and reduces event load during initial connect
257 if (!entities_iterator_.completed() && 0 != strcmp(event_type, "state_detail_all"))
258 return;
259
260 if (source == nullptr)
261 return;
262 if (event_type == nullptr)
263 return;
264 if (message_generator == nullptr)
265 return;
266
267 if (0 != strcmp(event_type, "state_detail_all") && 0 != strcmp(event_type, "state")) {
268 ESP_LOGE(TAG, "Can't defer non-state event");
269 }
270
271 if (!deferred_queue_.empty())
273 if (!deferred_queue_.empty()) {
274 // deferred queue still not empty which means downstream event queue full, no point trying to send first
275 deq_push_back_with_dedup_(source, message_generator);
276 } else {
277 auto message = message_generator(web_server_, source);
278 if (this->send(message.c_str(), "state") == DISCARDED) {
279 deq_push_back_with_dedup_(source, message_generator);
280 } else {
281 this->consecutive_send_failures_ = 0; // Reset failure count on successful send
282 }
283 }
284}
285
286// used for logs plus the initial ping/config
287void DeferredUpdateEventSource::try_send_nodefer(const char *message, const char *event, uint32_t id,
288 uint32_t reconnect) {
289 this->send(message, event, id, reconnect);
290}
291
293 for (DeferredUpdateEventSource *dues : *this) {
294 dues->loop();
295 }
296 return !this->empty();
297}
298
299void DeferredUpdateEventSourceList::deferrable_send_state(void *source, const char *event_type,
300 message_generator_t *message_generator) {
301 // Skip if no event sources (no connected clients) to avoid unnecessary iteration
302 if (this->empty())
303 return;
304 for (DeferredUpdateEventSource *dues : *this) {
305 dues->deferrable_send_state(source, event_type, message_generator);
306 }
307}
308
310 uint32_t reconnect) {
311 for (DeferredUpdateEventSource *dues : *this) {
312 dues->try_send_nodefer(message, event, id, reconnect);
313 }
314}
315
316void DeferredUpdateEventSourceList::add_new_client(WebServer *ws, AsyncWebServerRequest *request) {
318 this->push_back(es);
319
320 es->onConnect([this, es](AsyncEventSourceClient *client) { this->on_client_connect_(es); });
321
322 es->onDisconnect([this, es](AsyncEventSourceClient *client) { this->on_client_disconnect_(es); });
323
324 es->handleRequest(request);
326}
327
329 WebServer *ws = source->web_server_;
330 ws->defer([ws, source]() {
331 // Configure reconnect timeout and send config
332 // this should always go through since the AsyncEventSourceClient event queue is empty on connect
333 auto message = ws->get_config_json();
334 source->try_send_nodefer(message.c_str(), "ping", millis(), 30000);
335
336#ifdef USE_WEBSERVER_SORTING
337 for (auto &group : ws->sorting_groups_) {
338 json::JsonBuilder builder;
339 JsonObject root = builder.root();
340 root[ESPHOME_F("name")] = group.second.name;
341 root[ESPHOME_F("sorting_weight")] = group.second.weight;
342 auto group_msg = builder.serialize();
343
344 // up to 31 groups should be able to be queued initially without defer
345 source->try_send_nodefer(group_msg.c_str(), "sorting_group");
346 }
347#endif
348
350
351 // just dump them all up-front and take advantage of the deferred queue
352 // on second thought that takes too long, but leaving the commented code here for debug purposes
353 // while(!source->entities_iterator_.completed()) {
354 // source->entities_iterator_.advance();
355 //}
356 });
357}
358
360 source->web_server_->defer([this, source]() {
361 // This method was called via WebServer->defer() and is no longer executing in the
362 // context of the network callback. The object is now dead and can be safely deleted.
363 this->remove(source);
364 delete source; // NOLINT
365 });
366}
367#endif
368
370
371#ifdef USE_WEBSERVER_CSS_INCLUDE
372void WebServer::set_css_include(const char *css_include) { this->css_include_ = css_include; }
373#endif
374#ifdef USE_WEBSERVER_JS_INCLUDE
375void WebServer::set_js_include(const char *js_include) { this->js_include_ = js_include; }
376#endif
377
379 json::JsonBuilder builder;
380 JsonObject root = builder.root();
381
382 root[ESPHOME_F("title")] = App.get_friendly_name().empty() ? App.get_name().c_str() : App.get_friendly_name().c_str();
383 char comment_buffer[Application::ESPHOME_COMMENT_SIZE_MAX];
384 App.get_comment_string(comment_buffer);
385 root[ESPHOME_F("comment")] = comment_buffer;
386#if defined(USE_WEBSERVER_OTA_DISABLED) || !defined(USE_WEBSERVER_OTA)
387 root[ESPHOME_F("ota")] = false; // Note: USE_WEBSERVER_OTA_DISABLED only affects web_server, not captive_portal
388#else
389 root[ESPHOME_F("ota")] = true;
390#endif
391 root[ESPHOME_F("log")] = this->expose_log_;
392 root[ESPHOME_F("lang")] = "en";
393 root[ESPHOME_F("uptime")] = static_cast<uint32_t>(millis_64() / 1000);
394
395 return builder.serialize();
396}
397
400 this->base_->init();
401
402#ifdef USE_LOGGER
403 if (logger::global_logger != nullptr && this->expose_log_) {
405 this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) {
406 static_cast<WebServer *>(self)->on_log(level, tag, message, message_len);
407 });
408 }
409#endif
410
411#ifdef USE_ESP32
412 this->base_->add_handler(&this->events_);
413#endif
414 this->base_->add_handler(this);
415
416 // OTA is now handled by the web_server OTA platform
417
418 // doesn't need defer functionality - if the queue is full, the client JS knows it's alive because it's clearly
419 // getting a lot of events
420 this->set_interval(10000, [this]() {
421 if (this->events_.empty())
422 return;
423 char buf[32];
424 auto uptime = static_cast<uint32_t>(millis_64() / 1000);
425 buf_append_printf(buf, sizeof(buf), 0, "{\"uptime\":%" PRIu32 "}", uptime);
426 this->events_.try_send_nodefer(buf, "ping", millis(), 30000);
427 });
428}
430 // No SSE clients connected; stop looping until a new client connects via
431 // enable_loop_soon_any_context(). This is safe because:
432 // - set_interval/set_timeout/defer run via the Scheduler, independent of loop()
433 // - deferrable_send_state early-outs when no clients are connected
434 // - try_send_nodefer (log, ping) iterates sessions which are empty
435 // - REST API handlers use defer() which runs via the Scheduler
436 if (!this->events_.loop())
437 this->disable_loop();
438}
439
440#ifdef USE_LOGGER
441void WebServer::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) {
442 (void) level;
443 (void) tag;
444 (void) message_len;
445 this->events_.try_send_nodefer(message, "log", millis());
446}
447#endif
448
450 ESP_LOGCONFIG(TAG,
451 "Web Server:\n"
452 " Address: %s:%u",
454}
456
457#ifdef USE_WEBSERVER_LOCAL
458void WebServer::handle_index_request(AsyncWebServerRequest *request) {
459#ifndef USE_ESP8266
460 AsyncWebServerResponse *response = request->beginResponse(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ));
461#else
462 AsyncWebServerResponse *response = request->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ));
463#endif
464#ifdef USE_WEBSERVER_GZIP
465 response->addHeader(ESPHOME_F("Content-Encoding"), ESPHOME_F("gzip"));
466#else
467 response->addHeader(ESPHOME_F("Content-Encoding"), ESPHOME_F("br"));
468#endif
469 request->send(response);
470}
471#elif USE_WEBSERVER_VERSION >= 2
472void WebServer::handle_index_request(AsyncWebServerRequest *request) {
473#ifndef USE_ESP8266
474 AsyncWebServerResponse *response =
475 request->beginResponse(200, "text/html", ESPHOME_WEBSERVER_INDEX_HTML, ESPHOME_WEBSERVER_INDEX_HTML_SIZE);
476#else
477 AsyncWebServerResponse *response =
478 request->beginResponse_P(200, "text/html", ESPHOME_WEBSERVER_INDEX_HTML, ESPHOME_WEBSERVER_INDEX_HTML_SIZE);
479#endif
480 // No gzip header here because the HTML file is so small
481 request->send(response);
482}
483#endif
484
485#ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS
486void WebServer::handle_pna_cors_request(AsyncWebServerRequest *request) {
487 AsyncWebServerResponse *response = request->beginResponse(200, ESPHOME_F(""));
488 response->addHeader(ESPHOME_F("Access-Control-Allow-Private-Network"), ESPHOME_F("true"));
489 response->addHeader(ESPHOME_F("Private-Network-Access-Name"), App.get_name().c_str());
490 char mac_s[18];
491 response->addHeader(ESPHOME_F("Private-Network-Access-ID"), get_mac_address_pretty_into_buffer(mac_s));
492 request->send(response);
493}
494#endif
495
496#ifdef USE_WEBSERVER_CSS_INCLUDE
497void WebServer::handle_css_request(AsyncWebServerRequest *request) {
498#ifndef USE_ESP8266
499 AsyncWebServerResponse *response =
500 request->beginResponse(200, "text/css", ESPHOME_WEBSERVER_CSS_INCLUDE, ESPHOME_WEBSERVER_CSS_INCLUDE_SIZE);
501#else
502 AsyncWebServerResponse *response =
503 request->beginResponse_P(200, "text/css", ESPHOME_WEBSERVER_CSS_INCLUDE, ESPHOME_WEBSERVER_CSS_INCLUDE_SIZE);
504#endif
505 response->addHeader(ESPHOME_F("Content-Encoding"), ESPHOME_F("gzip"));
506 request->send(response);
507}
508#endif
509
510#ifdef USE_WEBSERVER_JS_INCLUDE
511void WebServer::handle_js_request(AsyncWebServerRequest *request) {
512#ifndef USE_ESP8266
513 AsyncWebServerResponse *response =
514 request->beginResponse(200, "text/javascript", ESPHOME_WEBSERVER_JS_INCLUDE, ESPHOME_WEBSERVER_JS_INCLUDE_SIZE);
515#else
516 AsyncWebServerResponse *response =
517 request->beginResponse_P(200, "text/javascript", ESPHOME_WEBSERVER_JS_INCLUDE, ESPHOME_WEBSERVER_JS_INCLUDE_SIZE);
518#endif
519 response->addHeader(ESPHOME_F("Content-Encoding"), ESPHOME_F("gzip"));
520 request->send(response);
521}
522#endif
523
524// Helper functions to reduce code size by avoiding macro expansion
525// Build unique id as: {domain}/{device_name}/{entity_name} or {domain}/{entity_name}
526// Uses names (not object_id) to avoid UTF-8 collision issues
527static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, JsonDetail start_config) {
528 const StringRef &name = obj->get_name();
529 size_t prefix_len = strlen(prefix);
530 size_t name_len = name.size();
531
532#ifdef USE_DEVICES
533 Device *device = obj->get_device();
534 const char *device_name = device ? device->get_name() : nullptr;
535 size_t device_len = device_name ? strlen(device_name) : 0;
536#endif
537
538 // Single stack buffer for both id formats - ArduinoJson copies the string before we overwrite
539 // Buffer sizes use constants from entity_base.h validated in core/config.py
540 // Note: Device name uses ESPHOME_FRIENDLY_NAME_MAX_LEN (sub-device max 120), not ESPHOME_DEVICE_NAME_MAX_LEN
541 // (hostname)
542 // Without USE_DEVICES: legacy id ({prefix}-{object_id}) is the largest format
543 // With USE_DEVICES: name_id ({prefix}/{device}/{name}) is the largest format
544 static constexpr size_t LEGACY_ID_SIZE = ESPHOME_DOMAIN_MAX_LEN + 1 + OBJECT_ID_MAX_LEN;
545#ifdef USE_DEVICES
546 static constexpr size_t ID_BUF_SIZE =
547 std::max(ESPHOME_DOMAIN_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1,
548 LEGACY_ID_SIZE);
549#else
550 static constexpr size_t ID_BUF_SIZE =
551 std::max(ESPHOME_DOMAIN_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1, LEGACY_ID_SIZE);
552#endif
553 char id_buf[ID_BUF_SIZE];
554 memcpy(id_buf, prefix, prefix_len); // NOLINT(bugprone-not-null-terminated-result)
555
556 // name_id: new format {prefix}/{device?}/{name} - frontend should prefer this
557 // Remove in 2026.8.0 when id switches to new format permanently
558 char *p = id_buf + prefix_len;
559 *p++ = '/';
560#ifdef USE_DEVICES
561 if (device_name) {
562 memcpy(p, device_name, device_len);
563 p += device_len;
564 *p++ = '/';
565 }
566#endif
567 memcpy(p, name.c_str(), name_len);
568 p[name_len] = '\0';
569 root[ESPHOME_F("name_id")] = id_buf;
570
571 // id: old format {prefix}-{object_id} for backward compatibility
572 // Will switch to new format in 2026.8.0 - reuses prefix already in id_buf
573 id_buf[prefix_len] = '-';
574 obj->write_object_id_to(id_buf + prefix_len + 1, ID_BUF_SIZE - prefix_len - 1);
575 root[ESPHOME_F("id")] = id_buf;
576
577 if (start_config == DETAIL_ALL) {
578 root[ESPHOME_F("domain")] = prefix;
579 // Use .c_str() to avoid instantiating set<StringRef> template (saves ~24B)
580 root[ESPHOME_F("name")] = name.c_str();
581#ifdef USE_DEVICES
582 if (device_name) {
583 root[ESPHOME_F("device")] = device_name;
584 }
585#endif
586#ifdef USE_ENTITY_ICON
587 char icon_buf[MAX_ICON_LENGTH];
588 root[ESPHOME_F("icon")] = obj->get_icon_to(icon_buf);
589#endif
590 root[ESPHOME_F("entity_category")] = obj->get_entity_category();
591 bool is_disabled = obj->is_disabled_by_default();
592 if (is_disabled)
593 root[ESPHOME_F("is_disabled_by_default")] = is_disabled;
594 }
595}
596
597// Keep as separate function even though only used once: reduces code size by ~48 bytes
598// by allowing compiler to share code between template instantiations (bool, float, etc.)
599template<typename T>
600static void set_json_value(JsonObject &root, EntityBase *obj, const char *prefix, const T &value,
601 JsonDetail start_config) {
602 set_json_id(root, obj, prefix, start_config);
603 root[ESPHOME_F("value")] = value;
604}
605
606template<typename T>
607static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const char *prefix, const char *state,
608 const T &value, JsonDetail start_config) {
609 set_json_value(root, obj, prefix, value, start_config);
610 root[ESPHOME_F("state")] = state;
611}
612
613// Helper to get request detail parameter
614[[maybe_unused]] static JsonDetail get_request_detail(AsyncWebServerRequest *request) {
615 return request->arg(ESPHOME_F("detail")) == "all" ? DETAIL_ALL : DETAIL_STATE;
616}
617
618#ifdef USE_SENSOR
620 if (!this->include_internal_ && obj->is_internal())
621 return;
622 this->events_.deferrable_send_state(obj, "state", sensor_state_json_generator);
623}
624void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) {
625 for (sensor::Sensor *obj : App.get_sensors()) {
626 auto entity_match = match.match_entity(obj);
627 if (!entity_match.matched)
628 continue;
629 // Note: request->method() is always HTTP_GET here (canHandle ensures this)
630 if (entity_match.action_is_empty) {
631 auto detail = get_request_detail(request);
632 auto data = this->sensor_json_(obj, obj->state, detail);
633 request->send(200, "application/json", data.c_str());
634 return;
635 }
636 }
637 request->send(404);
638}
640 return web_server->sensor_json_((sensor::Sensor *) (source), ((sensor::Sensor *) (source))->state, DETAIL_STATE);
641}
643 return web_server->sensor_json_((sensor::Sensor *) (source), ((sensor::Sensor *) (source))->state, DETAIL_ALL);
644}
645json::SerializationBuffer<> WebServer::sensor_json_(sensor::Sensor *obj, float value, JsonDetail start_config) {
646 json::JsonBuilder builder;
647 JsonObject root = builder.root();
648
649 const auto uom_ref = obj->get_unit_of_measurement_ref();
650 char buf[VALUE_ACCURACY_MAX_LEN];
651 const char *state = std::isnan(value)
652 ? "NA"
653 : (value_accuracy_with_uom_to_buf(buf, value, obj->get_accuracy_decimals(), uom_ref), buf);
654 set_json_icon_state_value(root, obj, "sensor", state, value, start_config);
655 if (start_config == DETAIL_ALL) {
656 this->add_sorting_info_(root, obj);
657 if (!uom_ref.empty())
658 root[ESPHOME_F("uom")] = uom_ref.c_str();
659 }
660
661 return builder.serialize();
662}
663#endif
664
665#ifdef USE_TEXT_SENSOR
667 if (!this->include_internal_ && obj->is_internal())
668 return;
669 this->events_.deferrable_send_state(obj, "state", text_sensor_state_json_generator);
670}
671void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) {
672 for (text_sensor::TextSensor *obj : App.get_text_sensors()) {
673 auto entity_match = match.match_entity(obj);
674 if (!entity_match.matched)
675 continue;
676 // Note: request->method() is always HTTP_GET here (canHandle ensures this)
677 if (entity_match.action_is_empty) {
678 auto detail = get_request_detail(request);
679 auto data = this->text_sensor_json_(obj, obj->state, detail);
680 request->send(200, "application/json", data.c_str());
681 return;
682 }
683 }
684 request->send(404);
685}
687 return web_server->text_sensor_json_((text_sensor::TextSensor *) (source),
689}
691 return web_server->text_sensor_json_((text_sensor::TextSensor *) (source),
692 ((text_sensor::TextSensor *) (source))->state, DETAIL_ALL);
693}
694json::SerializationBuffer<> WebServer::text_sensor_json_(text_sensor::TextSensor *obj, const std::string &value,
695 JsonDetail start_config) {
696 json::JsonBuilder builder;
697 JsonObject root = builder.root();
698
699 set_json_icon_state_value(root, obj, "text_sensor", value.c_str(), value.c_str(), start_config);
700 if (start_config == DETAIL_ALL) {
701 this->add_sorting_info_(root, obj);
702 }
703
704 return builder.serialize();
705}
706#endif
707
708#ifdef USE_SWITCH
710
711static void execute_switch_action(switch_::Switch *obj, SwitchAction action) {
712 switch (action) {
714 obj->toggle();
715 break;
717 obj->turn_on();
718 break;
720 obj->turn_off();
721 break;
722 default:
723 break;
724 }
725}
726
728 if (!this->include_internal_ && obj->is_internal())
729 return;
730 this->events_.deferrable_send_state(obj, "state", switch_state_json_generator);
731}
732void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match) {
733 for (switch_::Switch *obj : App.get_switches()) {
734 auto entity_match = match.match_entity(obj);
735 if (!entity_match.matched)
736 continue;
737
738 if (request->method() == HTTP_GET && entity_match.action_is_empty) {
739 auto detail = get_request_detail(request);
740 auto data = this->switch_json_(obj, obj->state, detail);
741 request->send(200, "application/json", data.c_str());
742 return;
743 }
744
746
747 if (match.method_equals(ESPHOME_F("toggle"))) {
748 action = SWITCH_ACTION_TOGGLE;
749 } else if (match.method_equals(ESPHOME_F("turn_on"))) {
750 action = SWITCH_ACTION_TURN_ON;
751 } else if (match.method_equals(ESPHOME_F("turn_off"))) {
752 action = SWITCH_ACTION_TURN_OFF;
753 }
754
755 if (action != SWITCH_ACTION_NONE) {
756 this->defer([obj, action]() { execute_switch_action(obj, action); });
757 request->send(200);
758 } else {
759 request->send(404);
760 }
761 return;
762 }
763 request->send(404);
764}
766 return web_server->switch_json_((switch_::Switch *) (source), ((switch_::Switch *) (source))->state, DETAIL_STATE);
767}
769 return web_server->switch_json_((switch_::Switch *) (source), ((switch_::Switch *) (source))->state, DETAIL_ALL);
770}
771json::SerializationBuffer<> WebServer::switch_json_(switch_::Switch *obj, bool value, JsonDetail start_config) {
772 json::JsonBuilder builder;
773 JsonObject root = builder.root();
774
775 set_json_icon_state_value(root, obj, "switch", value ? "ON" : "OFF", value, start_config);
776 if (start_config == DETAIL_ALL) {
777 root[ESPHOME_F("assumed_state")] = obj->assumed_state();
778 this->add_sorting_info_(root, obj);
779 }
780
781 return builder.serialize();
782}
783#endif
784
785#ifdef USE_BUTTON
786void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlMatch &match) {
787 for (button::Button *obj : App.get_buttons()) {
788 auto entity_match = match.match_entity(obj);
789 if (!entity_match.matched)
790 continue;
791 if (request->method() == HTTP_GET && entity_match.action_is_empty) {
792 auto detail = get_request_detail(request);
793 auto data = this->button_json_(obj, detail);
794 request->send(200, "application/json", data.c_str());
795 } else if (match.method_equals(ESPHOME_F("press"))) {
796 DEFER_ACTION(obj, obj->press());
797 request->send(200);
798 return;
799 } else {
800 request->send(404);
801 }
802 return;
803 }
804 request->send(404);
805}
807 return web_server->button_json_((button::Button *) (source), DETAIL_ALL);
808}
809json::SerializationBuffer<> WebServer::button_json_(button::Button *obj, JsonDetail start_config) {
810 json::JsonBuilder builder;
811 JsonObject root = builder.root();
812
813 set_json_id(root, obj, "button", start_config);
814 if (start_config == DETAIL_ALL) {
815 this->add_sorting_info_(root, obj);
816 }
817
818 return builder.serialize();
819}
820#endif
821
822#ifdef USE_BINARY_SENSOR
824 if (!this->include_internal_ && obj->is_internal())
825 return;
826 this->events_.deferrable_send_state(obj, "state", binary_sensor_state_json_generator);
827}
828void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) {
829 for (binary_sensor::BinarySensor *obj : App.get_binary_sensors()) {
830 auto entity_match = match.match_entity(obj);
831 if (!entity_match.matched)
832 continue;
833 // Note: request->method() is always HTTP_GET here (canHandle ensures this)
834 if (entity_match.action_is_empty) {
835 auto detail = get_request_detail(request);
836 auto data = this->binary_sensor_json_(obj, obj->state, detail);
837 request->send(200, "application/json", data.c_str());
838 return;
839 }
840 }
841 request->send(404);
842}
844 return web_server->binary_sensor_json_((binary_sensor::BinarySensor *) (source),
846}
848 return web_server->binary_sensor_json_((binary_sensor::BinarySensor *) (source),
850}
851json::SerializationBuffer<> WebServer::binary_sensor_json_(binary_sensor::BinarySensor *obj, bool value,
852 JsonDetail start_config) {
853 json::JsonBuilder builder;
854 JsonObject root = builder.root();
855
856 set_json_icon_state_value(root, obj, "binary_sensor", value ? "ON" : "OFF", value, start_config);
857 if (start_config == DETAIL_ALL) {
858 this->add_sorting_info_(root, obj);
859 }
860
861 return builder.serialize();
862}
863#endif
864
865#ifdef USE_FAN
867 if (!this->include_internal_ && obj->is_internal())
868 return;
869 this->events_.deferrable_send_state(obj, "state", fan_state_json_generator);
870}
871void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match) {
872 for (fan::Fan *obj : App.get_fans()) {
873 auto entity_match = match.match_entity(obj);
874 if (!entity_match.matched)
875 continue;
876
877 if (request->method() == HTTP_GET && entity_match.action_is_empty) {
878 auto detail = get_request_detail(request);
879 auto data = this->fan_json_(obj, detail);
880 request->send(200, "application/json", data.c_str());
881 } else if (match.method_equals(ESPHOME_F("toggle"))) {
882 DEFER_ACTION(obj, obj->toggle().perform());
883 request->send(200);
884 } else {
885 bool is_on = match.method_equals(ESPHOME_F("turn_on"));
886 bool is_off = match.method_equals(ESPHOME_F("turn_off"));
887 if (!is_on && !is_off) {
888 request->send(404);
889 return;
890 }
891 auto call = is_on ? obj->turn_on() : obj->turn_off();
892
893 parse_num_param_(request, ESPHOME_F("speed_level"), call, &decltype(call)::set_speed);
894
895 if (request->hasArg(ESPHOME_F("oscillation"))) {
896 auto speed = request->arg(ESPHOME_F("oscillation"));
897 auto val = parse_on_off(speed.c_str());
898 switch (val) {
899 case PARSE_ON:
900 call.set_oscillating(true);
901 break;
902 case PARSE_OFF:
903 call.set_oscillating(false);
904 break;
905 case PARSE_TOGGLE:
906 call.set_oscillating(!obj->oscillating);
907 break;
908 case PARSE_NONE:
909 request->send(404);
910 return;
911 }
912 }
913 DEFER_ACTION(call, call.perform());
914 request->send(200);
915 }
916 return;
917 }
918 request->send(404);
919}
921 return web_server->fan_json_((fan::Fan *) (source), DETAIL_STATE);
922}
924 return web_server->fan_json_((fan::Fan *) (source), DETAIL_ALL);
925}
926json::SerializationBuffer<> WebServer::fan_json_(fan::Fan *obj, JsonDetail start_config) {
927 json::JsonBuilder builder;
928 JsonObject root = builder.root();
929
930 set_json_icon_state_value(root, obj, "fan", obj->state ? "ON" : "OFF", obj->state, start_config);
931 const auto traits = obj->get_traits();
932 if (traits.supports_speed()) {
933 root[ESPHOME_F("speed_level")] = obj->speed;
934 root[ESPHOME_F("speed_count")] = traits.supported_speed_count();
935 }
936 if (obj->get_traits().supports_oscillation())
937 root[ESPHOME_F("oscillation")] = obj->oscillating;
938 if (start_config == DETAIL_ALL) {
939 this->add_sorting_info_(root, obj);
940 }
941
942 return builder.serialize();
943}
944#endif
945
946#ifdef USE_LIGHT
948 if (!this->include_internal_ && obj->is_internal())
949 return;
950 this->events_.deferrable_send_state(obj, "state", light_state_json_generator);
951}
952void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match) {
953 for (light::LightState *obj : App.get_lights()) {
954 auto entity_match = match.match_entity(obj);
955 if (!entity_match.matched)
956 continue;
957
958 if (request->method() == HTTP_GET && entity_match.action_is_empty) {
959 auto detail = get_request_detail(request);
960 auto data = this->light_json_(obj, detail);
961 request->send(200, "application/json", data.c_str());
962 } else if (match.method_equals(ESPHOME_F("toggle"))) {
963 DEFER_ACTION(obj, obj->toggle().perform());
964 request->send(200);
965 } else {
966 bool is_on = match.method_equals(ESPHOME_F("turn_on"));
967 bool is_off = match.method_equals(ESPHOME_F("turn_off"));
968 if (!is_on && !is_off) {
969 request->send(404);
970 return;
971 }
972 auto call = is_on ? obj->turn_on() : obj->turn_off();
973
974 if (is_on) {
975 // Parse color parameters
976 parse_light_param_(request, ESPHOME_F("brightness"), call, &decltype(call)::set_brightness, 255.0f);
977 parse_light_param_(request, ESPHOME_F("r"), call, &decltype(call)::set_red, 255.0f);
978 parse_light_param_(request, ESPHOME_F("g"), call, &decltype(call)::set_green, 255.0f);
979 parse_light_param_(request, ESPHOME_F("b"), call, &decltype(call)::set_blue, 255.0f);
980 parse_light_param_(request, ESPHOME_F("white_value"), call, &decltype(call)::set_white, 255.0f);
981 parse_light_param_(request, ESPHOME_F("color_temp"), call, &decltype(call)::set_color_temperature);
982
983 // Parse timing parameters
984 parse_light_param_uint_(request, ESPHOME_F("flash"), call, &decltype(call)::set_flash_length, 1000);
985 }
986 parse_light_param_uint_(request, ESPHOME_F("transition"), call, &decltype(call)::set_transition_length, 1000);
987
988 if (is_on) {
990 request, ESPHOME_F("effect"), call,
991 static_cast<light::LightCall &(light::LightCall::*) (const char *, size_t)>(&decltype(call)::set_effect));
992 }
993
994 DEFER_ACTION(call, call.perform());
995 request->send(200);
996 }
997 return;
998 }
999 request->send(404);
1000}
1002 return web_server->light_json_((light::LightState *) (source), DETAIL_STATE);
1003}
1005 return web_server->light_json_((light::LightState *) (source), DETAIL_ALL);
1006}
1007json::SerializationBuffer<> WebServer::light_json_(light::LightState *obj, JsonDetail start_config) {
1008 json::JsonBuilder builder;
1009 JsonObject root = builder.root();
1010
1011 set_json_value(root, obj, "light", obj->remote_values.is_on() ? "ON" : "OFF", start_config);
1012
1014 if (start_config == DETAIL_ALL) {
1015 JsonArray opt = root[ESPHOME_F("effects")].to<JsonArray>();
1016 opt.add("None");
1017 for (auto const &option : obj->get_effects()) {
1018 opt.add(option->get_name());
1019 }
1020 this->add_sorting_info_(root, obj);
1021 }
1022
1023 return builder.serialize();
1024}
1025#endif
1026
1027#ifdef USE_COVER
1029 if (!this->include_internal_ && obj->is_internal())
1030 return;
1031 this->events_.deferrable_send_state(obj, "state", cover_state_json_generator);
1032}
1033void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match) {
1034 for (cover::Cover *obj : App.get_covers()) {
1035 auto entity_match = match.match_entity(obj);
1036 if (!entity_match.matched)
1037 continue;
1038
1039 if (request->method() == HTTP_GET && entity_match.action_is_empty) {
1040 auto detail = get_request_detail(request);
1041 auto data = this->cover_json_(obj, detail);
1042 request->send(200, "application/json", data.c_str());
1043 return;
1044 }
1045
1046 auto call = obj->make_call();
1047
1048 // Lookup table for cover methods
1049 static const struct {
1050 const char *name;
1051 cover::CoverCall &(cover::CoverCall::*action)();
1052 } METHODS[] = {
1057 };
1058
1059 bool found = false;
1060 for (const auto &method : METHODS) {
1061 if (match.method_equals(method.name)) {
1062 (call.*method.action)();
1063 found = true;
1064 break;
1065 }
1066 }
1067
1068 if (!found && !match.method_equals(ESPHOME_F("set"))) {
1069 request->send(404);
1070 return;
1071 }
1072
1073 auto traits = obj->get_traits();
1074 if ((request->hasArg(ESPHOME_F("position")) && !traits.get_supports_position()) ||
1075 (request->hasArg(ESPHOME_F("tilt")) && !traits.get_supports_tilt())) {
1076 request->send(409);
1077 return;
1078 }
1079
1080 parse_num_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position);
1081 parse_num_param_(request, ESPHOME_F("tilt"), call, &decltype(call)::set_tilt);
1082
1083 DEFER_ACTION(call, call.perform());
1084 request->send(200);
1085 return;
1086 }
1087 request->send(404);
1088}
1090 return web_server->cover_json_((cover::Cover *) (source), DETAIL_STATE);
1091}
1093 return web_server->cover_json_((cover::Cover *) (source), DETAIL_ALL);
1094}
1095json::SerializationBuffer<> WebServer::cover_json_(cover::Cover *obj, JsonDetail start_config) {
1096 json::JsonBuilder builder;
1097 JsonObject root = builder.root();
1098
1099 set_json_icon_state_value(root, obj, "cover", obj->is_fully_closed() ? "CLOSED" : "OPEN", obj->position,
1100 start_config);
1101 char buf[PSTR_LOCAL_SIZE];
1102 root[ESPHOME_F("current_operation")] = PSTR_LOCAL(cover::cover_operation_to_str(obj->current_operation));
1103
1104 if (obj->get_traits().get_supports_position())
1105 root[ESPHOME_F("position")] = obj->position;
1106 if (obj->get_traits().get_supports_tilt())
1107 root[ESPHOME_F("tilt")] = obj->tilt;
1108 if (start_config == DETAIL_ALL) {
1109 this->add_sorting_info_(root, obj);
1110 }
1111
1112 return builder.serialize();
1113}
1114#endif
1115
1116#ifdef USE_NUMBER
1118 if (!this->include_internal_ && obj->is_internal())
1119 return;
1120 this->events_.deferrable_send_state(obj, "state", number_state_json_generator);
1121}
1122void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match) {
1123 for (auto *obj : App.get_numbers()) {
1124 auto entity_match = match.match_entity(obj);
1125 if (!entity_match.matched)
1126 continue;
1127
1128 if (request->method() == HTTP_GET && entity_match.action_is_empty) {
1129 auto detail = get_request_detail(request);
1130 auto data = this->number_json_(obj, obj->state, detail);
1131 request->send(200, "application/json", data.c_str());
1132 return;
1133 }
1134 if (!match.method_equals(ESPHOME_F("set"))) {
1135 request->send(404);
1136 return;
1137 }
1138
1139 auto call = obj->make_call();
1140 parse_num_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value);
1141
1142 DEFER_ACTION(call, call.perform());
1143 request->send(200);
1144 return;
1145 }
1146 request->send(404);
1147}
1148
1150 return web_server->number_json_((number::Number *) (source), ((number::Number *) (source))->state, DETAIL_STATE);
1151}
1153 return web_server->number_json_((number::Number *) (source), ((number::Number *) (source))->state, DETAIL_ALL);
1154}
1155json::SerializationBuffer<> WebServer::number_json_(number::Number *obj, float value, JsonDetail start_config) {
1156 json::JsonBuilder builder;
1157 JsonObject root = builder.root();
1158
1159 const auto uom_ref = obj->get_unit_of_measurement_ref();
1160 const int8_t accuracy = step_to_accuracy_decimals(obj->traits.get_step());
1161
1162 // Need two buffers: one for value, one for state with UOM
1163 char val_buf[VALUE_ACCURACY_MAX_LEN];
1164 char state_buf[VALUE_ACCURACY_MAX_LEN];
1165 const char *val_str = std::isnan(value) ? "\"NaN\"" : (value_accuracy_to_buf(val_buf, value, accuracy), val_buf);
1166 const char *state_str =
1167 std::isnan(value) ? "NA" : (value_accuracy_with_uom_to_buf(state_buf, value, accuracy, uom_ref), state_buf);
1168 set_json_icon_state_value(root, obj, "number", state_str, val_str, start_config);
1169 if (start_config == DETAIL_ALL) {
1170 // ArduinoJson copies the string immediately, so we can reuse val_buf
1171 root[ESPHOME_F("min_value")] = (value_accuracy_to_buf(val_buf, obj->traits.get_min_value(), accuracy), val_buf);
1172 root[ESPHOME_F("max_value")] = (value_accuracy_to_buf(val_buf, obj->traits.get_max_value(), accuracy), val_buf);
1173 root[ESPHOME_F("step")] = (value_accuracy_to_buf(val_buf, obj->traits.get_step(), accuracy), val_buf);
1174 root[ESPHOME_F("mode")] = (int) obj->traits.get_mode();
1175 if (!uom_ref.empty())
1176 root[ESPHOME_F("uom")] = uom_ref.c_str();
1177 this->add_sorting_info_(root, obj);
1178 }
1179
1180 return builder.serialize();
1181}
1182#endif
1183
1184#ifdef USE_DATETIME_DATE
1186 if (!this->include_internal_ && obj->is_internal())
1187 return;
1188 this->events_.deferrable_send_state(obj, "state", date_state_json_generator);
1189}
1190void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMatch &match) {
1191 for (auto *obj : App.get_dates()) {
1192 auto entity_match = match.match_entity(obj);
1193 if (!entity_match.matched)
1194 continue;
1195 if (request->method() == HTTP_GET && entity_match.action_is_empty) {
1196 auto detail = get_request_detail(request);
1197 auto data = this->date_json_(obj, detail);
1198 request->send(200, "application/json", data.c_str());
1199 return;
1200 }
1201 if (!match.method_equals(ESPHOME_F("set"))) {
1202 request->send(404);
1203 return;
1204 }
1205
1206 auto call = obj->make_call();
1207
1208 const auto &value = request->arg(ESPHOME_F("value"));
1209 // Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility
1210 if (value.length() == 0) { // NOLINT(readability-container-size-empty)
1211 request->send(409);
1212 return;
1213 }
1214 call.set_date(value.c_str(), value.length());
1215
1216 DEFER_ACTION(call, call.perform());
1217 request->send(200);
1218 return;
1219 }
1220 request->send(404);
1221}
1222
1224 return web_server->date_json_((datetime::DateEntity *) (source), DETAIL_STATE);
1225}
1227 return web_server->date_json_((datetime::DateEntity *) (source), DETAIL_ALL);
1228}
1229json::SerializationBuffer<> WebServer::date_json_(datetime::DateEntity *obj, JsonDetail start_config) {
1230 json::JsonBuilder builder;
1231 JsonObject root = builder.root();
1232
1233 // Format: YYYY-MM-DD (max 10 chars + null)
1234 char value[12];
1235 buf_append_printf(value, sizeof(value), 0, "%d-%02d-%02d", obj->year, obj->month, obj->day);
1236 set_json_icon_state_value(root, obj, "date", value, value, start_config);
1237 if (start_config == DETAIL_ALL) {
1238 this->add_sorting_info_(root, obj);
1239 }
1240
1241 return builder.serialize();
1242}
1243#endif // USE_DATETIME_DATE
1244
1245#ifdef USE_DATETIME_TIME
1247 if (!this->include_internal_ && obj->is_internal())
1248 return;
1249 this->events_.deferrable_send_state(obj, "state", time_state_json_generator);
1250}
1251void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match) {
1252 for (auto *obj : App.get_times()) {
1253 auto entity_match = match.match_entity(obj);
1254 if (!entity_match.matched)
1255 continue;
1256 if (request->method() == HTTP_GET && entity_match.action_is_empty) {
1257 auto detail = get_request_detail(request);
1258 auto data = this->time_json_(obj, detail);
1259 request->send(200, "application/json", data.c_str());
1260 return;
1261 }
1262 if (!match.method_equals(ESPHOME_F("set"))) {
1263 request->send(404);
1264 return;
1265 }
1266
1267 auto call = obj->make_call();
1268
1269 const auto &value = request->arg(ESPHOME_F("value"));
1270 // Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility
1271 if (value.length() == 0) { // NOLINT(readability-container-size-empty)
1272 request->send(409);
1273 return;
1274 }
1275 call.set_time(value.c_str(), value.length());
1276
1277 DEFER_ACTION(call, call.perform());
1278 request->send(200);
1279 return;
1280 }
1281 request->send(404);
1282}
1284 return web_server->time_json_((datetime::TimeEntity *) (source), DETAIL_STATE);
1285}
1287 return web_server->time_json_((datetime::TimeEntity *) (source), DETAIL_ALL);
1288}
1289json::SerializationBuffer<> WebServer::time_json_(datetime::TimeEntity *obj, JsonDetail start_config) {
1290 json::JsonBuilder builder;
1291 JsonObject root = builder.root();
1292
1293 // Format: HH:MM:SS (8 chars + null)
1294 char value[12];
1295 buf_append_printf(value, sizeof(value), 0, "%02d:%02d:%02d", obj->hour, obj->minute, obj->second);
1296 set_json_icon_state_value(root, obj, "time", value, value, start_config);
1297 if (start_config == DETAIL_ALL) {
1298 this->add_sorting_info_(root, obj);
1299 }
1300
1301 return builder.serialize();
1302}
1303#endif // USE_DATETIME_TIME
1304
1305#ifdef USE_DATETIME_DATETIME
1307 if (!this->include_internal_ && obj->is_internal())
1308 return;
1309 this->events_.deferrable_send_state(obj, "state", datetime_state_json_generator);
1310}
1311void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match) {
1312 for (auto *obj : App.get_datetimes()) {
1313 auto entity_match = match.match_entity(obj);
1314 if (!entity_match.matched)
1315 continue;
1316 if (request->method() == HTTP_GET && entity_match.action_is_empty) {
1317 auto detail = get_request_detail(request);
1318 auto data = this->datetime_json_(obj, detail);
1319 request->send(200, "application/json", data.c_str());
1320 return;
1321 }
1322 if (!match.method_equals(ESPHOME_F("set"))) {
1323 request->send(404);
1324 return;
1325 }
1326
1327 auto call = obj->make_call();
1328
1329 const auto &value = request->arg(ESPHOME_F("value"));
1330 // Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility
1331 if (value.length() == 0) { // NOLINT(readability-container-size-empty)
1332 request->send(409);
1333 return;
1334 }
1335 call.set_datetime(value.c_str(), value.length());
1336
1337 DEFER_ACTION(call, call.perform());
1338 request->send(200);
1339 return;
1340 }
1341 request->send(404);
1342}
1344 return web_server->datetime_json_((datetime::DateTimeEntity *) (source), DETAIL_STATE);
1345}
1347 return web_server->datetime_json_((datetime::DateTimeEntity *) (source), DETAIL_ALL);
1348}
1349json::SerializationBuffer<> WebServer::datetime_json_(datetime::DateTimeEntity *obj, JsonDetail start_config) {
1350 json::JsonBuilder builder;
1351 JsonObject root = builder.root();
1352
1353 // Format: YYYY-MM-DD HH:MM:SS (max 19 chars + null)
1354 char value[24];
1355 buf_append_printf(value, sizeof(value), 0, "%d-%02d-%02d %02d:%02d:%02d", obj->year, obj->month, obj->day, obj->hour,
1356 obj->minute, obj->second);
1357 set_json_icon_state_value(root, obj, "datetime", value, value, start_config);
1358 if (start_config == DETAIL_ALL) {
1359 this->add_sorting_info_(root, obj);
1360 }
1361
1362 return builder.serialize();
1363}
1364#endif // USE_DATETIME_DATETIME
1365
1366#ifdef USE_TEXT
1368 if (!this->include_internal_ && obj->is_internal())
1369 return;
1370 this->events_.deferrable_send_state(obj, "state", text_state_json_generator);
1371}
1372void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match) {
1373 for (auto *obj : App.get_texts()) {
1374 auto entity_match = match.match_entity(obj);
1375 if (!entity_match.matched)
1376 continue;
1377
1378 if (request->method() == HTTP_GET && entity_match.action_is_empty) {
1379 auto detail = get_request_detail(request);
1380 auto data = this->text_json_(obj, obj->state, detail);
1381 request->send(200, "application/json", data.c_str());
1382 return;
1383 }
1384 if (!match.method_equals(ESPHOME_F("set"))) {
1385 request->send(404);
1386 return;
1387 }
1388
1389 auto call = obj->make_call();
1391 request, ESPHOME_F("value"), call,
1392 static_cast<text::TextCall &(text::TextCall::*) (const char *, size_t)>(&decltype(call)::set_value));
1393
1394 DEFER_ACTION(call, call.perform());
1395 request->send(200);
1396 return;
1397 }
1398 request->send(404);
1399}
1400
1402 return web_server->text_json_((text::Text *) (source), ((text::Text *) (source))->state, DETAIL_STATE);
1403}
1405 return web_server->text_json_((text::Text *) (source), ((text::Text *) (source))->state, DETAIL_ALL);
1406}
1407json::SerializationBuffer<> WebServer::text_json_(text::Text *obj, const std::string &value, JsonDetail start_config) {
1408 json::JsonBuilder builder;
1409 JsonObject root = builder.root();
1410
1411 const char *state = obj->traits.get_mode() == text::TextMode::TEXT_MODE_PASSWORD ? "********" : value.c_str();
1412 set_json_icon_state_value(root, obj, "text", state, value.c_str(), start_config);
1413 root[ESPHOME_F("min_length")] = obj->traits.get_min_length();
1414 root[ESPHOME_F("max_length")] = obj->traits.get_max_length();
1415 root[ESPHOME_F("pattern")] = obj->traits.get_pattern_c_str();
1416 if (start_config == DETAIL_ALL) {
1417 root[ESPHOME_F("mode")] = (int) obj->traits.get_mode();
1418 this->add_sorting_info_(root, obj);
1419 }
1420
1421 return builder.serialize();
1422}
1423#endif
1424
1425#ifdef USE_SELECT
1427 if (!this->include_internal_ && obj->is_internal())
1428 return;
1429 this->events_.deferrable_send_state(obj, "state", select_state_json_generator);
1430}
1431void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match) {
1432 for (auto *obj : App.get_selects()) {
1433 auto entity_match = match.match_entity(obj);
1434 if (!entity_match.matched)
1435 continue;
1436
1437 if (request->method() == HTTP_GET && entity_match.action_is_empty) {
1438 auto detail = get_request_detail(request);
1439 auto data = this->select_json_(obj, obj->has_state() ? obj->current_option() : StringRef(), detail);
1440 request->send(200, "application/json", data.c_str());
1441 return;
1442 }
1443
1444 if (!match.method_equals(ESPHOME_F("set"))) {
1445 request->send(404);
1446 return;
1447 }
1448
1449 auto call = obj->make_call();
1451 request, ESPHOME_F("option"), call,
1452 static_cast<select::SelectCall &(select::SelectCall::*) (const char *, size_t)>(&decltype(call)::set_option));
1453
1454 DEFER_ACTION(call, call.perform());
1455 request->send(200);
1456 return;
1457 }
1458 request->send(404);
1459}
1461 auto *obj = (select::Select *) (source);
1462 return web_server->select_json_(obj, obj->has_state() ? obj->current_option() : StringRef(), DETAIL_STATE);
1463}
1465 auto *obj = (select::Select *) (source);
1466 return web_server->select_json_(obj, obj->has_state() ? obj->current_option() : StringRef(), DETAIL_ALL);
1467}
1468json::SerializationBuffer<> WebServer::select_json_(select::Select *obj, StringRef value, JsonDetail start_config) {
1469 json::JsonBuilder builder;
1470 JsonObject root = builder.root();
1471
1472 // value points to null-terminated string literals from codegen (via current_option())
1473 set_json_icon_state_value(root, obj, "select", value.c_str(), value.c_str(), start_config);
1474 if (start_config == DETAIL_ALL) {
1475 JsonArray opt = root[ESPHOME_F("option")].to<JsonArray>();
1476 for (auto &option : obj->traits.get_options()) {
1477 opt.add(option);
1478 }
1479 this->add_sorting_info_(root, obj);
1480 }
1481
1482 return builder.serialize();
1483}
1484#endif
1485
1486#ifdef USE_CLIMATE
1488 if (!this->include_internal_ && obj->is_internal())
1489 return;
1490 this->events_.deferrable_send_state(obj, "state", climate_state_json_generator);
1491}
1492void WebServer::handle_climate_request(AsyncWebServerRequest *request, const UrlMatch &match) {
1493 for (auto *obj : App.get_climates()) {
1494 auto entity_match = match.match_entity(obj);
1495 if (!entity_match.matched)
1496 continue;
1497
1498 if (request->method() == HTTP_GET && entity_match.action_is_empty) {
1499 auto detail = get_request_detail(request);
1500 auto data = this->climate_json_(obj, detail);
1501 request->send(200, "application/json", data.c_str());
1502 return;
1503 }
1504
1505 if (!match.method_equals(ESPHOME_F("set"))) {
1506 request->send(404);
1507 return;
1508 }
1509
1510 auto call = obj->make_call();
1511
1512 // Parse string mode parameters
1514 request, ESPHOME_F("mode"), call,
1515 static_cast<climate::ClimateCall &(climate::ClimateCall::*) (const char *, size_t)>(&decltype(call)::set_mode));
1516 parse_cstr_param_(request, ESPHOME_F("fan_mode"), call,
1517 static_cast<climate::ClimateCall &(climate::ClimateCall::*) (const char *, size_t)>(
1518 &decltype(call)::set_fan_mode));
1519 parse_cstr_param_(request, ESPHOME_F("swing_mode"), call,
1520 static_cast<climate::ClimateCall &(climate::ClimateCall::*) (const char *, size_t)>(
1521 &decltype(call)::set_swing_mode));
1522 parse_cstr_param_(request, ESPHOME_F("preset"), call,
1523 static_cast<climate::ClimateCall &(climate::ClimateCall::*) (const char *, size_t)>(
1524 &decltype(call)::set_preset));
1525
1526 // Parse temperature parameters
1527 // static_cast needed to disambiguate overloaded setters (float vs optional<float>)
1528 using ClimateCall = decltype(call);
1529 parse_num_param_(request, ESPHOME_F("target_temperature_high"), call,
1530 static_cast<ClimateCall &(ClimateCall::*) (float)>(&ClimateCall::set_target_temperature_high));
1531 parse_num_param_(request, ESPHOME_F("target_temperature_low"), call,
1532 static_cast<ClimateCall &(ClimateCall::*) (float)>(&ClimateCall::set_target_temperature_low));
1533 parse_num_param_(request, ESPHOME_F("target_temperature"), call,
1534 static_cast<ClimateCall &(ClimateCall::*) (float)>(&ClimateCall::set_target_temperature));
1535
1536 DEFER_ACTION(call, call.perform());
1537 request->send(200);
1538 return;
1539 }
1540 request->send(404);
1541}
1543 // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
1544 return web_server->climate_json_((climate::Climate *) (source), DETAIL_STATE);
1545}
1547 // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
1548 return web_server->climate_json_((climate::Climate *) (source), DETAIL_ALL);
1549}
1550json::SerializationBuffer<> WebServer::climate_json_(climate::Climate *obj, JsonDetail start_config) {
1551 // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
1552 json::JsonBuilder builder;
1553 JsonObject root = builder.root();
1554 set_json_id(root, obj, "climate", start_config);
1555 const auto traits = obj->get_traits();
1556 int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals();
1557 int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals();
1558 char buf[PSTR_LOCAL_SIZE];
1559 char temp_buf[VALUE_ACCURACY_MAX_LEN];
1560
1561 if (start_config == DETAIL_ALL) {
1562 JsonArray opt = root[ESPHOME_F("modes")].to<JsonArray>();
1563 for (climate::ClimateMode m : traits.get_supported_modes())
1564 opt.add(PSTR_LOCAL(climate::climate_mode_to_string(m)));
1565 if (traits.get_supports_fan_modes()) {
1566 JsonArray opt = root[ESPHOME_F("fan_modes")].to<JsonArray>();
1567 for (climate::ClimateFanMode m : traits.get_supported_fan_modes())
1568 opt.add(PSTR_LOCAL(climate::climate_fan_mode_to_string(m)));
1569 }
1570
1571 if (!traits.get_supported_custom_fan_modes().empty()) {
1572 JsonArray opt = root[ESPHOME_F("custom_fan_modes")].to<JsonArray>();
1573 for (auto const &custom_fan_mode : traits.get_supported_custom_fan_modes())
1574 opt.add(custom_fan_mode);
1575 }
1576 if (traits.get_supports_swing_modes()) {
1577 JsonArray opt = root[ESPHOME_F("swing_modes")].to<JsonArray>();
1578 for (auto swing_mode : traits.get_supported_swing_modes())
1580 }
1581 if (traits.get_supports_presets()) {
1582 JsonArray opt = root[ESPHOME_F("presets")].to<JsonArray>();
1583 for (climate::ClimatePreset m : traits.get_supported_presets())
1584 opt.add(PSTR_LOCAL(climate::climate_preset_to_string(m)));
1585 }
1586 if (!traits.get_supported_custom_presets().empty()) {
1587 JsonArray opt = root[ESPHOME_F("custom_presets")].to<JsonArray>();
1588 for (auto const &custom_preset : traits.get_supported_custom_presets())
1589 opt.add(custom_preset);
1590 }
1591 root[ESPHOME_F("max_temp")] =
1592 (value_accuracy_to_buf(temp_buf, traits.get_visual_max_temperature(), target_accuracy), temp_buf);
1593 root[ESPHOME_F("min_temp")] =
1594 (value_accuracy_to_buf(temp_buf, traits.get_visual_min_temperature(), target_accuracy), temp_buf);
1595 root[ESPHOME_F("step")] = traits.get_visual_target_temperature_step();
1596 this->add_sorting_info_(root, obj);
1597 }
1598
1599 bool has_state = false;
1600 root[ESPHOME_F("mode")] = PSTR_LOCAL(climate_mode_to_string(obj->mode));
1601 if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
1602 root[ESPHOME_F("action")] = PSTR_LOCAL(climate_action_to_string(obj->action));
1603 root[ESPHOME_F("state")] = root[ESPHOME_F("action")];
1604 has_state = true;
1605 }
1606 if (traits.get_supports_fan_modes() && obj->fan_mode.has_value()) {
1607 root[ESPHOME_F("fan_mode")] = PSTR_LOCAL(climate_fan_mode_to_string(obj->fan_mode.value()));
1608 }
1609 if (!traits.get_supported_custom_fan_modes().empty() && obj->has_custom_fan_mode()) {
1610 root[ESPHOME_F("custom_fan_mode")] = obj->get_custom_fan_mode();
1611 }
1612 if (traits.get_supports_presets() && obj->preset.has_value()) {
1613 root[ESPHOME_F("preset")] = PSTR_LOCAL(climate_preset_to_string(obj->preset.value()));
1614 }
1615 if (!traits.get_supported_custom_presets().empty() && obj->has_custom_preset()) {
1616 root[ESPHOME_F("custom_preset")] = obj->get_custom_preset();
1617 }
1618 if (traits.get_supports_swing_modes()) {
1619 root[ESPHOME_F("swing_mode")] = PSTR_LOCAL(climate_swing_mode_to_string(obj->swing_mode));
1620 }
1621 if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
1622 root[ESPHOME_F("current_temperature")] =
1623 std::isnan(obj->current_temperature)
1624 ? "NA"
1625 : (value_accuracy_to_buf(temp_buf, obj->current_temperature, current_accuracy), temp_buf);
1626 }
1627 if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
1628 root[ESPHOME_F("current_humidity")] = std::isnan(obj->current_humidity)
1629 ? "NA"
1630 : (value_accuracy_to_buf(temp_buf, obj->current_humidity, 0), temp_buf);
1631 }
1632 if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
1634 root[ESPHOME_F("target_temperature_low")] =
1635 (value_accuracy_to_buf(temp_buf, obj->target_temperature_low, target_accuracy), temp_buf);
1636 root[ESPHOME_F("target_temperature_high")] =
1637 (value_accuracy_to_buf(temp_buf, obj->target_temperature_high, target_accuracy), temp_buf);
1638 if (!has_state) {
1639 root[ESPHOME_F("state")] =
1641 target_accuracy),
1642 temp_buf);
1643 }
1644 } else {
1645 root[ESPHOME_F("target_temperature")] =
1646 (value_accuracy_to_buf(temp_buf, obj->target_temperature, target_accuracy), temp_buf);
1647 if (!has_state)
1648 root[ESPHOME_F("state")] = root[ESPHOME_F("target_temperature")];
1649 }
1650
1651 return builder.serialize();
1652 // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
1653}
1654#endif
1655
1656#ifdef USE_LOCK
1658
1659static void execute_lock_action(lock::Lock *obj, LockAction action) {
1660 switch (action) {
1661 case LOCK_ACTION_LOCK:
1662 obj->lock();
1663 break;
1664 case LOCK_ACTION_UNLOCK:
1665 obj->unlock();
1666 break;
1667 case LOCK_ACTION_OPEN:
1668 obj->open();
1669 break;
1670 default:
1671 break;
1672 }
1673}
1674
1676 if (!this->include_internal_ && obj->is_internal())
1677 return;
1678 this->events_.deferrable_send_state(obj, "state", lock_state_json_generator);
1679}
1680void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match) {
1681 for (lock::Lock *obj : App.get_locks()) {
1682 auto entity_match = match.match_entity(obj);
1683 if (!entity_match.matched)
1684 continue;
1685
1686 if (request->method() == HTTP_GET && entity_match.action_is_empty) {
1687 auto detail = get_request_detail(request);
1688 auto data = this->lock_json_(obj, obj->state, detail);
1689 request->send(200, "application/json", data.c_str());
1690 return;
1691 }
1692
1694
1695 if (match.method_equals(ESPHOME_F("lock"))) {
1696 action = LOCK_ACTION_LOCK;
1697 } else if (match.method_equals(ESPHOME_F("unlock"))) {
1698 action = LOCK_ACTION_UNLOCK;
1699 } else if (match.method_equals(ESPHOME_F("open"))) {
1700 action = LOCK_ACTION_OPEN;
1701 }
1702
1703 if (action != LOCK_ACTION_NONE) {
1704 this->defer([obj, action]() { execute_lock_action(obj, action); });
1705 request->send(200);
1706 } else {
1707 request->send(404);
1708 }
1709 return;
1710 }
1711 request->send(404);
1712}
1714 return web_server->lock_json_((lock::Lock *) (source), ((lock::Lock *) (source))->state, DETAIL_STATE);
1715}
1717 return web_server->lock_json_((lock::Lock *) (source), ((lock::Lock *) (source))->state, DETAIL_ALL);
1718}
1719json::SerializationBuffer<> WebServer::lock_json_(lock::Lock *obj, lock::LockState value, JsonDetail start_config) {
1720 json::JsonBuilder builder;
1721 JsonObject root = builder.root();
1722
1723 char buf[PSTR_LOCAL_SIZE];
1724 set_json_icon_state_value(root, obj, "lock", PSTR_LOCAL(lock::lock_state_to_string(value)), value, start_config);
1725 if (start_config == DETAIL_ALL) {
1726 this->add_sorting_info_(root, obj);
1727 }
1728
1729 return builder.serialize();
1730}
1731#endif
1732
1733#ifdef USE_VALVE
1735 if (!this->include_internal_ && obj->is_internal())
1736 return;
1737 this->events_.deferrable_send_state(obj, "state", valve_state_json_generator);
1738}
1739void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMatch &match) {
1740 for (valve::Valve *obj : App.get_valves()) {
1741 auto entity_match = match.match_entity(obj);
1742 if (!entity_match.matched)
1743 continue;
1744
1745 if (request->method() == HTTP_GET && entity_match.action_is_empty) {
1746 auto detail = get_request_detail(request);
1747 auto data = this->valve_json_(obj, detail);
1748 request->send(200, "application/json", data.c_str());
1749 return;
1750 }
1751
1752 auto call = obj->make_call();
1753
1754 // Lookup table for valve methods
1755 static const struct {
1756 const char *name;
1757 valve::ValveCall &(valve::ValveCall::*action)();
1758 } METHODS[] = {
1763 };
1764
1765 bool found = false;
1766 for (const auto &method : METHODS) {
1767 if (match.method_equals(method.name)) {
1768 (call.*method.action)();
1769 found = true;
1770 break;
1771 }
1772 }
1773
1774 if (!found && !match.method_equals(ESPHOME_F("set"))) {
1775 request->send(404);
1776 return;
1777 }
1778
1779 auto traits = obj->get_traits();
1780 if (request->hasArg(ESPHOME_F("position")) && !traits.get_supports_position()) {
1781 request->send(409);
1782 return;
1783 }
1784
1785 parse_num_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position);
1786
1787 DEFER_ACTION(call, call.perform());
1788 request->send(200);
1789 return;
1790 }
1791 request->send(404);
1792}
1794 return web_server->valve_json_((valve::Valve *) (source), DETAIL_STATE);
1795}
1797 return web_server->valve_json_((valve::Valve *) (source), DETAIL_ALL);
1798}
1799json::SerializationBuffer<> WebServer::valve_json_(valve::Valve *obj, JsonDetail start_config) {
1800 json::JsonBuilder builder;
1801 JsonObject root = builder.root();
1802
1803 set_json_icon_state_value(root, obj, "valve", obj->is_fully_closed() ? "CLOSED" : "OPEN", obj->position,
1804 start_config);
1805 char buf[PSTR_LOCAL_SIZE];
1806 root[ESPHOME_F("current_operation")] = PSTR_LOCAL(valve::valve_operation_to_str(obj->current_operation));
1807
1808 if (obj->get_traits().get_supports_position())
1809 root[ESPHOME_F("position")] = obj->position;
1810 if (start_config == DETAIL_ALL) {
1811 this->add_sorting_info_(root, obj);
1812 }
1813
1814 return builder.serialize();
1815}
1816#endif
1817
1818#ifdef USE_ALARM_CONTROL_PANEL
1820 if (!this->include_internal_ && obj->is_internal())
1821 return;
1822 this->events_.deferrable_send_state(obj, "state", alarm_control_panel_state_json_generator);
1823}
1824void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match) {
1825 for (alarm_control_panel::AlarmControlPanel *obj : App.get_alarm_control_panels()) {
1826 auto entity_match = match.match_entity(obj);
1827 if (!entity_match.matched)
1828 continue;
1829
1830 if (request->method() == HTTP_GET && entity_match.action_is_empty) {
1831 auto detail = get_request_detail(request);
1832 auto data = this->alarm_control_panel_json_(obj, obj->get_state(), detail);
1833 request->send(200, "application/json", data.c_str());
1834 return;
1835 }
1836
1837 auto call = obj->make_call();
1839 request, ESPHOME_F("code"), call,
1841 alarm_control_panel::AlarmControlPanelCall::*) (const char *, size_t)>(&decltype(call)::set_code));
1842
1843 // Lookup table for alarm control panel methods
1844 static const struct {
1845 const char *name;
1847 } METHODS[] = {
1853 };
1854
1855 bool found = false;
1856 for (const auto &method : METHODS) {
1857 if (match.method_equals(method.name)) {
1858 (call.*method.action)();
1859 found = true;
1860 break;
1861 }
1862 }
1863
1864 if (!found) {
1865 request->send(404);
1866 return;
1867 }
1868
1869 DEFER_ACTION(call, call.perform());
1870 request->send(200);
1871 return;
1872 }
1873 request->send(404);
1874}
1876 return web_server->alarm_control_panel_json_((alarm_control_panel::AlarmControlPanel *) (source),
1877 ((alarm_control_panel::AlarmControlPanel *) (source))->get_state(),
1878 DETAIL_STATE);
1879}
1881 return web_server->alarm_control_panel_json_((alarm_control_panel::AlarmControlPanel *) (source),
1882 ((alarm_control_panel::AlarmControlPanel *) (source))->get_state(),
1883 DETAIL_ALL);
1884}
1885json::SerializationBuffer<> WebServer::alarm_control_panel_json_(alarm_control_panel::AlarmControlPanel *obj,
1887 JsonDetail start_config) {
1888 json::JsonBuilder builder;
1889 JsonObject root = builder.root();
1890
1891 char buf[PSTR_LOCAL_SIZE];
1892 set_json_icon_state_value(root, obj, "alarm-control-panel", PSTR_LOCAL(alarm_control_panel_state_to_string(value)),
1893 value, start_config);
1894 if (start_config == DETAIL_ALL) {
1895 this->add_sorting_info_(root, obj);
1896 }
1897
1898 return builder.serialize();
1899}
1900#endif
1901
1902#ifdef USE_WATER_HEATER
1904 if (!this->include_internal_ && obj->is_internal())
1905 return;
1906 this->events_.deferrable_send_state(obj, "state", water_heater_state_json_generator);
1907}
1908void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, const UrlMatch &match) {
1909 for (water_heater::WaterHeater *obj : App.get_water_heaters()) {
1910 auto entity_match = match.match_entity(obj);
1911 if (!entity_match.matched)
1912 continue;
1913
1914 if (request->method() == HTTP_GET && entity_match.action_is_empty) {
1915 auto detail = get_request_detail(request);
1916 auto data = this->water_heater_json_(obj, detail);
1917 request->send(200, "application/json", data.c_str());
1918 return;
1919 }
1920 if (!match.method_equals(ESPHOME_F("set"))) {
1921 request->send(404);
1922 return;
1923 }
1924 auto call = obj->make_call();
1925 // Use base class reference for template deduction (make_call returns WaterHeaterCallInternal)
1927
1928 // Parse mode parameter
1930 request, ESPHOME_F("mode"), base_call,
1931 static_cast<water_heater::WaterHeaterCall &(water_heater::WaterHeaterCall::*) (const char *, size_t)>(
1933
1934 // Parse temperature parameters
1935 parse_num_param_(request, ESPHOME_F("target_temperature"), base_call,
1937 parse_num_param_(request, ESPHOME_F("target_temperature_low"), base_call,
1939 parse_num_param_(request, ESPHOME_F("target_temperature_high"), base_call,
1941
1942 // Parse away mode parameter
1943 parse_bool_param_(request, ESPHOME_F("away"), base_call, &water_heater::WaterHeaterCall::set_away);
1944
1945 // Parse on/off parameter
1946 parse_bool_param_(request, ESPHOME_F("is_on"), base_call, &water_heater::WaterHeaterCall::set_on);
1947
1948 DEFER_ACTION(call, call.perform());
1949 request->send(200);
1950 return;
1951 }
1952 request->send(404);
1953}
1954
1956 return web_server->water_heater_json_(static_cast<water_heater::WaterHeater *>(source), DETAIL_STATE);
1957}
1959 // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
1960 return web_server->water_heater_json_(static_cast<water_heater::WaterHeater *>(source), DETAIL_ALL);
1961}
1962json::SerializationBuffer<> WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDetail start_config) {
1963 json::JsonBuilder builder;
1964 JsonObject root = builder.root();
1965 char buf[PSTR_LOCAL_SIZE];
1966
1967 const auto mode = obj->get_mode();
1968 const char *mode_s = PSTR_LOCAL(water_heater::water_heater_mode_to_string(mode));
1969
1970 set_json_icon_state_value(root, obj, "water_heater", mode_s, mode, start_config);
1971
1972 auto traits = obj->get_traits();
1973
1974 if (start_config == DETAIL_ALL) {
1975 JsonArray modes = root[ESPHOME_F("modes")].to<JsonArray>();
1976 for (auto m : traits.get_supported_modes())
1977 modes.add(PSTR_LOCAL(water_heater::water_heater_mode_to_string(m)));
1978 root[ESPHOME_F("min_temp")] = traits.get_min_temperature();
1979 root[ESPHOME_F("max_temp")] = traits.get_max_temperature();
1980 root[ESPHOME_F("step")] = traits.get_target_temperature_step();
1981 this->add_sorting_info_(root, obj);
1982 }
1983
1984 if (traits.get_supports_current_temperature()) {
1985 float current = obj->get_current_temperature();
1986 if (!std::isnan(current))
1987 root[ESPHOME_F("current_temperature")] = current;
1988 }
1989
1990 if (traits.get_supports_two_point_target_temperature()) {
1991 float low = obj->get_target_temperature_low();
1992 float high = obj->get_target_temperature_high();
1993 if (!std::isnan(low))
1994 root[ESPHOME_F("target_temperature_low")] = low;
1995 if (!std::isnan(high))
1996 root[ESPHOME_F("target_temperature_high")] = high;
1997 } else {
1998 float target = obj->get_target_temperature();
1999 if (!std::isnan(target))
2000 root[ESPHOME_F("target_temperature")] = target;
2001 }
2002
2003 if (traits.get_supports_away_mode()) {
2004 root[ESPHOME_F("away")] = obj->is_away();
2005 }
2006
2007 if (traits.has_feature_flags(water_heater::WATER_HEATER_SUPPORTS_ON_OFF)) {
2008 root[ESPHOME_F("is_on")] = obj->is_on();
2009 }
2010
2011 return builder.serialize();
2012}
2013#endif
2014
2015#ifdef USE_INFRARED
2016void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const UrlMatch &match) {
2017 for (infrared::Infrared *obj : App.get_infrareds()) {
2018 auto entity_match = match.match_entity(obj);
2019 if (!entity_match.matched)
2020 continue;
2021
2022 if (request->method() == HTTP_GET && entity_match.action_is_empty) {
2023 auto detail = get_request_detail(request);
2024 auto data = this->infrared_json_(obj, detail);
2025 request->send(200, ESPHOME_F("application/json"), data.c_str());
2026 return;
2027 }
2028 if (!match.method_equals(ESPHOME_F("transmit"))) {
2029 request->send(404);
2030 return;
2031 }
2032
2033 // Only allow transmit if the device supports it
2034 if (!obj->has_transmitter()) {
2035 request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Device does not support transmission"));
2036 return;
2037 }
2038
2039 // Parse parameters
2040 auto call = obj->make_call();
2041
2042 // Parse carrier frequency (optional)
2043 {
2044 auto value = parse_number<uint32_t>(request->arg(ESPHOME_F("carrier_frequency")).c_str());
2045 if (value.has_value()) {
2046 call.set_carrier_frequency(*value);
2047 }
2048 }
2049
2050 // Parse repeat count (optional, defaults to 1)
2051 {
2052 auto value = parse_number<uint32_t>(request->arg(ESPHOME_F("repeat_count")).c_str());
2053 if (value.has_value()) {
2054 call.set_repeat_count(*value);
2055 }
2056 }
2057
2058 // Parse base64url-encoded raw timings (required)
2059 // Base64url is URL-safe: uses A-Za-z0-9-_ (no special characters needing escaping)
2060 const auto &data_arg = request->arg(ESPHOME_F("data"));
2061
2062 // Validate base64url is not empty (also catches missing parameter since arg() returns empty string)
2063 // Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility
2064 if (data_arg.length() == 0) { // NOLINT(readability-container-size-empty)
2065 request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Missing or empty 'data' parameter"));
2066 return;
2067 }
2068
2069 // Defer to main loop for thread safety. Move encoded string into lambda to ensure
2070 // it outlives the call - set_raw_timings_base64url stores a pointer, so the string
2071 // must remain valid until perform() completes.
2072 // ESP8266 also needs this because ESPAsyncWebServer callbacks run in "sys" context.
2073 this->defer([call, encoded = std::string(data_arg.c_str(), data_arg.length())]() mutable {
2074 call.set_raw_timings_base64url(encoded);
2075 call.perform();
2076 });
2077
2078 request->send(200);
2079 return;
2080 }
2081 request->send(404);
2082}
2083
2085 // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
2086 return web_server->infrared_json_(static_cast<infrared::Infrared *>(source), DETAIL_ALL);
2087}
2088
2089json::SerializationBuffer<> WebServer::infrared_json_(infrared::Infrared *obj, JsonDetail start_config) {
2090 json::JsonBuilder builder;
2091 JsonObject root = builder.root();
2092
2093 set_json_icon_state_value(root, obj, "infrared", "", 0, start_config);
2094
2095 auto traits = obj->get_traits();
2096
2097 root[ESPHOME_F("supports_transmitter")] = traits.get_supports_transmitter();
2098 root[ESPHOME_F("supports_receiver")] = traits.get_supports_receiver();
2099
2100 if (start_config == DETAIL_ALL) {
2101 this->add_sorting_info_(root, obj);
2102 }
2103
2104 return builder.serialize();
2105}
2106#endif
2107
2108#ifdef USE_RADIO_FREQUENCY
2109void WebServer::handle_radio_frequency_request(AsyncWebServerRequest *request, const UrlMatch &match) {
2110 for (radio_frequency::RadioFrequency *obj : App.get_radio_frequencies()) {
2111 auto entity_match = match.match_entity(obj);
2112 if (!entity_match.matched)
2113 continue;
2114
2115 if (request->method() == HTTP_GET && entity_match.action_is_empty) {
2116 auto detail = get_request_detail(request);
2117 auto data = this->radio_frequency_json_(obj, detail);
2118 request->send(200, ESPHOME_F("application/json"), data.c_str());
2119 return;
2120 }
2121 if (!match.method_equals(ESPHOME_F("transmit"))) {
2122 request->send(404);
2123 return;
2124 }
2125
2126 // Only allow transmit if the device supports it
2128 request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Device does not support transmission"));
2129 return;
2130 }
2131
2132 auto call = obj->make_call();
2133
2134 // Parse carrier frequency (optional — overrides IC default)
2135 {
2136 auto value = parse_number<uint32_t>(request->arg(ESPHOME_F("frequency")).c_str());
2137 if (value.has_value()) {
2138 call.set_frequency(*value);
2139 }
2140 }
2141
2142 // Parse repeat count (optional, defaults to 1)
2143 {
2144 auto value = parse_number<uint32_t>(request->arg(ESPHOME_F("repeat_count")).c_str());
2145 if (value.has_value()) {
2146 call.set_repeat_count(*value);
2147 }
2148 }
2149
2150 // Parse base64url-encoded raw timings (required)
2151 // Base64url is URL-safe: uses A-Za-z0-9-_ (no special characters needing escaping)
2152 const auto &data_arg = request->arg(ESPHOME_F("data"));
2153
2154 // Validate base64url is not empty (also catches missing parameter since arg() returns empty string)
2155 // Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility
2156 if (data_arg.length() == 0) { // NOLINT(readability-container-size-empty)
2157 request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Missing or empty 'data' parameter"));
2158 return;
2159 }
2160
2161 // Defer to main loop for thread safety. Move encoded string into lambda to ensure
2162 // it outlives the call - set_raw_timings_base64url stores a pointer, so the string
2163 // must remain valid until perform() completes.
2164 // ESP8266 also needs this because ESPAsyncWebServer callbacks run in "sys" context.
2165 this->defer([call, encoded = std::string(data_arg.c_str(), data_arg.length())]() mutable {
2166 call.set_raw_timings_base64url(encoded);
2167 call.perform();
2168 });
2169
2170 request->send(200);
2171 return;
2172 }
2173 request->send(404);
2174}
2175
2177 // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
2178 return web_server->radio_frequency_json_(static_cast<radio_frequency::RadioFrequency *>(source), DETAIL_ALL);
2179}
2180
2181json::SerializationBuffer<> WebServer::radio_frequency_json_(radio_frequency::RadioFrequency *obj,
2182 JsonDetail start_config) {
2183 json::JsonBuilder builder;
2184 JsonObject root = builder.root();
2185
2186 set_json_icon_state_value(root, obj, "radio_frequency", "", 0, start_config);
2187
2188 const auto &traits = obj->get_traits();
2189 auto caps = obj->get_capability_flags();
2190
2191 root[ESPHOME_F("supports_transmitter")] = bool(caps & radio_frequency::CAPABILITY_TRANSMITTER);
2192 root[ESPHOME_F("supports_receiver")] = bool(caps & radio_frequency::CAPABILITY_RECEIVER);
2193 if (traits.get_frequency_min_hz() != 0) {
2194 root[ESPHOME_F("frequency_min")] = traits.get_frequency_min_hz();
2195 root[ESPHOME_F("frequency_max")] = traits.get_frequency_max_hz();
2196 }
2197
2198 if (start_config == DETAIL_ALL) {
2199 this->add_sorting_info_(root, obj);
2200 }
2201
2202 return builder.serialize();
2203}
2204#endif
2205
2206#ifdef USE_EVENT
2208 if (!this->include_internal_ && obj->is_internal())
2209 return;
2210 this->events_.deferrable_send_state(obj, "state", event_state_json_generator);
2211}
2212
2213void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMatch &match) {
2214 for (event::Event *obj : App.get_events()) {
2215 auto entity_match = match.match_entity(obj);
2216 if (!entity_match.matched)
2217 continue;
2218
2219 // Note: request->method() is always HTTP_GET here (canHandle ensures this)
2220 if (entity_match.action_is_empty) {
2221 auto detail = get_request_detail(request);
2222 auto data = this->event_json_(obj, StringRef(), detail);
2223 request->send(200, "application/json", data.c_str());
2224 return;
2225 }
2226 }
2227 request->send(404);
2228}
2229
2230static StringRef get_event_type(event::Event *event) { return event ? event->get_last_event_type() : StringRef(); }
2231
2233 auto *event = static_cast<event::Event *>(source);
2234 return web_server->event_json_(event, get_event_type(event), DETAIL_STATE);
2235}
2236// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
2238 auto *event = static_cast<event::Event *>(source);
2239 return web_server->event_json_(event, get_event_type(event), DETAIL_ALL);
2240}
2241json::SerializationBuffer<> WebServer::event_json_(event::Event *obj, StringRef event_type, JsonDetail start_config) {
2242 json::JsonBuilder builder;
2243 JsonObject root = builder.root();
2244
2245 set_json_id(root, obj, "event", start_config);
2246 if (!event_type.empty()) {
2247 root[ESPHOME_F("event_type")] = event_type;
2248 }
2249 if (start_config == DETAIL_ALL) {
2250 JsonArray event_types = root[ESPHOME_F("event_types")].to<JsonArray>();
2251 for (const char *event_type : obj->get_event_types()) {
2252 event_types.add(event_type);
2253 }
2254 char dc_buf[MAX_DEVICE_CLASS_LENGTH];
2255 root[ESPHOME_F("device_class")] = obj->get_device_class_to(dc_buf);
2256 this->add_sorting_info_(root, obj);
2257 }
2258
2259 return builder.serialize();
2260}
2261// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
2262#endif
2263
2264#ifdef USE_UPDATE
2266 this->events_.deferrable_send_state(obj, "state", update_state_json_generator);
2267}
2268void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlMatch &match) {
2269 for (update::UpdateEntity *obj : App.get_updates()) {
2270 auto entity_match = match.match_entity(obj);
2271 if (!entity_match.matched)
2272 continue;
2273
2274 if (request->method() == HTTP_GET && entity_match.action_is_empty) {
2275 auto detail = get_request_detail(request);
2276 auto data = this->update_json_(obj, detail);
2277 request->send(200, "application/json", data.c_str());
2278 return;
2279 }
2280
2281 if (!match.method_equals(ESPHOME_F("install"))) {
2282 request->send(404);
2283 return;
2284 }
2285
2286 DEFER_ACTION(obj, obj->perform());
2287 request->send(200);
2288 return;
2289 }
2290 request->send(404);
2291}
2293 // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
2294 return web_server->update_json_((update::UpdateEntity *) (source), DETAIL_STATE);
2295}
2297 // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
2298 return web_server->update_json_((update::UpdateEntity *) (source), DETAIL_ALL);
2299}
2300json::SerializationBuffer<> WebServer::update_json_(update::UpdateEntity *obj, JsonDetail start_config) {
2301 // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
2302 json::JsonBuilder builder;
2303 JsonObject root = builder.root();
2304
2305 char buf[PSTR_LOCAL_SIZE];
2306 set_json_icon_state_value(root, obj, "update", PSTR_LOCAL(update::update_state_to_string(obj->state)),
2307 obj->update_info.latest_version, start_config);
2308 if (start_config == DETAIL_ALL) {
2309 root[ESPHOME_F("current_version")] = obj->update_info.current_version;
2310 root[ESPHOME_F("title")] = obj->update_info.title;
2311 // Truncate long changelogs — full text available via release_url
2312 constexpr size_t max_summary_len = 256;
2313 root[ESPHOME_F("summary")] = obj->update_info.summary.size() <= max_summary_len
2314 ? obj->update_info.summary
2315 : obj->update_info.summary.substr(0, max_summary_len);
2316 root[ESPHOME_F("release_url")] = obj->update_info.release_url;
2317 this->add_sorting_info_(root, obj);
2318 }
2319
2320 return builder.serialize();
2321 // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
2322}
2323#endif
2324
2325bool WebServer::canHandle(AsyncWebServerRequest *request) const {
2326#ifdef USE_ESP32
2327 char url_buf[AsyncWebServerRequest::URL_BUF_SIZE];
2328 StringRef url = request->url_to(url_buf);
2329#else
2330 const auto &url = request->url();
2331#endif
2332 const auto method = request->method();
2333
2334 // Static URL checks - use ESPHOME_F to keep strings in flash on ESP8266
2335 if (url == ESPHOME_F("/"))
2336 return true;
2337#if !defined(USE_ESP32) && defined(USE_ARDUINO)
2338 if (url == ESPHOME_F("/events"))
2339 return true;
2340#endif
2341#ifdef USE_WEBSERVER_CSS_INCLUDE
2342 if (url == ESPHOME_F("/0.css"))
2343 return true;
2344#endif
2345#ifdef USE_WEBSERVER_JS_INCLUDE
2346 if (url == ESPHOME_F("/0.js"))
2347 return true;
2348#endif
2349
2350#ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS
2351 if (method == HTTP_OPTIONS && request->hasHeader(ESPHOME_F("Access-Control-Request-Private-Network")))
2352 return true;
2353#endif
2354
2355 // Parse URL for component checks
2356 UrlMatch match = match_url(url.c_str(), url.length(), true);
2357 if (!match.valid)
2358 return false;
2359
2360 // Common pattern check
2361 bool is_get = method == HTTP_GET;
2362 bool is_post = method == HTTP_POST;
2363 bool is_get_or_post = is_get || is_post;
2364
2365 if (!is_get_or_post)
2366 return false;
2367
2368 // Check GET-only domains - use ESPHOME_F to keep strings in flash on ESP8266
2369 if (is_get) {
2370#ifdef USE_SENSOR
2371 if (match.domain_equals(ESPHOME_F("sensor")))
2372 return true;
2373#endif
2374#ifdef USE_BINARY_SENSOR
2375 if (match.domain_equals(ESPHOME_F("binary_sensor")))
2376 return true;
2377#endif
2378#ifdef USE_TEXT_SENSOR
2379 if (match.domain_equals(ESPHOME_F("text_sensor")))
2380 return true;
2381#endif
2382#ifdef USE_EVENT
2383 if (match.domain_equals(ESPHOME_F("event")))
2384 return true;
2385#endif
2386 }
2387
2388 // Check GET+POST domains
2389 if (is_get_or_post) {
2390#ifdef USE_SWITCH
2391 if (match.domain_equals(ESPHOME_F("switch")))
2392 return true;
2393#endif
2394#ifdef USE_BUTTON
2395 if (match.domain_equals(ESPHOME_F("button")))
2396 return true;
2397#endif
2398#ifdef USE_FAN
2399 if (match.domain_equals(ESPHOME_F("fan")))
2400 return true;
2401#endif
2402#ifdef USE_LIGHT
2403 if (match.domain_equals(ESPHOME_F("light")))
2404 return true;
2405#endif
2406#ifdef USE_COVER
2407 if (match.domain_equals(ESPHOME_F("cover")))
2408 return true;
2409#endif
2410#ifdef USE_NUMBER
2411 if (match.domain_equals(ESPHOME_F("number")))
2412 return true;
2413#endif
2414#ifdef USE_DATETIME_DATE
2415 if (match.domain_equals(ESPHOME_F("date")))
2416 return true;
2417#endif
2418#ifdef USE_DATETIME_TIME
2419 if (match.domain_equals(ESPHOME_F("time")))
2420 return true;
2421#endif
2422#ifdef USE_DATETIME_DATETIME
2423 if (match.domain_equals(ESPHOME_F("datetime")))
2424 return true;
2425#endif
2426#ifdef USE_TEXT
2427 if (match.domain_equals(ESPHOME_F("text")))
2428 return true;
2429#endif
2430#ifdef USE_SELECT
2431 if (match.domain_equals(ESPHOME_F("select")))
2432 return true;
2433#endif
2434#ifdef USE_CLIMATE
2435 if (match.domain_equals(ESPHOME_F("climate")))
2436 return true;
2437#endif
2438#ifdef USE_LOCK
2439 if (match.domain_equals(ESPHOME_F("lock")))
2440 return true;
2441#endif
2442#ifdef USE_VALVE
2443 if (match.domain_equals(ESPHOME_F("valve")))
2444 return true;
2445#endif
2446#ifdef USE_ALARM_CONTROL_PANEL
2447 if (match.domain_equals(ESPHOME_F("alarm_control_panel")))
2448 return true;
2449#endif
2450#ifdef USE_UPDATE
2451 if (match.domain_equals(ESPHOME_F("update")))
2452 return true;
2453#endif
2454#ifdef USE_WATER_HEATER
2455 if (match.domain_equals(ESPHOME_F("water_heater")))
2456 return true;
2457#endif
2458#ifdef USE_INFRARED
2459 if (match.domain_equals(ESPHOME_F("infrared")))
2460 return true;
2461#endif
2462#ifdef USE_RADIO_FREQUENCY
2463 if (match.domain_equals(ESPHOME_F("radio_frequency")))
2464 return true;
2465#endif
2466 }
2467
2468 return false;
2469}
2470void WebServer::handleRequest(AsyncWebServerRequest *request) {
2471#ifdef USE_ESP32
2472 char url_buf[AsyncWebServerRequest::URL_BUF_SIZE];
2473 StringRef url = request->url_to(url_buf);
2474#else
2475 const auto &url = request->url();
2476#endif
2477
2478 // Handle static routes first
2479 if (url == ESPHOME_F("/")) {
2480 this->handle_index_request(request);
2481 return;
2482 }
2483
2484#if !defined(USE_ESP32) && defined(USE_ARDUINO)
2485 if (url == ESPHOME_F("/events")) {
2486 this->events_.add_new_client(this, request);
2487 return;
2488 }
2489#endif
2490
2491#ifdef USE_WEBSERVER_CSS_INCLUDE
2492 if (url == ESPHOME_F("/0.css")) {
2493 this->handle_css_request(request);
2494 return;
2495 }
2496#endif
2497
2498#ifdef USE_WEBSERVER_JS_INCLUDE
2499 if (url == ESPHOME_F("/0.js")) {
2500 this->handle_js_request(request);
2501 return;
2502 }
2503#endif
2504
2505#ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS
2506 if (request->method() == HTTP_OPTIONS && request->hasHeader(ESPHOME_F("Access-Control-Request-Private-Network"))) {
2507 this->handle_pna_cors_request(request);
2508 return;
2509 }
2510#endif
2511
2512 // Parse URL for component routing
2513 // Pass HTTP method to disambiguate 3-segment URLs (GET=sub-device state, POST=main device action)
2514 UrlMatch match = match_url(url.c_str(), url.length(), false, request->method() == HTTP_POST);
2515
2516 // Route to appropriate handler based on domain
2517 // NOLINTNEXTLINE(readability-simplify-boolean-expr)
2518 if (false) { // Start chain for else-if macro pattern
2519 }
2520#ifdef USE_SENSOR
2521 else if (match.domain_equals(ESPHOME_F("sensor"))) {
2522 this->handle_sensor_request(request, match);
2523 }
2524#endif
2525#ifdef USE_SWITCH
2526 else if (match.domain_equals(ESPHOME_F("switch"))) {
2527 this->handle_switch_request(request, match);
2528 }
2529#endif
2530#ifdef USE_BUTTON
2531 else if (match.domain_equals(ESPHOME_F("button"))) {
2532 this->handle_button_request(request, match);
2533 }
2534#endif
2535#ifdef USE_BINARY_SENSOR
2536 else if (match.domain_equals(ESPHOME_F("binary_sensor"))) {
2537 this->handle_binary_sensor_request(request, match);
2538 }
2539#endif
2540#ifdef USE_FAN
2541 else if (match.domain_equals(ESPHOME_F("fan"))) {
2542 this->handle_fan_request(request, match);
2543 }
2544#endif
2545#ifdef USE_LIGHT
2546 else if (match.domain_equals(ESPHOME_F("light"))) {
2547 this->handle_light_request(request, match);
2548 }
2549#endif
2550#ifdef USE_TEXT_SENSOR
2551 else if (match.domain_equals(ESPHOME_F("text_sensor"))) {
2552 this->handle_text_sensor_request(request, match);
2553 }
2554#endif
2555#ifdef USE_COVER
2556 else if (match.domain_equals(ESPHOME_F("cover"))) {
2557 this->handle_cover_request(request, match);
2558 }
2559#endif
2560#ifdef USE_NUMBER
2561 else if (match.domain_equals(ESPHOME_F("number"))) {
2562 this->handle_number_request(request, match);
2563 }
2564#endif
2565#ifdef USE_DATETIME_DATE
2566 else if (match.domain_equals(ESPHOME_F("date"))) {
2567 this->handle_date_request(request, match);
2568 }
2569#endif
2570#ifdef USE_DATETIME_TIME
2571 else if (match.domain_equals(ESPHOME_F("time"))) {
2572 this->handle_time_request(request, match);
2573 }
2574#endif
2575#ifdef USE_DATETIME_DATETIME
2576 else if (match.domain_equals(ESPHOME_F("datetime"))) {
2577 this->handle_datetime_request(request, match);
2578 }
2579#endif
2580#ifdef USE_TEXT
2581 else if (match.domain_equals(ESPHOME_F("text"))) {
2582 this->handle_text_request(request, match);
2583 }
2584#endif
2585#ifdef USE_SELECT
2586 else if (match.domain_equals(ESPHOME_F("select"))) {
2587 this->handle_select_request(request, match);
2588 }
2589#endif
2590#ifdef USE_CLIMATE
2591 else if (match.domain_equals(ESPHOME_F("climate"))) {
2592 this->handle_climate_request(request, match);
2593 }
2594#endif
2595#ifdef USE_LOCK
2596 else if (match.domain_equals(ESPHOME_F("lock"))) {
2597 this->handle_lock_request(request, match);
2598 }
2599#endif
2600#ifdef USE_VALVE
2601 else if (match.domain_equals(ESPHOME_F("valve"))) {
2602 this->handle_valve_request(request, match);
2603 }
2604#endif
2605#ifdef USE_ALARM_CONTROL_PANEL
2606 else if (match.domain_equals(ESPHOME_F("alarm_control_panel"))) {
2607 this->handle_alarm_control_panel_request(request, match);
2608 }
2609#endif
2610#ifdef USE_UPDATE
2611 else if (match.domain_equals(ESPHOME_F("update"))) {
2612 this->handle_update_request(request, match);
2613 }
2614#endif
2615#ifdef USE_WATER_HEATER
2616 else if (match.domain_equals(ESPHOME_F("water_heater"))) {
2617 this->handle_water_heater_request(request, match);
2618 }
2619#endif
2620#ifdef USE_INFRARED
2621 else if (match.domain_equals(ESPHOME_F("infrared"))) {
2622 this->handle_infrared_request(request, match);
2623 }
2624#endif
2625#ifdef USE_RADIO_FREQUENCY
2626 else if (match.domain_equals(ESPHOME_F("radio_frequency"))) {
2627 this->handle_radio_frequency_request(request, match);
2628 }
2629#endif
2630 else {
2631 // No matching handler found - send 404
2632 ESP_LOGV(TAG, "Request for unknown URL: %s", url.c_str());
2633 request->send(404, ESPHOME_F("text/plain"), ESPHOME_F("Not Found"));
2634 }
2635}
2636
2637bool WebServer::isRequestHandlerTrivial() const { return false; }
2638
2639void WebServer::add_sorting_info_(JsonObject &root, EntityBase *entity) {
2640#ifdef USE_WEBSERVER_SORTING
2641 if (this->sorting_entitys_.find(entity) != this->sorting_entitys_.end()) {
2642 root[ESPHOME_F("sorting_weight")] = this->sorting_entitys_[entity].weight;
2643 if (this->sorting_groups_.find(this->sorting_entitys_[entity].group_id) != this->sorting_groups_.end()) {
2644 root[ESPHOME_F("sorting_group")] = this->sorting_groups_[this->sorting_entitys_[entity].group_id].name;
2645 }
2646 }
2647#endif
2648}
2649
2650#ifdef USE_WEBSERVER_SORTING
2651void WebServer::add_entity_config(EntityBase *entity, float weight, uint64_t group) {
2652 this->sorting_entitys_[entity] = SortingComponents{weight, group};
2653}
2654
2655void WebServer::add_sorting_group(uint64_t group_id, const std::string &group_name, float weight) {
2656 this->sorting_groups_[group_id] = SortingGroup{group_name, weight};
2657}
2658#endif
2659
2660} // namespace esphome::web_server
2661#endif
BedjetMode mode
BedJet operating mode.
uint8_t m
Definition bl0906.h:1
const StringRef & get_name() const
Get the name of this Application set by pre_setup().
const StringRef & get_friendly_name() const
Get the friendly name of this Application set by pre_setup().
static constexpr size_t ESPHOME_COMMENT_SIZE_MAX
Maximum size of the comment buffer (including null terminator)
void get_comment_string(std::span< char, ESPHOME_COMMENT_SIZE_MAX > buffer)
Copy the comment string into the provided buffer.
ESPDEPRECATED("Use const char* overload instead. Removed in 2026.7.0", "2026.1.0") void defer(const std voi defer)(const char *name, std::function< void()> &&f)
Defer a callback to the next loop() call.
Definition component.h:560
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") void set_interval(const std voi set_interval)(const char *name, uint32_t interval, std::function< void()> &&f)
Set an interval function with a unique name.
Definition component.h:417
void enable_loop_soon_any_context()
Thread and ISR-safe version of enable_loop() that can be called from any context.
void disable_loop()
Disable this component's loop.
void begin(bool include_internal=false)
static void register_controller(Controller *controller)
Register a controller to receive entity state updates.
const char * get_name()
Definition device.h:10
const char * get_device_class_to(std::span< char, MAX_DEVICE_CLASS_LENGTH > buffer) const
bool is_internal() const
const StringRef & get_name() const
Definition entity_base.h:71
ESPDEPRECATED("Use get_unit_of_measurement_ref() instead for better performance (avoids string copy). Will be " "removed in ESPHome 2026.9.0", "2026.3.0") std const char * get_icon_to(std::span< char, MAX_ICON_LENGTH > buffer) const
Get the unit of measurement as std::string (deprecated, prefer get_unit_of_measurement_ref())
size_t write_object_id_to(char *buf, size_t buf_size) const
Write object_id directly to buffer, returns length written (excluding null) Useful for building compo...
bool is_disabled_by_default() const
ESPDEPRECATED("Use get_device_class_to() instead. Will be removed in ESPHome 2026.9.0", "2026.3.0") std StringRef get_unit_of_measurement_ref() const
Device * get_device() const
bool has_state() const
EntityCategory get_entity_category() const
StringRef get_object_id_to(std::span< char, OBJECT_ID_MAX_LEN > buf) const
Get object_id with zero heap allocation For static case: returns StringRef to internal storage (buffe...
StringRef is a reference to a string owned by something else.
Definition string_ref.h:26
constexpr const char * c_str() const
Definition string_ref.h:73
constexpr size_type length() const
Definition string_ref.h:75
constexpr bool empty() const
Definition string_ref.h:76
constexpr size_type size() const
Definition string_ref.h:74
AlarmControlPanelCall make_call()
Make a AlarmControlPanelCall.
Base class for all binary_sensor-type classes.
Base class for all buttons.
Definition button.h:25
This class is used to encode all control actions on a climate device.
Definition climate.h:34
ClimateDevice - This is the base class for all climate integrations.
Definition climate.h:187
ClimateMode mode
The active mode of the climate device.
Definition climate.h:293
optional< ClimateFanMode > fan_mode
The active fan mode of the climate device.
Definition climate.h:287
ClimateTraits get_traits()
Get the traits of this climate device with all overrides applied.
Definition climate.cpp:486
float target_temperature
The target temperature of the climate device.
Definition climate.h:274
float current_humidity
The current humidity of the climate device, as reported from the integration.
Definition climate.h:270
ClimateSwingMode swing_mode
The active swing mode of the climate device.
Definition climate.h:299
float target_temperature_low
The minimum target temperature of the climate device, for climate devices with split target temperatu...
Definition climate.h:277
bool has_custom_preset() const
Check if a custom preset is currently active.
Definition climate.h:264
float current_temperature
The current temperature of the climate device, as reported from the integration.
Definition climate.h:267
ClimateAction action
The active state of the climate device.
Definition climate.h:296
StringRef get_custom_preset() const
Get the active custom preset (read-only access). Returns StringRef.
Definition climate.h:305
bool has_custom_fan_mode() const
Check if a custom fan mode is currently active.
Definition climate.h:261
optional< ClimatePreset > preset
The active preset of the climate device.
Definition climate.h:290
float target_temperature_high
The maximum target temperature of the climate device, for climate devices with split target temperatu...
Definition climate.h:279
StringRef get_custom_fan_mode() const
Get the active custom fan mode (read-only access). Returns StringRef.
Definition climate.h:302
int8_t get_target_temperature_accuracy_decimals() const
CoverCall & set_command_toggle()
Set the command to toggle the cover.
Definition cover.cpp:58
CoverCall & set_command_open()
Set the command to open the cover.
Definition cover.cpp:46
CoverCall & set_command_close()
Set the command to close the cover.
Definition cover.cpp:50
CoverCall & set_command_stop()
Set the command to stop the cover.
Definition cover.cpp:54
Base class for all cover devices.
Definition cover.h:110
CoverOperation current_operation
The current operation of the cover (idle, opening, closing).
Definition cover.h:115
CoverCall make_call()
Construct a new cover call used to control the cover.
Definition cover.cpp:140
float tilt
The current tilt value of the cover from 0.0 to 1.0.
Definition cover.h:123
float position
The position of the cover from 0.0 (fully closed) to 1.0 (fully open).
Definition cover.h:121
bool is_fully_closed() const
Helper method to check if the cover is fully closed. Equivalent to comparing .position against 0....
Definition cover.cpp:188
virtual CoverTraits get_traits()=0
bool get_supports_position() const
const FixedVector< const char * > & get_event_types() const
Return the event types supported by this event.
Definition event.h:42
FanCall turn_on()
Definition fan.cpp:156
FanCall turn_off()
Definition fan.cpp:157
virtual FanTraits get_traits()=0
FanCall toggle()
Definition fan.cpp:158
bool oscillating
The current oscillation state of the fan.
Definition fan.h:112
bool state
The current on/off state of the fan.
Definition fan.h:110
int speed
The current fan speed level.
Definition fan.h:114
bool supports_oscillation() const
Return if this fan supports oscillation.
Definition fan_traits.h:21
Infrared - Base class for infrared remote control implementations.
Definition infrared.h:114
InfraredCall make_call()
Create a call object for transmitting.
Definition infrared.cpp:78
InfraredTraits & get_traits()
Get the traits for this infrared implementation.
Definition infrared.h:133
uint32_t get_capability_flags() const
Get capability flags for this infrared instance.
Definition infrared.cpp:141
bool get_supports_transmitter() const
Definition infrared.h:98
Builder class for creating JSON documents without lambdas.
Definition json_util.h:169
SerializationBuffer serialize()
Serialize the JSON document to a SerializationBuffer (stack-first allocation) Uses 512-byte stack buf...
Definition json_util.cpp:69
Buffer for JSON serialization that uses stack allocation for small payloads.
Definition json_util.h:21
This class represents a requested change in a light state.
Definition light_call.h:22
bool is_on() const
Get the binary true/false state of these light color values.
static void dump_json(LightState &state, JsonObject root)
Dump the state of a light as JSON.
This class represents the communication layer between the front-end MQTT layer and the hardware outpu...
Definition light_state.h:93
LightColorValues remote_values
The remote color values reported to the frontend.
const FixedVector< LightEffect * > & get_effects() const
Get all effects for this light state.
Base class for all locks.
Definition lock.h:112
LockCall make_call()
Make a lock device control call, this is used to control the lock device, see the LockCall descriptio...
Definition lock.cpp:21
void lock()
Turn this lock on.
Definition lock.cpp:29
LockState state
The current reported state of the lock.
Definition lock.h:131
void unlock()
Turn this lock off.
Definition lock.cpp:30
void open()
Open (unlatch) this lock.
Definition lock.cpp:31
void add_log_callback(void *instance, void(*fn)(void *, uint8_t, const char *, const char *, size_t))
Register a log callback to receive log messages.
Definition logger.h:187
Base-class for all numbers.
Definition number.h:29
NumberCall make_call()
Definition number.h:35
NumberTraits traits
Definition number.h:41
NumberMode get_mode() const
RadioFrequency - Base class for radio frequency implementations.
uint32_t get_capability_flags() const
Get capability flags for this radio frequency instance.
RadioFrequencyTraits & get_traits()
Get the traits for this radio frequency implementation.
Base-class for all selects.
Definition select.h:29
SelectCall make_call()
Instantiate a SelectCall object to modify this select component's state.
Definition select.h:53
SelectTraits traits
Definition select.h:31
const FixedVector< const char * > & get_options() const
Base-class for all sensors.
Definition sensor.h:47
float state
This member variable stores the last state that has passed through all filters.
Definition sensor.h:138
int8_t get_accuracy_decimals()
Get the accuracy in decimals, using the manual override if set.
Definition sensor.cpp:48
Base class for all switches.
Definition switch.h:38
void toggle()
Toggle this switch.
Definition switch.cpp:28
void turn_on()
Turn this switch on.
Definition switch.cpp:20
void turn_off()
Turn this switch off.
Definition switch.cpp:24
bool state
The current reported state of the binary sensor.
Definition switch.h:55
virtual bool assumed_state()
Return whether this switch uses an assumed state - i.e.
Definition switch.cpp:70
Base-class for all text inputs.
Definition text.h:21
TextCall make_call()
Instantiate a TextCall object to modify this text component's state.
Definition text.h:31
TextTraits traits
Definition text.h:24
TextMode get_mode() const
Definition text_traits.h:30
const char * get_pattern_c_str() const
Definition text_traits.h:25
const UpdateState & state
const UpdateInfo & update_info
ValveCall & set_command_close()
Set the command to close the valve.
Definition valve.cpp:53
ValveCall & set_command_toggle()
Set the command to toggle the valve.
Definition valve.cpp:61
ValveCall & set_command_stop()
Set the command to stop the valve.
Definition valve.cpp:57
ValveCall & set_command_open()
Set the command to open the valve.
Definition valve.cpp:49
Base class for all valve devices.
Definition valve.h:103
bool is_fully_closed() const
Helper method to check if the valve is fully closed. Equivalent to comparing .position against 0....
Definition valve.cpp:166
float position
The position of the valve from 0.0 (fully closed) to 1.0 (fully open).
Definition valve.h:114
ValveCall make_call()
Construct a new valve call used to control the valve.
Definition valve.cpp:125
ValveOperation current_operation
The current operation of the valve (idle, opening, closing).
Definition valve.h:108
virtual ValveTraits get_traits()=0
bool get_supports_position() const
WaterHeaterCall & set_away(bool away)
WaterHeaterCall & set_target_temperature_high(float temperature)
WaterHeaterCall & set_mode(WaterHeaterMode mode)
WaterHeaterCall & set_target_temperature_low(float temperature)
WaterHeaterCall & set_on(bool on)
WaterHeaterCall & set_target_temperature(float temperature)
bool is_on() const
Check if the water heater is on.
bool is_away() const
Check if away mode is currently active.
virtual WaterHeaterCallInternal make_call()=0
WaterHeaterMode get_mode() const
virtual WaterHeaterTraits get_traits()
void try_send_nodefer(const char *message, const char *event=nullptr, uint32_t id=0, uint32_t reconnect=0)
static constexpr uint16_t MAX_CONSECUTIVE_SEND_FAILURES
Definition web_server.h:149
void deq_push_back_with_dedup_(void *source, message_generator_t *message_generator)
void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator)
std::vector< DeferredEvent > deferred_queue_
Definition web_server.h:146
void on_client_connect_(DeferredUpdateEventSource *source)
void add_new_client(WebServer *ws, AsyncWebServerRequest *request)
void deferrable_send_state(void *source, const char *event_type, message_generator_t *message_generator)
void try_send_nodefer(const char *message, const char *event=nullptr, uint32_t id=0, uint32_t reconnect=0)
void on_client_disconnect_(DeferredUpdateEventSource *source)
bool loop()
Returns true if there are event sources remaining (including pending cleanup).
This class allows users to create a web server with their ESP nodes.
Definition web_server.h:191
void setup() override
Setup the internal web server and register handlers.
void on_update(update::UpdateEntity *obj) override
static json::SerializationBuffer radio_frequency_all_json_generator(WebServer *web_server, void *source)
void on_water_heater_update(water_heater::WaterHeater *obj) override
json::SerializationBuffer get_config_json()
Return the webserver configuration as JSON.
std::map< EntityBase *, SortingComponents > sorting_entitys_
Definition web_server.h:503
static json::SerializationBuffer text_state_json_generator(WebServer *web_server, void *source)
void on_text_update(text::Text *obj) override
void on_light_update(light::LightState *obj) override
static json::SerializationBuffer datetime_state_json_generator(WebServer *web_server, void *source)
void on_cover_update(cover::Cover *obj) override
static json::SerializationBuffer lock_all_json_generator(WebServer *web_server, void *source)
static json::SerializationBuffer alarm_control_panel_all_json_generator(WebServer *web_server, void *source)
static json::SerializationBuffer text_all_json_generator(WebServer *web_server, void *source)
static json::SerializationBuffer switch_all_json_generator(WebServer *web_server, void *source)
void handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match)
Handle a select request under '/select/<id>'.
void on_log(uint8_t level, const char *tag, const char *message, size_t message_len)
static json::SerializationBuffer event_all_json_generator(WebServer *web_server, void *source)
static json::SerializationBuffer update_all_json_generator(WebServer *web_server, void *source)
static json::SerializationBuffer text_sensor_all_json_generator(WebServer *web_server, void *source)
void handle_water_heater_request(AsyncWebServerRequest *request, const UrlMatch &match)
Handle a water_heater request under '/water_heater/<id>/<mode/set>'.
bool isRequestHandlerTrivial() const override
This web handle is not trivial.
static json::SerializationBuffer cover_all_json_generator(WebServer *web_server, void *source)
WebServer(web_server_base::WebServerBase *base)
void handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match)
Handle a switch request under '/switch/<id>/</turn_on/turn_off/toggle>'.
void handle_event_request(AsyncWebServerRequest *request, const UrlMatch &match)
Handle a event request under '/event<id>'.
void parse_light_param_uint_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret(T::*setter)(uint32_t), uint32_t scale=1)
Definition web_server.h:525
static json::SerializationBuffer datetime_all_json_generator(WebServer *web_server, void *source)
static json::SerializationBuffer light_state_json_generator(WebServer *web_server, void *source)
static json::SerializationBuffer climate_state_json_generator(WebServer *web_server, void *source)
void handle_button_request(AsyncWebServerRequest *request, const UrlMatch &match)
Handle a button request under '/button/<id>/press'.
void on_date_update(datetime::DateEntity *obj) override
static json::SerializationBuffer date_all_json_generator(WebServer *web_server, void *source)
static json::SerializationBuffer sensor_state_json_generator(WebServer *web_server, void *source)
void on_number_update(number::Number *obj) override
void add_entity_config(EntityBase *entity, float weight, uint64_t group)
void handle_radio_frequency_request(AsyncWebServerRequest *request, const UrlMatch &match)
Handle a radio frequency request under '/radio_frequency/<id>/transmit'.
void parse_light_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret(T::*setter)(float), float scale=1.0f)
Definition web_server.h:515
void handle_css_request(AsyncWebServerRequest *request)
Handle included css request under '/0.css'.
static json::SerializationBuffer select_all_json_generator(WebServer *web_server, void *source)
static json::SerializationBuffer infrared_all_json_generator(WebServer *web_server, void *source)
void on_valve_update(valve::Valve *obj) override
void on_climate_update(climate::Climate *obj) override
void add_sorting_info_(JsonObject &root, EntityBase *entity)
void handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match)
Handle a light request under '/light/<id>/</turn_on/turn_off/toggle>'.
static json::SerializationBuffer sensor_all_json_generator(WebServer *web_server, void *source)
void on_binary_sensor_update(binary_sensor::BinarySensor *obj) override
void handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match)
Handle a text input request under '/text/<id>'.
static json::SerializationBuffer number_all_json_generator(WebServer *web_server, void *source)
void handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match)
Handle a cover request under '/cover/<id>/<open/close/stop/set>'.
static json::SerializationBuffer select_state_json_generator(WebServer *web_server, void *source)
void on_switch_update(switch_::Switch *obj) override
static json::SerializationBuffer water_heater_state_json_generator(WebServer *web_server, void *source)
web_server_base::WebServerBase * base_
Definition web_server.h:576
void handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match)
Handle a lock request under '/lock/<id>/</lock/unlock/open>'.
void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) override
static json::SerializationBuffer time_state_json_generator(WebServer *web_server, void *source)
void handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match)
Handle a text sensor request under '/text_sensor/<id>'.
void parse_bool_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret(T::*setter)(bool))
Definition web_server.h:558
void handle_date_request(AsyncWebServerRequest *request, const UrlMatch &match)
Handle a date request under '/date/<id>'.
void handle_infrared_request(AsyncWebServerRequest *request, const UrlMatch &match)
Handle an infrared request under '/infrared/<id>/transmit'.
void handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match)
Handle a sensor request under '/sensor/<id>'.
void handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match)
Handle a number request under '/number/<id>'.
void handle_index_request(AsyncWebServerRequest *request)
Handle an index request under '/'.
void handle_js_request(AsyncWebServerRequest *request)
Handle included js request under '/0.js'.
static json::SerializationBuffer valve_state_json_generator(WebServer *web_server, void *source)
static json::SerializationBuffer fan_all_json_generator(WebServer *web_server, void *source)
void set_js_include(const char *js_include)
Set local path to the script that's embedded in the index page.
static json::SerializationBuffer lock_state_json_generator(WebServer *web_server, void *source)
static json::SerializationBuffer update_state_json_generator(WebServer *web_server, void *source)
void handleRequest(AsyncWebServerRequest *request) override
Override the web handler's handleRequest method.
static json::SerializationBuffer button_all_json_generator(WebServer *web_server, void *source)
void on_datetime_update(datetime::DateTimeEntity *obj) override
void handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match)
Handle a fan request under '/fan/<id>/</turn_on/turn_off/toggle>'.
static json::SerializationBuffer alarm_control_panel_state_json_generator(WebServer *web_server, void *source)
static json::SerializationBuffer time_all_json_generator(WebServer *web_server, void *source)
void parse_cstr_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret(T::*setter)(const char *, size_t))
Definition web_server.h:545
static json::SerializationBuffer water_heater_all_json_generator(WebServer *web_server, void *source)
void handle_valve_request(AsyncWebServerRequest *request, const UrlMatch &match)
Handle a valve request under '/valve/<id>/<open/close/stop/set>'.
static json::SerializationBuffer date_state_json_generator(WebServer *web_server, void *source)
void handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match)
Handle a binary sensor request under '/binary_sensor/<id>'.
void handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match)
Handle a time request under '/time/<id>'.
void on_sensor_update(sensor::Sensor *obj) override
std::map< uint64_t, SortingGroup > sorting_groups_
Definition web_server.h:504
void set_css_include(const char *css_include)
Set local path to the script that's embedded in the index page.
bool canHandle(AsyncWebServerRequest *request) const override
Override the web handler's canHandle method.
static json::SerializationBuffer climate_all_json_generator(WebServer *web_server, void *source)
static json::SerializationBuffer fan_state_json_generator(WebServer *web_server, void *source)
void on_event(event::Event *obj) override
static json::SerializationBuffer cover_state_json_generator(WebServer *web_server, void *source)
void handle_pna_cors_request(AsyncWebServerRequest *request)
static json::SerializationBuffer binary_sensor_state_json_generator(WebServer *web_server, void *source)
void on_fan_update(fan::Fan *obj) override
void handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match)
Handle a datetime request under '/datetime/<id>'.
static json::SerializationBuffer event_state_json_generator(WebServer *web_server, void *source)
static json::SerializationBuffer binary_sensor_all_json_generator(WebServer *web_server, void *source)
void handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match)
Handle a alarm_control_panel request under '/alarm_control_panel/<id>'.
void on_lock_update(lock::Lock *obj) override
static json::SerializationBuffer switch_state_json_generator(WebServer *web_server, void *source)
float get_setup_priority() const override
MQTT setup priority.
void on_select_update(select::Select *obj) override
void on_time_update(datetime::TimeEntity *obj) override
void parse_num_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret(T::*setter)(NumT))
Definition web_server.h:536
static json::SerializationBuffer text_sensor_state_json_generator(WebServer *web_server, void *source)
static json::SerializationBuffer number_state_json_generator(WebServer *web_server, void *source)
static json::SerializationBuffer valve_all_json_generator(WebServer *web_server, void *source)
void handle_update_request(AsyncWebServerRequest *request, const UrlMatch &match)
Handle a update request under '/update/<id>'.
static json::SerializationBuffer light_all_json_generator(WebServer *web_server, void *source)
void add_sorting_group(uint64_t group_id, const std::string &group_name, float weight)
void handle_climate_request(AsyncWebServerRequest *request, const UrlMatch &match)
Handle a climate request under '/climate/<id>'.
void on_text_sensor_update(text_sensor::TextSensor *obj) override
void add_handler(AsyncWebHandler *handler)
ClimateSwingMode swing_mode
Definition climate.h:11
struct @65::@66 __attribute__
Wake the main loop task from an ISR. ISR-safe.
Definition main_task.h:32
uint8_t custom_preset
Definition climate.h:9
uint8_t custom_fan_mode
Definition climate.h:4
const char * message
Definition component.cpp:35
int speed
Definition fan.h:3
bool state
Definition fan.h:2
mopeka_std_values val[3]
const LogString * climate_action_to_string(ClimateAction action)
Convert the given ClimateAction to a human-readable string.
@ CLIMATE_SUPPORTS_CURRENT_HUMIDITY
@ CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE
@ CLIMATE_SUPPORTS_CURRENT_TEMPERATURE
@ CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE
const LogString * climate_swing_mode_to_string(ClimateSwingMode swing_mode)
Convert the given ClimateSwingMode to a human-readable string.
const LogString * climate_preset_to_string(ClimatePreset preset)
Convert the given PresetMode to a human-readable string.
ClimatePreset
Enum for all preset modes NOTE: If adding values, update ClimatePresetMask in climate_traits....
const LogString * climate_fan_mode_to_string(ClimateFanMode fan_mode)
Convert the given ClimateFanMode to a human-readable string.
ClimateMode
Enum for all modes a climate device can be in.
const LogString * climate_mode_to_string(ClimateMode mode)
Convert the given ClimateMode to a human-readable string.
ClimateFanMode
NOTE: If adding values, update ClimateFanModeMask in climate_traits.h to use the new last value.
const LogString * cover_operation_to_str(CoverOperation op)
Definition cover.cpp:25
const LogString * lock_state_to_string(LockState state)
Definition lock.cpp:16
LockState
Enum for all states a lock can be in.
Definition lock.h:23
Logger * global_logger
Definition logger.cpp:275
ESPHOME_ALWAYS_INLINE const char * get_use_address()
Get the active network hostname.
Definition util.h:57
constexpr float WIFI
Definition component.h:48
const char *const TAG
Definition spi.cpp:7
const LogString * update_state_to_string(UpdateState state)
const LogString * valve_operation_to_str(ValveOperation op)
Definition valve.cpp:28
@ WATER_HEATER_SUPPORTS_ON_OFF
The water heater can be turned on/off.
const LogString * water_heater_mode_to_string(WaterHeaterMode mode)
Convert the given WaterHeaterMode to a human-readable string for logging.
const char * tag
Definition log.h:74
size_t value_accuracy_to_buf(std::span< char, VALUE_ACCURACY_MAX_LEN > buf, float value, int8_t accuracy_decimals)
Format value with accuracy to buffer, returns chars written (excluding null)
Definition helpers.cpp:474
ParseOnOffState parse_on_off(const char *str, const char *on, const char *off)
Parse a string that contains either on, off or toggle.
Definition helpers.cpp:401
if(written< 0)
Definition helpers.h:1047
optional< T > parse_number(const char *str)
Parse an unsigned decimal number from a null-terminated string.
Definition helpers.h:1143
int8_t step_to_accuracy_decimals(float step)
Derive accuracy in decimals from an increment step.
Definition helpers.cpp:504
uint64_t millis_64()
Definition hal.cpp:29
const char * get_mac_address_pretty_into_buffer(std::span< char, MAC_ADDRESS_PRETTY_BUFFER_SIZE > buf)
Get the device MAC address into the given buffer, in colon-separated uppercase hex notation.
Definition helpers.cpp:751
size_t value_accuracy_with_uom_to_buf(std::span< char, VALUE_ACCURACY_MAX_LEN > buf, float value, int8_t accuracy_decimals, StringRef unit_of_measurement)
Format value with accuracy and UOM to buffer, returns chars written (excluding null)
Definition helpers.cpp:493
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:28
Application App
Global storage of Application pointer - only one Application can exist.
@ PARSE_ON
Definition helpers.h:1553
@ PARSE_TOGGLE
Definition helpers.h:1555
@ PARSE_OFF
Definition helpers.h:1554
@ PARSE_NONE
Definition helpers.h:1552
static void uint32_t
Result of matching a URL against an entity.
Definition web_server.h:54
Internal helper struct that is used to parse incoming URLs.
Definition web_server.h:60
StringRef device_name
Device name within URL, empty for main device.
Definition web_server.h:65
bool valid
Whether this match is valid.
Definition web_server.h:67
EntityMatchResult match_entity(EntityBase *entity) const
Match entity by name first, then fall back to object_id with deprecation warning Returns EntityMatchR...
StringRef method
Method within URL, for example "turn_on".
Definition web_server.h:63
bool domain_equals(const char *str) const
Definition web_server.h:70
bool method_equals(const char *str) const
Definition web_server.h:71
StringRef domain
Domain within URL, for example "sensor".
Definition web_server.h:61
uint8_t end[39]
Definition sun_gtil2.cpp:17
const size_t ESPHOME_WEBSERVER_INDEX_HTML_SIZE
const size_t ESPHOME_WEBSERVER_CSS_INCLUDE_SIZE
const size_t ESPHOME_WEBSERVER_JS_INCLUDE_SIZE