ESPHome 2026.10.2
Loading...
Searching...
No Matches
web_server_v1.cpp
Go to the documentation of this file.
1#include "web_server.h"
3
4#if USE_WEBSERVER_VERSION == 1
5
6namespace esphome::web_server {
7
8// Write HTML-escaped text to stream (escapes ", &, <, >)
9static void write_html_escaped(AsyncResponseStream *stream, const char *text) {
10 for (const char *p = text; *p; ++p) {
11 switch (*p) {
12 case '"':
13 stream->print("&quot;");
14 break;
15 case '&':
16 stream->print("&amp;");
17 break;
18 case '<':
19 stream->print("&lt;");
20 break;
21 case '>':
22 stream->print("&gt;");
23 break;
24 default:
25 stream->write(*p);
26 break;
27 }
28 }
29}
30
31void write_row(AsyncResponseStream *stream, EntityBase *obj, const std::string &klass, const std::string &action,
32 const std::function<void(AsyncResponseStream &stream, EntityBase *obj)> &action_func = nullptr) {
33 stream->print("<tr class=\"");
34 stream->print(klass.c_str());
35 if (obj->is_internal())
36 stream->print(" internal");
37 stream->print("\" id=\"");
38 stream->print(klass.c_str());
39 stream->print("-");
40 char object_id_buf[OBJECT_ID_MAX_LEN];
41 stream->print(obj->get_object_id_to(object_id_buf).c_str());
42 // Add data attributes for hierarchical URL support
43 stream->print("\" data-domain=\"");
44 stream->print(klass.c_str());
45 stream->print("\" data-name=\"");
46 write_html_escaped(stream, obj->get_name().c_str());
47#ifdef USE_DEVICES
48 Device *device = obj->get_device();
49 if (device != nullptr) {
50 stream->print("\" data-device=\"");
51 write_html_escaped(stream, device->get_name());
52 }
53#endif
54 stream->print("\"><td>");
55#ifdef USE_DEVICES
56 if (device != nullptr) {
57 stream->print("[");
58 write_html_escaped(stream, device->get_name());
59 stream->print("] ");
60 }
61#endif
62 write_html_escaped(stream, obj->get_name().c_str());
63 stream->print("</td><td></td><td>");
64 stream->print(action.c_str());
65 if (action_func) {
66 action_func(*stream, obj);
67 }
68 stream->print("</td>");
69 stream->print("</tr>");
70}
71
72void WebServer::handle_index_request(AsyncWebServerRequest *request) {
73 AsyncResponseStream *stream = request->beginResponseStream(ESPHOME_F("text/html"));
74 const auto &title = App.get_name();
75 stream->print(ESPHOME_F("<!DOCTYPE html><html lang=\"en\"><head><meta charset=UTF-8><meta "
76 "name=viewport content=\"width=device-width, initial-scale=1,user-scalable=no\"><title>"));
77 stream->print(title.c_str());
78 stream->print(ESPHOME_F("</title>"));
79#ifdef USE_WEBSERVER_CSS_INCLUDE
80 stream->print(ESPHOME_F("<link rel=\"stylesheet\" href=\"/0.css\">"));
81#endif
82 if (strlen(this->css_url_) > 0) {
83 stream->print(ESPHOME_F(R"(<link rel="stylesheet" href=")"));
84 stream->print(this->css_url_);
85 stream->print(ESPHOME_F("\">"));
86 }
87 stream->print(ESPHOME_F("</head><body>"));
88 stream->print(ESPHOME_F("<article class=\"markdown-body\"><h1>"));
89 stream->print(title.c_str());
90 stream->print(ESPHOME_F("</h1>"));
91 stream->print(ESPHOME_F("<h2>States</h2><table id=\"states\"><thead><tr><th>Name<th>State<th>Actions<tbody>"));
92
93#ifdef USE_SENSOR
94 for (auto *obj : App.get_sensors()) {
95 if (this->include_internal_ || !obj->is_internal())
96 write_row(stream, obj, "sensor", "");
97 }
98#endif
99
100#ifdef USE_SWITCH
101 for (auto *obj : App.get_switches()) {
102 if (this->include_internal_ || !obj->is_internal())
103 write_row(stream, obj, "switch", "<button>Toggle</button>");
104 }
105#endif
106
107#ifdef USE_BUTTON
108 for (auto *obj : App.get_buttons())
109 write_row(stream, obj, "button", "<button>Press</button>");
110#endif
111
112#ifdef USE_BINARY_SENSOR
113 for (auto *obj : App.get_binary_sensors()) {
114 if (this->include_internal_ || !obj->is_internal())
115 write_row(stream, obj, "binary_sensor", "");
116 }
117#endif
118
119#ifdef USE_FAN
120 for (auto *obj : App.get_fans()) {
121 if (this->include_internal_ || !obj->is_internal())
122 write_row(stream, obj, "fan", "<button>Toggle</button>");
123 }
124#endif
125
126#ifdef USE_LIGHT
127 for (auto *obj : App.get_lights()) {
128 if (this->include_internal_ || !obj->is_internal())
129 write_row(stream, obj, "light", "<button>Toggle</button>");
130 }
131#endif
132
133#ifdef USE_TEXT_SENSOR
134 for (auto *obj : App.get_text_sensors()) {
135 if (this->include_internal_ || !obj->is_internal())
136 write_row(stream, obj, "text_sensor", "");
137 }
138#endif
139
140#ifdef USE_COVER
141 for (auto *obj : App.get_covers()) {
142 if (this->include_internal_ || !obj->is_internal())
143 write_row(stream, obj, "cover", "<button>Open</button><button>Close</button>");
144 }
145#endif
146
147#ifdef USE_NUMBER
148 for (auto *obj : App.get_numbers()) {
149 if (this->include_internal_ || !obj->is_internal()) {
150 write_row(stream, obj, "number", "", [](AsyncResponseStream &stream, EntityBase *obj) {
151 number::Number *number = (number::Number *) obj;
152 stream.print(R"(<input type="number" min=")");
153 stream.print(number->traits.get_min_value());
154 stream.print(R"(" max=")");
155 stream.print(number->traits.get_max_value());
156 stream.print(R"(" step=")");
157 stream.print(number->traits.get_step());
158 stream.print(R"(" value=")");
159 stream.print(number->state);
160 stream.print(R"("/>)");
161 });
162 }
163 }
164#endif
165
166#ifdef USE_TEXT
167 for (auto *obj : App.get_texts()) {
168 if (this->include_internal_ || !obj->is_internal()) {
169 write_row(stream, obj, "text", "", [](AsyncResponseStream &stream, EntityBase *obj) {
170 text::Text *text = (text::Text *) obj;
171 auto mode = (int) text->traits.get_mode();
172 stream.print(R"(<input type=")");
173 if (mode == 2) {
174 stream.print(R"(password)");
175 } else { // default
176 stream.print(R"(text)");
177 }
178 stream.print(R"(" minlength=")");
179 stream.print(text->traits.get_min_length());
180 stream.print(R"(" maxlength=")");
181 stream.print(text->traits.get_max_length());
182 stream.print(R"(" pattern=")");
183 stream.print(text->traits.get_pattern_c_str());
184 stream.print(R"(" value=")");
185 stream.print(text->state.c_str());
186 stream.print(R"("/>)");
187 });
188 }
189 }
190#endif
191
192#ifdef USE_SELECT
193 for (auto *obj : App.get_selects()) {
194 if (this->include_internal_ || !obj->is_internal()) {
195 write_row(stream, obj, "select", "", [](AsyncResponseStream &stream, EntityBase *obj) {
196 select::Select *select = (select::Select *) obj;
197 stream.print("<select>");
198 stream.print("<option></option>");
199 for (auto const &option : select->traits.get_options()) {
200 stream.print("<option>");
201 stream.print(option);
202 stream.print("</option>");
203 }
204 stream.print("</select>");
205 });
206 }
207 }
208#endif
209
210#ifdef USE_LOCK
211 for (auto *obj : App.get_locks()) {
212 if (this->include_internal_ || !obj->is_internal()) {
213 write_row(stream, obj, "lock", "", [](AsyncResponseStream &stream, EntityBase *obj) {
214 lock::Lock *lock = (lock::Lock *) obj;
215 stream.print("<button>Lock</button><button>Unlock</button>");
216 if (lock->traits.get_supports_open()) {
217 stream.print("<button>Open</button>");
218 }
219 });
220 }
221 }
222#endif
223
224#ifdef USE_CLIMATE
225 for (auto *obj : App.get_climates()) {
226 if (this->include_internal_ || !obj->is_internal())
227 write_row(stream, obj, "climate", "");
228 }
229#endif
230
231#ifdef USE_WATER_HEATER
232 for (auto *obj : App.get_water_heaters()) {
233 if (this->include_internal_ || !obj->is_internal())
234 write_row(stream, obj, "water_heater", "");
235 }
236#endif
237
238 stream->print(ESPHOME_F("</tbody></table><p>See <a href=\"https://esphome.io/web-api/\">ESPHome Web API</a> for "
239 "REST API documentation.</p>"));
240#if defined(USE_WEBSERVER_OTA) && !defined(USE_WEBSERVER_OTA_DISABLED)
241 // Show OTA form only if web_server OTA is not explicitly disabled
242 // Note: USE_WEBSERVER_OTA_DISABLED only affects web_server, not captive_portal
243 stream->print(
244 ESPHOME_F("<h2>OTA Update</h2><form method=\"POST\" action=\"/update\" enctype=\"multipart/form-data\"><input "
245 "type=\"file\" name=\"update\"><input type=\"submit\" value=\"Update\"></form>"));
246#endif
247 stream->print(ESPHOME_F("<h2>Debug Log</h2><pre id=\"log\"></pre>"));
248#ifdef USE_WEBSERVER_JS_INCLUDE
249 if (this->js_include_ != nullptr) {
250 stream->print(ESPHOME_F("<script type=\"module\" src=\"/0.js\"></script>"));
251 }
252#endif
253 if (strlen(this->js_url_) > 0) {
254 stream->print(ESPHOME_F("<script src=\""));
255 stream->print(this->js_url_);
256 stream->print(ESPHOME_F("\"></script>"));
257 }
258 stream->print(ESPHOME_F("</article></body></html>"));
259 request->send(stream);
260}
261
262} // namespace esphome::web_server
263#endif
BedjetMode mode
BedJet operating mode.
const StringRef & get_name() const
Get the name of this Application set by pre_setup().
void handle_index_request(AsyncWebServerRequest *request)
Handle an index request under '/'.
Application App
Global storage of Application pointer - only one Application can exist.
SemaphoreHandle_t lock