mirror of
https://github.com/esphome/esphome.git
synced 2026-06-24 15:46:54 +00:00
[wifi] Refuse to compile when wifi_ssid is the device-builder placeholder (#16444)
This commit is contained in:
committed by
Jesse Hills
parent
25dbef83de
commit
50495c7085
@@ -50,6 +50,7 @@ from esphome.const import (
|
||||
CONF_TOPIC,
|
||||
CONF_USERNAME,
|
||||
CONF_WEB_SERVER,
|
||||
CONF_WIFI,
|
||||
ENV_NOGITIGNORE,
|
||||
KEY_CORE,
|
||||
KEY_TARGET_PLATFORM,
|
||||
@@ -733,6 +734,13 @@ def write_cpp_file() -> int:
|
||||
|
||||
|
||||
def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
# Keep this gate here, NOT in config validation: device-builder needs
|
||||
# `esphome config` to keep succeeding with placeholders so onboarding can run.
|
||||
if CONF_WIFI in config:
|
||||
from esphome.components.wifi import check_placeholder_credentials
|
||||
|
||||
check_placeholder_credentials(config)
|
||||
|
||||
# NOTE: "Build path:" format is parsed by script/ci_memory_impact_extract.py
|
||||
# If you change this format, update the regex in that script as well
|
||||
_LOGGER.info("Compiling app... Build path: %s", CORE.build_path)
|
||||
|
||||
@@ -54,10 +54,18 @@ from esphome.const import (
|
||||
CONF_TTLS_PHASE_2,
|
||||
CONF_USE_ADDRESS,
|
||||
CONF_USERNAME,
|
||||
CONF_WIFI,
|
||||
PLACEHOLDER_WIFI_SSID,
|
||||
Platform,
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, HexInt, coroutine_with_priority
|
||||
from esphome.core import (
|
||||
CORE,
|
||||
CoroPriority,
|
||||
EsphomeError,
|
||||
HexInt,
|
||||
coroutine_with_priority,
|
||||
)
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
@@ -903,3 +911,45 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
|
||||
"wifi_component_pico_w.cpp": {PlatformFramework.RP2040_ARDUINO},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _placeholder_wifi_credentials(config: ConfigType) -> list[str]:
|
||||
"""Return human-readable locations where the dashboard's placeholder wifi
|
||||
values still appear. Empty list means no placeholders were found.
|
||||
"""
|
||||
placeholders: list[str] = []
|
||||
wifi_conf = config.get(CONF_WIFI)
|
||||
if not wifi_conf:
|
||||
return placeholders
|
||||
|
||||
for idx, network in enumerate(wifi_conf.get(CONF_NETWORKS, [])):
|
||||
ssid = network.get(CONF_SSID)
|
||||
if isinstance(ssid, str) and ssid == PLACEHOLDER_WIFI_SSID:
|
||||
placeholders.append(f"wifi.networks[{idx}].ssid")
|
||||
|
||||
ap_conf = wifi_conf.get(CONF_AP)
|
||||
if ap_conf:
|
||||
ap_ssid = ap_conf.get(CONF_SSID)
|
||||
if isinstance(ap_ssid, str) and ap_ssid == PLACEHOLDER_WIFI_SSID:
|
||||
placeholders.append("wifi.ap.ssid")
|
||||
|
||||
return placeholders
|
||||
|
||||
|
||||
def check_placeholder_credentials(config: ConfigType) -> None:
|
||||
"""Raise EsphomeError if any wifi credential is the dashboard placeholder.
|
||||
|
||||
Call only at compile time. NEVER from CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA,
|
||||
or any path reached by `esphome config`; device-builder relies on
|
||||
validation passing with the placeholders still in place.
|
||||
"""
|
||||
locations = _placeholder_wifi_credentials(config)
|
||||
if not locations:
|
||||
return
|
||||
formatted = ", ".join(locations)
|
||||
raise EsphomeError(
|
||||
f"wifi configuration still contains the dashboard placeholder value "
|
||||
f"'{PLACEHOLDER_WIFI_SSID}' at: {formatted}. "
|
||||
f"Open secrets.yaml and replace 'wifi_ssid' (and 'wifi_password') "
|
||||
f"with your real wifi credentials before flashing."
|
||||
)
|
||||
|
||||
@@ -1415,3 +1415,12 @@ ENTITY_CATEGORY_DIAGNOSTIC = "diagnostic"
|
||||
# The corresponding constant exists in c++
|
||||
# when update_interval is set to never, it becomes SCHEDULER_DONT_RUN milliseconds
|
||||
SCHEDULER_DONT_RUN = 4294967295
|
||||
|
||||
# Sentinel values written by the esphome-device-builder dashboard into
|
||||
# secrets.yaml on first boot so that !secret wifi_ssid / !secret wifi_password
|
||||
# references resolve cleanly through validation before the user has finished
|
||||
# the onboarding wizard. Compilation refuses if these reach the binary so that
|
||||
# a user who dismisses onboarding can't accidentally flash a device that will
|
||||
# never associate with their wifi.
|
||||
PLACEHOLDER_WIFI_SSID = "REPLACE_WITH_YOUR_WIFI_NETWORK"
|
||||
PLACEHOLDER_WIFI_PASSWORD = "REPLACE_WITH_YOUR_WIFI_PASSWORD" # noqa: S105
|
||||
|
||||
@@ -3,8 +3,20 @@
|
||||
import pytest
|
||||
|
||||
from esphome.components.esp32 import const
|
||||
from esphome.components.wifi import has_native_wifi, variant_has_wifi
|
||||
from esphome.const import Platform
|
||||
from esphome.components.wifi import (
|
||||
check_placeholder_credentials,
|
||||
has_native_wifi,
|
||||
variant_has_wifi,
|
||||
)
|
||||
from esphome.const import (
|
||||
CONF_AP,
|
||||
CONF_NETWORKS,
|
||||
CONF_SSID,
|
||||
CONF_WIFI,
|
||||
PLACEHOLDER_WIFI_SSID,
|
||||
Platform,
|
||||
)
|
||||
from esphome.core import EsphomeError, Lambda
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -123,3 +135,65 @@ def test_has_native_wifi_esp32_without_variant_assumes_wifi() -> None:
|
||||
def test_has_native_wifi_rp2040_without_board_assumes_wifi() -> None:
|
||||
"""RP2040 without a board id falls open to True (custom-board default)."""
|
||||
assert has_native_wifi(platform=Platform.RP2040) is True
|
||||
|
||||
|
||||
def _wifi_config(
|
||||
*,
|
||||
networks: list[dict] | None = None,
|
||||
ap: dict | None = None,
|
||||
) -> dict:
|
||||
"""Build a minimal config dict matching the post-validation shape."""
|
||||
wifi: dict = {}
|
||||
if networks is not None:
|
||||
wifi[CONF_NETWORKS] = networks
|
||||
if ap is not None:
|
||||
wifi[CONF_AP] = ap
|
||||
return {CONF_WIFI: wifi}
|
||||
|
||||
|
||||
def test_check_placeholder_credentials_passes_with_real_ssid() -> None:
|
||||
"""A real SSID compiles without complaint."""
|
||||
config = _wifi_config(networks=[{CONF_SSID: "home_network"}])
|
||||
assert check_placeholder_credentials(config) is None
|
||||
|
||||
|
||||
def test_check_placeholder_credentials_refuses_placeholder_ssid() -> None:
|
||||
"""The placeholder SSID is rejected with an actionable message."""
|
||||
config = _wifi_config(networks=[{CONF_SSID: PLACEHOLDER_WIFI_SSID}])
|
||||
with pytest.raises(EsphomeError) as exc_info:
|
||||
check_placeholder_credentials(config)
|
||||
message = str(exc_info.value)
|
||||
assert "wifi.networks[0].ssid" in message
|
||||
assert "secrets.yaml" in message
|
||||
|
||||
|
||||
def test_check_placeholder_credentials_refuses_placeholder_in_second_network() -> None:
|
||||
"""Index reporting picks the placeholder out of a mixed network list."""
|
||||
config = _wifi_config(
|
||||
networks=[
|
||||
{CONF_SSID: "home_network"},
|
||||
{CONF_SSID: PLACEHOLDER_WIFI_SSID},
|
||||
],
|
||||
)
|
||||
with pytest.raises(EsphomeError) as exc_info:
|
||||
check_placeholder_credentials(config)
|
||||
assert "wifi.networks[1].ssid" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_check_placeholder_credentials_refuses_placeholder_ap_ssid() -> None:
|
||||
"""An AP using the placeholder broadcast name is also refused."""
|
||||
config = _wifi_config(ap={CONF_SSID: PLACEHOLDER_WIFI_SSID})
|
||||
with pytest.raises(EsphomeError) as exc_info:
|
||||
check_placeholder_credentials(config)
|
||||
assert "wifi.ap.ssid" in str(exc_info.value)
|
||||
|
||||
|
||||
def test_check_placeholder_credentials_no_wifi_passes() -> None:
|
||||
"""Ethernet-only / wifi-less configs skip the check entirely."""
|
||||
assert check_placeholder_credentials({}) is None
|
||||
|
||||
|
||||
def test_check_placeholder_credentials_skips_template_ssid() -> None:
|
||||
"""A templated (Lambda) SSID is not a string and is skipped."""
|
||||
config = _wifi_config(networks=[{CONF_SSID: Lambda('return "x";')}])
|
||||
assert check_placeholder_credentials(config) is None
|
||||
|
||||
Reference in New Issue
Block a user