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