diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index b2c0ea14ad..d9f357f8f4 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations import base64 import gzip +from html import escape as html_escape import logging import re @@ -344,8 +345,20 @@ def build_index_html(config) -> str: if js_include: html += "" html += "" - if config[CONF_JS_URL]: - html += f'' + if js_url := config[CONF_JS_URL]: + # The interface is downloaded from the internet. Show a hint instead of a blank + # page when the browser cannot reach it, which is common in WiFi AP mode. + hint = ( + f"Could not load the web interface from {js_url}. This browser needs internet " + "access to download it. To serve it from the device instead (for example in " + "WiFi access point mode), set local: true under web_server: in the YAML " + "configuration and install the firmware again." + ) + # Escape for a JS single quoted string inside a double quoted HTML attribute. + hint_js = html_escape( + hint.replace("\\", "\\\\").replace("'", "\\'"), quote=True + ) + html += f'' html += "" return html diff --git a/tests/unit_tests/components/test_web_server.py b/tests/unit_tests/components/test_web_server.py new file mode 100644 index 0000000000..6245f9a6f9 --- /dev/null +++ b/tests/unit_tests/components/test_web_server.py @@ -0,0 +1,28 @@ +"""Tests for the web_server component index page builder.""" + +from esphome.components.web_server import build_index_html +from esphome.const import CONF_CSS_URL, CONF_JS_URL + + +def test_build_index_html_has_offline_hint() -> None: + """The hosted script tag shows a hint when the browser cannot download it.""" + html = build_index_html( + {CONF_JS_URL: "https://oi.esphome.io/v2/www.js", CONF_CSS_URL: ""} + ) + assert '