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