ESPHome 2026.2.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::set_css_url(const char *css_url) { this->css_url_ = css_url; }
73
74void WebServer::set_js_url(const char *js_url) { this->js_url_ = js_url; }
75
76void WebServer::handle_index_request(AsyncWebServerRequest *request) {
77 AsyncResponseStream *stream = request->beginResponseStream(ESPHOME_F("text/html"));
78 const std::string &title = App.get_name();
79 stream->print(ESPHOME_F("<!DOCTYPE html><html lang=\"en\"><head><meta charset=UTF-8><meta "
80 "name=viewport content=\"width=device-width, initial-scale=1,user-scalable=no\"><title>"));
81 stream->print(title.c_str());
82 stream->print(ESPHOME_F("</title>"));
83#ifdef USE_WEBSERVER_CSS_INCLUDE
84 stream->print(ESPHOME_F("<link rel=\"stylesheet\" href=\"/0.css\">"));
85#endif
86 if (strlen(this->css_url_) > 0) {
87 stream->print(ESPHOME_F(R"(<link rel="stylesheet" href=")"));
88 stream->print(this->css_url_);
89 stream->print(ESPHOME_F("\">"));
90 }
91 stream->print(ESPHOME_F("</head><body>"));
92 stream->print(ESPHOME_F("<article class=\"markdown-body\"><h1>"));
93 stream->print(title.c_str());
94 stream->print(ESPHOME_F("</h1>"));
95 stream->print(ESPHOME_F("<h2>States</h2><table id=\"states\"><thead><tr><th>Name<th>State<th>Actions<tbody>"));
96
97#ifdef USE_SENSOR
98 for (auto *obj : App.get_sensors()) {
99 if (this->include_internal_ || !obj->is_internal())
100 write_row(stream, obj, "sensor", "");
101 }
102#endif
103
104#ifdef USE_SWITCH
105 for (auto *obj : App.get_switches()) {
106 if (this->include_internal_ || !obj->is_internal())
107 write_row(stream, obj, "switch", "<button>Toggle</button>");
108 }
109#endif
110
111#ifdef USE_BUTTON
112 for (auto *obj : App.get_buttons())
113 write_row(stream, obj, "button", "<button>Press</button>");
114#endif
115
116#ifdef USE_BINARY_SENSOR
117 for (auto *obj : App.get_binary_sensors()) {
118 if (this->include_internal_ || !obj->is_internal())
119 write_row(stream, obj, "binary_sensor", "");
120 }
121#endif
122
123#ifdef USE_FAN
124 for (auto *obj : App.get_fans()) {
125 if (this->include_internal_ || !obj->is_internal())
126 write_row(stream, obj, "fan", "<button>Toggle</button>");
127 }
128#endif
129
130#ifdef USE_LIGHT
131 for (auto *obj : App.get_lights()) {
132 if (this->include_internal_ || !obj->is_internal())
133 write_row(stream, obj, "light", "<button>Toggle</button>");
134 }
135#endif
136
137#ifdef USE_TEXT_SENSOR
138 for (auto *obj : App.get_text_sensors()) {
139 if (this->include_internal_ || !obj->is_internal())
140 write_row(stream, obj, "text_sensor", "");
141 }
142#endif
143
144#ifdef USE_COVER
145 for (auto *obj : App.get_covers()) {
146 if (this->include_internal_ || !obj->is_internal())
147 write_row(stream, obj, "cover", "<button>Open</button><button>Close</button>");
148 }
149#endif
150
151#ifdef USE_NUMBER
152 for (auto *obj : App.get_numbers()) {
153 if (this->include_internal_ || !obj->is_internal()) {
154 write_row(stream, obj, "number", "", [](AsyncResponseStream &stream, EntityBase *obj) {
155 number::Number *number = (number::Number *) obj;
156 stream.print(R"(<input type="number" min=")");
157 stream.print(number->traits.get_min_value());
158 stream.print(R"(" max=")");
159 stream.print(number->traits.get_max_value());
160 stream.print(R"(" step=")");
161 stream.print(number->traits.get_step());
162 stream.print(R"(" value=")");
163 stream.print(number->state);
164 stream.print(R"("/>)");
165 });
166 }
167 }
168#endif
169
170#ifdef USE_TEXT
171 for (auto *obj : App.get_texts()) {
172 if (this->include_internal_ || !obj->is_internal()) {
173 write_row(stream, obj, "text", "", [](AsyncResponseStream &stream, EntityBase *obj) {
174 text::Text *text = (text::Text *) obj;
175 auto mode = (int) text->traits.get_mode();
176 stream.print(R"(<input type=")");
177 if (mode == 2) {
178 stream.print(R"(password)");
179 } else { // default
180 stream.print(R"(text)");
181 }
182 stream.print(R"(" minlength=")");
183 stream.print(text->traits.get_min_length());
184 stream.print(R"(" maxlength=")");
185 stream.print(text->traits.get_max_length());
186 stream.print(R"(" pattern=")");
187 stream.print(text->traits.get_pattern_c_str());
188 stream.print(R"(" value=")");
189 stream.print(text->state.c_str());
190 stream.print(R"("/>)");
191 });
192 }
193 }
194#endif
195
196#ifdef USE_SELECT
197 for (auto *obj : App.get_selects()) {
198 if (this->include_internal_ || !obj->is_internal()) {
199 write_row(stream, obj, "select", "", [](AsyncResponseStream &stream, EntityBase *obj) {
200 select::Select *select = (select::Select *) obj;
201 stream.print("<select>");
202 stream.print("<option></option>");
203 for (auto const &option : select->traits.get_options()) {
204 stream.print("<option>");
205 stream.print(option);
206 stream.print("</option>");
207 }
208 stream.print("</select>");
209 });
210 }
211 }
212#endif
213
214#ifdef USE_LOCK
215 for (auto *obj : App.get_locks()) {
216 if (this->include_internal_ || !obj->is_internal()) {
217 write_row(stream, obj, "lock", "", [](AsyncResponseStream &stream, EntityBase *obj) {
218 lock::Lock *lock = (lock::Lock *) obj;
219 stream.print("<button>Lock</button><button>Unlock</button>");
220 if (lock->traits.get_supports_open()) {
221 stream.print("<button>Open</button>");
222 }
223 });
224 }
225 }
226#endif
227
228#ifdef USE_CLIMATE
229 for (auto *obj : App.get_climates()) {
230 if (this->include_internal_ || !obj->is_internal())
231 write_row(stream, obj, "climate", "");
232 }
233#endif
234
235#ifdef USE_WATER_HEATER
236 for (auto *obj : App.get_water_heaters()) {
237 if (this->include_internal_ || !obj->is_internal())
238 write_row(stream, obj, "water_heater", "");
239 }
240#endif
241
242 stream->print(ESPHOME_F("</tbody></table><p>See <a href=\"https://esphome.io/web-api/\">ESPHome Web API</a> for "
243 "REST API documentation.</p>"));
244#if defined(USE_WEBSERVER_OTA) && !defined(USE_WEBSERVER_OTA_DISABLED)
245 // Show OTA form only if web_server OTA is not explicitly disabled
246 // Note: USE_WEBSERVER_OTA_DISABLED only affects web_server, not captive_portal
247 stream->print(
248 ESPHOME_F("<h2>OTA Update</h2><form method=\"POST\" action=\"/update\" enctype=\"multipart/form-data\"><input "
249 "type=\"file\" name=\"update\"><input type=\"submit\" value=\"Update\"></form>"));
250#endif
251 stream->print(ESPHOME_F("<h2>Debug Log</h2><pre id=\"log\"></pre>"));
252#ifdef USE_WEBSERVER_JS_INCLUDE
253 if (this->js_include_ != nullptr) {
254 stream->print(ESPHOME_F("<script type=\"module\" src=\"/0.js\"></script>"));
255 }
256#endif
257 if (strlen(this->js_url_) > 0) {
258 stream->print(ESPHOME_F("<script src=\""));
259 stream->print(this->js_url_);
260 stream->print(ESPHOME_F("\"></script>"));
261 }
262 stream->print(ESPHOME_F("</article></body></html>"));
263 request->send(stream);
264}
265
266} // namespace esphome::web_server
267#endif
BedjetMode mode
BedJet operating mode.
const std::string & get_name() const
Get the name of this Application set by pre_setup().
auto & get_binary_sensors() const
void set_css_url(const char *css_url)
Set the URL to the CSS <link> that's sent to each client.
void set_js_url(const char *js_url)
Set the URL to the script that's embedded in the index page.
void handle_index_request(AsyncWebServerRequest *request)
Handle an index request under '/'.
Application App
Global storage of Application pointer - only one Application can exist.