[web_server] Surface implied behavior changes and honor manual_ip in the port warning

This commit is contained in:
J. Nick Koston
2026-08-19 16:09:27 -05:00
parent f8a60d0259
commit 7f437aa680
2 changed files with 60 additions and 15 deletions
+29 -15
View File
@@ -23,11 +23,13 @@ from esphome.const import (
CONF_JS_URL,
CONF_LOCAL,
CONF_LOG,
CONF_MANUAL_IP,
CONF_NAME,
CONF_NETWORKS,
CONF_OTA,
CONF_PASSWORD,
CONF_PORT,
CONF_STATIC_IP,
CONF_TYPE,
CONF_USERNAME,
CONF_VERSION,
@@ -384,32 +386,44 @@ def serve_captive(config: ConfigType, full_config: ConfigType) -> bool:
def _final_validate_ap_mode(config: ConfigType) -> None:
full_config = fv.full_config.get()
wifi_config = full_config.get(CONF_WIFI)
if serve_captive(config, full_config):
captive = serve_captive(config, full_config)
local = serve_local(config, wifi_config)
if captive:
web_server_base.consume_captive_dns_sockets(config, "web_server")
if CONF_LOCAL not in config:
_LOGGER.info(
"WiFi is AP only: embedding the web interface in the firmware "
"(local: true, roughly 13 KB of flash for version 2, 78 KB for "
"version 3) and serving it as a captive portal on the access point. "
"Set 'local: false' to load it from the internet instead."
)
elif wifi_is_ap_only(wifi_config) and not serve_local(config, wifi_config):
# Surface behavior that the config does not spell out.
if local and CONF_LOCAL not in config:
_LOGGER.info(
"WiFi is AP only: embedding the web interface in the firmware "
"(local: true, roughly 13 KB of flash for version 2, 78 KB for version 3)%s. "
"Set 'local: false' to load it from the internet instead.",
" and serving it as a captive portal on the access point"
if captive
else "",
)
elif captive and not wifi_is_ap_only(wifi_config):
_LOGGER.info(
"web_server will act as a captive portal while the fallback access point "
"is active."
)
if not wifi_is_ap_only(wifi_config):
return
if not local:
_LOGGER.warning(
"WiFi is AP only and the web_server interface is loaded from the internet, "
"which browsers on the access point usually cannot reach; the page stays "
"blank. Remove 'local: false', or migrate off version 1, so the interface "
"is embedded in the firmware."
)
elif (
wifi_is_ap_only(wifi_config)
and serve_local(config, wifi_config)
and config[CONF_PORT] != 80
):
elif config[CONF_PORT] != 80:
ap_ip = "192.168.4.1"
if (manual_ip := wifi_config[CONF_AP].get(CONF_MANUAL_IP)) is not None:
ap_ip = str(manual_ip[CONF_STATIC_IP])
_LOGGER.warning(
"WiFi is AP only and web_server uses port %d. The interface cannot open "
"automatically on the access point (captive portal detection only works on "
"port 80); open http://192.168.4.1:%d/ manually, or remove 'port:' to use 80.",
"port 80); open http://%s:%d/ manually, or remove 'port:' to use 80.",
config[CONF_PORT],
ap_ip,
config[CONF_PORT],
)
@@ -108,3 +108,34 @@ def test_final_validate_ap_mode_warns_for_non_default_port(
fv.full_config.reset(token)
assert "cannot open automatically" in caplog.text
assert "http://192.168.4.1:8080/" in caplog.text
def test_final_validate_ap_mode_port_warning_uses_manual_ip(
caplog: pytest.LogCaptureFixture,
) -> None:
"""The manual URL in the port warning honors wifi.ap.manual_ip."""
from esphome.const import CONF_MANUAL_IP, CONF_STATIC_IP
config = {CONF_VERSION: 2, CONF_PORT: 8080}
wifi = {CONF_AP: {CONF_MANUAL_IP: {CONF_STATIC_IP: "10.0.0.1"}}}
token = fv.full_config.set({"web_server": config, CONF_WIFI: wifi})
try:
with caplog.at_level(logging.WARNING):
_final_validate_ap_mode(config)
finally:
fv.full_config.reset(token)
assert "http://10.0.0.1:8080/" in caplog.text
def test_final_validate_ap_mode_informs_fallback_captive(
caplog: pytest.LogCaptureFixture,
) -> None:
"""Explicit local: true on a fallback AP logs that web_server becomes captive."""
config = {CONF_VERSION: 2, CONF_PORT: 80, CONF_LOCAL: True}
token = fv.full_config.set({"web_server": config, CONF_WIFI: AP_FALLBACK})
try:
with caplog.at_level(logging.INFO):
_final_validate_ap_mode(config)
finally:
fv.full_config.reset(token)
assert "fallback access point" in caplog.text