Warn about a non default port on a fallback AP with local: true as well

This commit is contained in:
J. Nick Koston
2026-10-06 21:16:35 -10:00
parent 8f244cb4f6
commit 0d76cdffa9
3 changed files with 49 additions and 20 deletions
+22 -15
View File
@@ -408,9 +408,28 @@ def _final_validate_ap_mode(config: ConfigType) -> None:
"web_server will act as a captive portal while the %saccess point is active.",
"" if ap_only else "fallback ",
)
if not ap_only:
return
if not local:
# An embedded interface on an AP (AP only, or a fallback with local: true) that is not
# on port 80 cannot be a captive portal; say so, unless captive_portal has that role.
if (
local
and config[CONF_PORT] != 80
and wifi_config is not None
and CONF_AP in wifi_config
and "captive_portal" not in full_config
):
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(
"web_server uses port %d, so the interface cannot open automatically on the "
"%saccess point (captive portal detection only works on port 80); open "
"http://%s:%d/ manually, or remove 'port:' to use 80.",
config[CONF_PORT],
"" if ap_only else "fallback ",
ap_ip,
config[CONF_PORT],
)
if ap_only and 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 "
@@ -419,18 +438,6 @@ def _final_validate_ap_mode(config: ConfigType) -> None:
if config.get(CONF_LOCAL) is False
else "Migrate to version 2 or 3",
)
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://%s:%d/ manually, or remove 'port:' to use 80.",
config[CONF_PORT],
ap_ip,
config[CONF_PORT],
)
def _final_validate(config: ConfigType) -> None:
@@ -20,7 +20,7 @@ namespace esphome::web_server_base {
// The server object only exists while running, so an idle owner (AP not up) pays one pointer.
class CaptiveDNS {
public:
/// Returns false when the DNS server could not start; nothing is kept, so a later start retries.
/// Returns false when the DNS server could not start; nothing is kept, so is_running() stays false.
bool start(const network::IPAddress &ip) {
if (this->dns_server_ != nullptr)
return true;
+26 -4
View File
@@ -95,18 +95,40 @@ def test_final_validate_ap_mode_warns_for_hosted_page(
assert ("stays blank" in caplog.text) is expect_warning
@pytest.mark.parametrize(
("web_server_config", "wifi_config", "expected"),
[
# AP only: local is implied, so the port alone blocks captive mode.
({CONF_VERSION: 2, CONF_PORT: 8080}, AP_ONLY, "on the access point"),
# Fallback AP with local: true opted into captive mode; say why it is skipped.
(
{CONF_VERSION: 2, CONF_PORT: 8080, CONF_LOCAL: True},
AP_FALLBACK,
"on the fallback access point",
),
# Fallback AP without local: hosted page, nothing was promised.
({CONF_VERSION: 2, CONF_PORT: 8080}, AP_FALLBACK, None),
],
)
def test_final_validate_ap_mode_warns_for_non_default_port(
web_server_config: dict,
wifi_config: dict,
expected: str | None,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Captive portal detection needs port 80; other ports get a hint, not captive mode."""
config = {CONF_VERSION: 2, CONF_PORT: 8080}
token = fv.full_config.set({"web_server": config, CONF_WIFI: AP_ONLY})
token = fv.full_config.set(
{"web_server": web_server_config, CONF_WIFI: wifi_config}
)
try:
with caplog.at_level(logging.WARNING):
_final_validate_ap_mode(config)
_final_validate_ap_mode(web_server_config)
finally:
fv.full_config.reset(token)
assert "cannot open automatically" in caplog.text
if expected is None:
assert "cannot open automatically" not in caplog.text
return
assert expected in caplog.text
assert "http://192.168.4.1:8080/" in caplog.text