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