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