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