[web_server] Share the captive DNS server, use the not-found fallback for probes, fold portal hooks in wifi

This commit is contained in:
J. Nick Koston
2026-08-19 14:15:06 -05:00
parent 4dad932cd8
commit d88a95c3f6
14 changed files with 243 additions and 223 deletions
+29 -39
View File
@@ -8,7 +8,6 @@ from esphome.components.web_server import (
_final_validate_ap_mode,
serve_captive,
serve_local,
wifi_is_ap_only,
)
from esphome.const import (
CONF_AP,
@@ -25,14 +24,6 @@ AP_FALLBACK = {CONF_AP: {}, CONF_NETWORKS: [{CONF_SSID: "x"}]}
STA_ONLY = {CONF_NETWORKS: [{CONF_SSID: "x"}]}
@pytest.mark.parametrize(
("wifi_config", "expected"),
[(AP_ONLY, True), (AP_FALLBACK, False), (STA_ONLY, False), (None, False)],
)
def test_wifi_is_ap_only(wifi_config: dict | None, expected: bool) -> None:
assert wifi_is_ap_only(wifi_config) is expected
@pytest.mark.parametrize(
("web_server_config", "wifi_config", "expected"),
[
@@ -57,45 +48,44 @@ def test_serve_local(
@pytest.mark.parametrize(
("web_server_config", "wifi_config", "has_captive_portal", "expected"),
("web_server_config", "full_config", "expected"),
[
# AP only: local is implied, web_server is the captive portal.
({CONF_VERSION: 2}, AP_ONLY, False, True),
({CONF_VERSION: 2}, {CONF_WIFI: AP_ONLY}, True),
# AP fallback needs an explicit local: true to be captive.
({CONF_VERSION: 2}, AP_FALLBACK, False, False),
({CONF_VERSION: 2, CONF_LOCAL: True}, AP_FALLBACK, False, True),
({CONF_VERSION: 2}, {CONF_WIFI: AP_FALLBACK}, False),
({CONF_VERSION: 2, CONF_LOCAL: True}, {CONF_WIFI: AP_FALLBACK}, True),
# captive_portal owns the role when configured.
({CONF_VERSION: 2}, AP_ONLY, True, False),
# No AP, hosted page, or version 1: never captive.
({CONF_VERSION: 2, CONF_LOCAL: True}, STA_ONLY, False, False),
({CONF_VERSION: 2, CONF_LOCAL: False}, AP_ONLY, False, False),
({CONF_VERSION: 1}, AP_ONLY, False, False),
({CONF_VERSION: 2}, {CONF_WIFI: AP_ONLY, "captive_portal": {}}, False),
# No AP, no wifi, hosted page, or version 1: never captive.
({CONF_VERSION: 2, CONF_LOCAL: True}, {CONF_WIFI: STA_ONLY}, False),
({CONF_VERSION: 2, CONF_LOCAL: True}, {}, False),
({CONF_VERSION: 2, CONF_LOCAL: False}, {CONF_WIFI: AP_ONLY}, False),
({CONF_VERSION: 1}, {CONF_WIFI: AP_ONLY}, False),
],
)
def test_serve_captive(
web_server_config: dict,
wifi_config: dict | None,
has_captive_portal: bool,
expected: bool,
web_server_config: dict, full_config: dict, expected: bool
) -> None:
assert serve_captive(web_server_config, wifi_config, has_captive_portal) is expected
assert serve_captive(web_server_config, full_config) is expected
def test_final_validate_ap_mode_warns_for_hosted_page(
caplog: pytest.LogCaptureFixture,
) -> None:
"""AP only with an explicit local: false gets a warning; AP only default does not."""
for web_server_config, expect_warning in (
@pytest.mark.parametrize(
("web_server_config", "expect_warning"),
[
# Explicit local: false on an AP only device: the hosted page will stay blank.
({CONF_VERSION: 2, CONF_LOCAL: False}, True),
# Default: embedded and captive, nothing to warn about.
({CONF_VERSION: 2}, False),
):
caplog.clear()
token = fv.full_config.set(
{"web_server": web_server_config, CONF_WIFI: AP_ONLY}
)
try:
with caplog.at_level(logging.WARNING):
_final_validate_ap_mode(web_server_config)
finally:
fv.full_config.reset(token)
assert ("stays blank" in caplog.text) is expect_warning
],
)
def test_final_validate_ap_mode_warns_for_hosted_page(
web_server_config: dict, expect_warning: bool, caplog: pytest.LogCaptureFixture
) -> None:
token = fv.full_config.set({"web_server": web_server_config, CONF_WIFI: AP_ONLY})
try:
with caplog.at_level(logging.WARNING):
_final_validate_ap_mode(web_server_config)
finally:
fv.full_config.reset(token)
assert ("stays blank" in caplog.text) is expect_warning