Construct web_server assets at build time instead of run time (#4944)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
J. Nick Koston 2023-06-18 20:51:19 -05:00 committed by Jesse Hills
parent 76e947651d
commit 959e7745a6
No known key found for this signature in database
GPG key ID: BEAAE804EFD8E83A
4 changed files with 100 additions and 45 deletions

View file

@ -81,6 +81,37 @@ CONFIG_SCHEMA = cv.All(
) )
def build_index_html(config) -> str:
html = "<!DOCTYPE html><html><head><meta charset=UTF-8><link rel=icon href=data:>"
css_include = config.get(CONF_CSS_INCLUDE)
js_include = config.get(CONF_JS_INCLUDE)
if css_include:
html += "<link rel=stylesheet href=/0.css>"
if config[CONF_CSS_URL]:
html += f'<link rel=stylesheet href="{config[CONF_CSS_URL]}">'
html += "</head><body>"
if js_include:
html += "<script type=module src=/0.js></script>"
html += "<esp-app></esp-app>"
if config[CONF_JS_URL]:
html += f'<script src="{config[CONF_JS_URL]}"></script>'
html += "</body></html>"
return html
def add_resource_as_progmem(resource_name: str, content: str) -> None:
"""Add a resource to progmem."""
content_encoded = content.encode("utf-8")
content_encoded_size = len(content_encoded)
bytes_as_int = ", ".join(str(x) for x in content_encoded)
uint8_t = f"const uint8_t ESPHOME_WEBSERVER_{resource_name}[{content_encoded_size}] PROGMEM = {{{bytes_as_int}}}"
size_t = (
f"const size_t ESPHOME_WEBSERVER_{resource_name}_SIZE = {content_encoded_size}"
)
cg.add_global(cg.RawExpression(uint8_t))
cg.add_global(cg.RawExpression(size_t))
@coroutine_with_priority(40.0) @coroutine_with_priority(40.0)
async def to_code(config): async def to_code(config):
paren = await cg.get_variable(config[CONF_WEB_SERVER_BASE_ID]) paren = await cg.get_variable(config[CONF_WEB_SERVER_BASE_ID])
@ -89,13 +120,17 @@ async def to_code(config):
await cg.register_component(var, config) await cg.register_component(var, config)
cg.add_define("USE_WEBSERVER") cg.add_define("USE_WEBSERVER")
version = config[CONF_VERSION]
cg.add(paren.set_port(config[CONF_PORT])) cg.add(paren.set_port(config[CONF_PORT]))
cg.add_define("USE_WEBSERVER") cg.add_define("USE_WEBSERVER")
cg.add_define("USE_WEBSERVER_PORT", config[CONF_PORT]) cg.add_define("USE_WEBSERVER_PORT", config[CONF_PORT])
cg.add_define("USE_WEBSERVER_VERSION", config[CONF_VERSION]) cg.add_define("USE_WEBSERVER_VERSION", version)
cg.add(var.set_css_url(config[CONF_CSS_URL])) if version == 2:
cg.add(var.set_js_url(config[CONF_JS_URL])) add_resource_as_progmem("INDEX_HTML", build_index_html(config))
else:
cg.add(var.set_css_url(config[CONF_CSS_URL]))
cg.add(var.set_js_url(config[CONF_JS_URL]))
cg.add(var.set_allow_ota(config[CONF_OTA])) cg.add(var.set_allow_ota(config[CONF_OTA]))
if CONF_AUTH in config: if CONF_AUTH in config:
cg.add(paren.set_auth_username(config[CONF_AUTH][CONF_USERNAME])) cg.add(paren.set_auth_username(config[CONF_AUTH][CONF_USERNAME]))
@ -103,13 +138,13 @@ async def to_code(config):
if CONF_CSS_INCLUDE in config: if CONF_CSS_INCLUDE in config:
cg.add_define("USE_WEBSERVER_CSS_INCLUDE") cg.add_define("USE_WEBSERVER_CSS_INCLUDE")
path = CORE.relative_config_path(config[CONF_CSS_INCLUDE]) path = CORE.relative_config_path(config[CONF_CSS_INCLUDE])
with open(file=path, encoding="utf-8") as myfile: with open(file=path, encoding="utf-8") as css_file:
cg.add(var.set_css_include(myfile.read())) add_resource_as_progmem("CSS_INCLUDE", css_file.read())
if CONF_JS_INCLUDE in config: if CONF_JS_INCLUDE in config:
cg.add_define("USE_WEBSERVER_JS_INCLUDE") cg.add_define("USE_WEBSERVER_JS_INCLUDE")
path = CORE.relative_config_path(config[CONF_JS_INCLUDE]) path = CORE.relative_config_path(config[CONF_JS_INCLUDE])
with open(file=path, encoding="utf-8") as myfile: with open(file=path, encoding="utf-8") as js_file:
cg.add(var.set_js_include(myfile.read())) add_resource_as_progmem("JS_INCLUDE", js_file.read())
cg.add(var.set_include_internal(config[CONF_INCLUDE_INTERNAL])) cg.add(var.set_include_internal(config[CONF_INCLUDE_INTERNAL]))
if CONF_LOCAL in config and config[CONF_LOCAL]: if CONF_LOCAL in config and config[CONF_LOCAL]:
cg.add_define("USE_WEBSERVER_LOCAL") cg.add_define("USE_WEBSERVER_LOCAL")

View file

@ -90,10 +90,17 @@ WebServer::WebServer(web_server_base::WebServerBase *base)
#endif #endif
} }
#if USE_WEBSERVER_VERSION == 1
void WebServer::set_css_url(const char *css_url) { this->css_url_ = css_url; } void WebServer::set_css_url(const char *css_url) { this->css_url_ = css_url; }
void WebServer::set_css_include(const char *css_include) { this->css_include_ = css_include; }
void WebServer::set_js_url(const char *js_url) { this->js_url_ = js_url; } void WebServer::set_js_url(const char *js_url) { this->js_url_ = js_url; }
#endif
#ifdef USE_WEBSERVER_CSS_INCLUDE
void WebServer::set_css_include(const char *css_include) { this->css_include_ = css_include; }
#endif
#ifdef USE_WEBSERVER_JS_INCLUDE
void WebServer::set_js_include(const char *js_include) { this->js_include_ = js_include; } void WebServer::set_js_include(const char *js_include) { this->js_include_ = js_include; }
#endif
void WebServer::setup() { void WebServer::setup() {
ESP_LOGCONFIG(TAG, "Setting up web server..."); ESP_LOGCONFIG(TAG, "Setting up web server...");
@ -159,20 +166,14 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) {
response->addHeader("Content-Encoding", "gzip"); response->addHeader("Content-Encoding", "gzip");
request->send(response); request->send(response);
} }
#else #elif USE_WEBSERVER_VERSION == 1
void WebServer::handle_index_request(AsyncWebServerRequest *request) { void WebServer::handle_index_request(AsyncWebServerRequest *request) {
AsyncResponseStream *stream = request->beginResponseStream("text/html"); AsyncResponseStream *stream = request->beginResponseStream("text/html");
// All content is controlled and created by user - so allowing all origins is fine here.
stream->addHeader("Access-Control-Allow-Origin", "*");
#if USE_WEBSERVER_VERSION == 1
const std::string &title = App.get_name(); const std::string &title = App.get_name();
stream->print(F("<!DOCTYPE html><html lang=\"en\"><head><meta charset=UTF-8><meta " stream->print(F("<!DOCTYPE html><html lang=\"en\"><head><meta charset=UTF-8><meta "
"name=viewport content=\"width=device-width, initial-scale=1,user-scalable=no\"><title>")); "name=viewport content=\"width=device-width, initial-scale=1,user-scalable=no\"><title>"));
stream->print(title.c_str()); stream->print(title.c_str());
stream->print(F("</title>")); stream->print(F("</title>"));
#else
stream->print(F("<!DOCTYPE html><html><head><meta charset=UTF-8><link rel=icon href=data:>"));
#endif
#ifdef USE_WEBSERVER_CSS_INCLUDE #ifdef USE_WEBSERVER_CSS_INCLUDE
stream->print(F("<link rel=\"stylesheet\" href=\"/0.css\">")); stream->print(F("<link rel=\"stylesheet\" href=\"/0.css\">"));
#endif #endif
@ -182,7 +183,6 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) {
stream->print(F("\">")); stream->print(F("\">"));
} }
stream->print(F("</head><body>")); stream->print(F("</head><body>"));
#if USE_WEBSERVER_VERSION == 1
stream->print(F("<article class=\"markdown-body\"><h1>")); stream->print(F("<article class=\"markdown-body\"><h1>"));
stream->print(title.c_str()); stream->print(title.c_str());
stream->print(F("</h1>")); stream->print(F("</h1>"));
@ -308,49 +308,40 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) {
"type=\"file\" name=\"update\"><input type=\"submit\" value=\"Update\"></form>")); "type=\"file\" name=\"update\"><input type=\"submit\" value=\"Update\"></form>"));
} }
stream->print(F("<h2>Debug Log</h2><pre id=\"log\"></pre>")); stream->print(F("<h2>Debug Log</h2><pre id=\"log\"></pre>"));
#endif
#ifdef USE_WEBSERVER_JS_INCLUDE #ifdef USE_WEBSERVER_JS_INCLUDE
if (this->js_include_ != nullptr) { if (this->js_include_ != nullptr) {
stream->print(F("<script type=\"module\" src=\"/0.js\"></script>")); stream->print(F("<script type=\"module\" src=\"/0.js\"></script>"));
} }
#endif
#if USE_WEBSERVER_VERSION == 2
stream->print(F("<esp-app></esp-app>"));
#endif #endif
if (strlen(this->js_url_) > 0) { if (strlen(this->js_url_) > 0) {
stream->print(F("<script src=\"")); stream->print(F("<script src=\""));
stream->print(this->js_url_); stream->print(this->js_url_);
stream->print(F("\"></script>")); stream->print(F("\"></script>"));
} }
#if USE_WEBSERVER_VERSION == 1
stream->print(F("</article></body></html>")); stream->print(F("</article></body></html>"));
#else
stream->print(F("</body></html>"));
#endif
request->send(stream); request->send(stream);
} }
#elif USE_WEBSERVER_VERSION == 2
void WebServer::handle_index_request(AsyncWebServerRequest *request) {
AsyncWebServerResponse *response =
request->beginResponse_P(200, "text/html", ESPHOME_WEBSERVER_INDEX_HTML, ESPHOME_WEBSERVER_INDEX_HTML_SIZE);
request->send(response);
}
#endif #endif
#ifdef USE_WEBSERVER_CSS_INCLUDE #ifdef USE_WEBSERVER_CSS_INCLUDE
void WebServer::handle_css_request(AsyncWebServerRequest *request) { void WebServer::handle_css_request(AsyncWebServerRequest *request) {
AsyncResponseStream *stream = request->beginResponseStream("text/css"); AsyncWebServerResponse *response =
if (this->css_include_ != nullptr) { request->beginResponse_P(200, "text/css", ESPHOME_WEBSERVER_CSS_INCLUDE, ESPHOME_WEBSERVER_CSS_INCLUDE_SIZE);
stream->print(this->css_include_); request->send(response);
}
request->send(stream);
} }
#endif #endif
#ifdef USE_WEBSERVER_JS_INCLUDE #ifdef USE_WEBSERVER_JS_INCLUDE
void WebServer::handle_js_request(AsyncWebServerRequest *request) { void WebServer::handle_js_request(AsyncWebServerRequest *request) {
AsyncResponseStream *stream = request->beginResponseStream("text/javascript"); AsyncWebServerResponse *response =
if (this->js_include_ != nullptr) { request->beginResponse_P(200, "text/javascript", ESPHOME_WEBSERVER_JS_INCLUDE, ESPHOME_WEBSERVER_JS_INCLUDE_SIZE);
stream->addHeader("Access-Control-Allow-Origin", "*"); request->send(response);
stream->print(this->js_include_);
}
request->send(stream);
} }
#endif #endif

View file

@ -14,6 +14,22 @@
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
#include <freertos/semphr.h> #include <freertos/semphr.h>
#endif #endif
#if USE_WEBSERVER_VERSION == 2
extern const uint8_t ESPHOME_WEBSERVER_INDEX_HTML[] PROGMEM;
extern const size_t ESPHOME_WEBSERVER_INDEX_HTML_SIZE;
#endif
#ifdef USE_WEBSERVER_CSS_INCLUDE
extern const uint8_t ESPHOME_WEBSERVER_CSS_INCLUDE[] PROGMEM;
extern const size_t ESPHOME_WEBSERVER_CSS_INCLUDE_SIZE;
#endif
#ifdef USE_WEBSERVER_JS_INCLUDE
extern const uint8_t ESPHOME_WEBSERVER_JS_INCLUDE[] PROGMEM;
extern const size_t ESPHOME_WEBSERVER_JS_INCLUDE_SIZE;
#endif
namespace esphome { namespace esphome {
namespace web_server { namespace web_server {
@ -40,6 +56,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
public: public:
WebServer(web_server_base::WebServerBase *base); WebServer(web_server_base::WebServerBase *base);
#if USE_WEBSERVER_VERSION == 1
/** Set the URL to the CSS <link> that's sent to each client. Defaults to /** Set the URL to the CSS <link> that's sent to each client. Defaults to
* https://esphome.io/_static/webserver-v1.min.css * https://esphome.io/_static/webserver-v1.min.css
* *
@ -47,24 +64,29 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
*/ */
void set_css_url(const char *css_url); void set_css_url(const char *css_url);
/** Set local path to the script that's embedded in the index page. Defaults to
*
* @param css_include Local path to web server script.
*/
void set_css_include(const char *css_include);
/** Set the URL to the script that's embedded in the index page. Defaults to /** Set the URL to the script that's embedded in the index page. Defaults to
* https://esphome.io/_static/webserver-v1.min.js * https://esphome.io/_static/webserver-v1.min.js
* *
* @param js_url The url to the web server script. * @param js_url The url to the web server script.
*/ */
void set_js_url(const char *js_url); void set_js_url(const char *js_url);
#endif
#ifdef USE_WEBSERVER_CSS_INCLUDE
/** Set local path to the script that's embedded in the index page. Defaults to
*
* @param css_include Local path to web server script.
*/
void set_css_include(const char *css_include);
#endif
#ifdef USE_WEBSERVER_JS_INCLUDE
/** Set local path to the script that's embedded in the index page. Defaults to /** Set local path to the script that's embedded in the index page. Defaults to
* *
* @param js_include Local path to web server script. * @param js_include Local path to web server script.
*/ */
void set_js_include(const char *js_include); void set_js_include(const char *js_include);
#endif
/** Determine whether internal components should be displayed on the web server. /** Determine whether internal components should be displayed on the web server.
* Defaults to false. * Defaults to false.
@ -240,10 +262,16 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
web_server_base::WebServerBase *base_; web_server_base::WebServerBase *base_;
AsyncEventSource events_{"/events"}; AsyncEventSource events_{"/events"};
ListEntitiesIterator entities_iterator_; ListEntitiesIterator entities_iterator_;
#if USE_WEBSERVER_VERSION == 1
const char *css_url_{nullptr}; const char *css_url_{nullptr};
const char *css_include_{nullptr};
const char *js_url_{nullptr}; const char *js_url_{nullptr};
#endif
#ifdef USE_WEBSERVER_CSS_INCLUDE
const char *css_include_{nullptr};
#endif
#ifdef USE_WEBSERVER_JS_INCLUDE
const char *js_include_{nullptr}; const char *js_include_{nullptr};
#endif
bool include_internal_{false}; bool include_internal_{false};
bool allow_ota_{true}; bool allow_ota_{true};
#ifdef USE_ESP32 #ifdef USE_ESP32

View file

@ -83,6 +83,7 @@ class WebServerBase : public Component {
return; return;
} }
this->server_ = std::make_shared<AsyncWebServer>(this->port_); this->server_ = std::make_shared<AsyncWebServer>(this->port_);
// All content is controlled and created by user - so allowing all origins is fine here.
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*"); DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*");
this->server_->begin(); this->server_->begin();