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