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