From c40cbc8929e995471662a3c591674cfced534747 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:04:03 +1200 Subject: [PATCH] [core] Add schema visibility hints to build & framework internals Mark raw build-system and framework/SDK options with cv.Visibility so schema-aware editors (device-builder) render them appropriately: - YAML_ONLY (never in a visual editor): esphome build internals (build_path, platformio_options, build_flags, environment_variables, includes, includes_c, libraries, debug_scheduler) and platform framework internals (esp32 release/source/platform_version/ sdkconfig_options/advanced/components and partitions; esp8266/rp2040 source/platform_version; libretiny source; nrf52 framework advanced). - ADVANCED (advanced disclosure): min_version, compile_process_limit and the esp32 toolchain override -- real but rarely-touched knobs. Pure UI hints; validation and runtime are unchanged. --- esphome/components/esp32/__init__.py | 28 +++++++++----- esphome/components/esp8266/__init__.py | 8 +++- esphome/components/libretiny/__init__.py | 5 ++- esphome/components/nrf52/__init__.py | 4 +- esphome/components/rp2040/__init__.py | 8 +++- esphome/core/config.py | 46 +++++++++++++++++------ tests/component_tests/esp32/test_esp32.py | 34 +++++++++++++++++ tests/unit_tests/core/test_config.py | 31 +++++++++++++++ 8 files changed, 136 insertions(+), 28 deletions(-) diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index aee86a0554..ee307d6914 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -1483,16 +1483,20 @@ FRAMEWORK_SCHEMA = cv.Schema( { cv.Optional(CONF_TYPE): cv.one_of(FRAMEWORK_ESP_IDF, FRAMEWORK_ARDUINO), cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, - cv.Optional(CONF_RELEASE): cv.string_strict, - cv.Optional(CONF_SOURCE): cv.string_strict, - cv.Optional(CONF_PLATFORM_VERSION): _parse_pio_platform_version, - cv.Optional(CONF_SDKCONFIG_OPTIONS, default={}): { - cv.string_strict: cv.string_strict - }, + cv.Optional(CONF_RELEASE, visibility=cv.Visibility.YAML_ONLY): cv.string_strict, + cv.Optional(CONF_SOURCE, visibility=cv.Visibility.YAML_ONLY): cv.string_strict, + cv.Optional( + CONF_PLATFORM_VERSION, visibility=cv.Visibility.YAML_ONLY + ): _parse_pio_platform_version, + cv.Optional( + CONF_SDKCONFIG_OPTIONS, default={}, visibility=cv.Visibility.YAML_ONLY + ): {cv.string_strict: cv.string_strict}, cv.Optional(CONF_LOG_LEVEL, default="ERROR"): cv.one_of( *LOG_LEVELS_IDF, upper=True ), - cv.Optional(CONF_ADVANCED, default={}): cv.Schema( + cv.Optional( + CONF_ADVANCED, default={}, visibility=cv.Visibility.YAML_ONLY + ): cv.Schema( { cv.Optional(CONF_ASSERTION_LEVEL): cv.one_of( *ASSERTION_LEVELS, upper=True @@ -1570,7 +1574,9 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional(CONF_DISABLE_FATFS, default=True): cv.boolean, } ), - cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( + cv.Optional( + CONF_COMPONENTS, default=[], visibility=cv.Visibility.YAML_ONLY + ): cv.ensure_list( cv.All( cv.Any( cv.All(cv.string_strict, _parse_idf_component), @@ -1723,7 +1729,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_FLASH_FREQUENCY): cv.one_of( *FLASH_FREQUENCIES, upper=True ), - cv.Optional(CONF_PARTITIONS): cv.Any( + cv.Optional(CONF_PARTITIONS, visibility=cv.Visibility.YAML_ONLY): cv.Any( cv.file_, cv.ensure_list( cv.All( @@ -1747,7 +1753,9 @@ CONFIG_SCHEMA = cv.All( ), cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True), cv.Optional(CONF_FRAMEWORK): FRAMEWORK_SCHEMA, - cv.Optional(CONF_TOOLCHAIN): _validate_toolchain, + cv.Optional( + CONF_TOOLCHAIN, visibility=cv.Visibility.ADVANCED + ): _validate_toolchain, cv.Optional(CONF_WATCHDOG_TIMEOUT, default="5s"): cv.All( cv.positive_time_period_seconds, cv.Range(min=cv.TimePeriod(seconds=5), max=cv.TimePeriod(seconds=60)), diff --git a/esphome/components/esp8266/__init__.py b/esphome/components/esp8266/__init__.py index db94f0ec6d..66d56f2aad 100644 --- a/esphome/components/esp8266/__init__.py +++ b/esphome/components/esp8266/__init__.py @@ -205,8 +205,12 @@ ARDUINO_FRAMEWORK_SCHEMA = cv.All( cv.Schema( { cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, - cv.Optional(CONF_SOURCE): cv.string_strict, - cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version, + cv.Optional( + CONF_SOURCE, visibility=cv.Visibility.YAML_ONLY + ): cv.string_strict, + cv.Optional( + CONF_PLATFORM_VERSION, visibility=cv.Visibility.YAML_ONLY + ): _parse_platform_version, } ), _arduino_check_versions, diff --git a/esphome/components/libretiny/__init__.py b/esphome/components/libretiny/__init__.py index afe0360c22..89f465066d 100644 --- a/esphome/components/libretiny/__init__.py +++ b/esphome/components/libretiny/__init__.py @@ -259,7 +259,10 @@ FRAMEWORK_SCHEMA = cv.All( cv.Schema( { cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, - cv.Optional(CONF_SOURCE): cv.string_strict, + # Raw PlatformIO package source — build internal, not a UI field. + cv.Optional( + CONF_SOURCE, visibility=cv.Visibility.YAML_ONLY + ): cv.string_strict, cv.Optional(CONF_LOGLEVEL, default="warn"): ( cv.one_of(*LT_LOGLEVELS, upper=True) ), diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index d87318b03d..e15e1520a9 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -238,7 +238,9 @@ CONFIG_SCHEMA = cv.All( ): cv.Schema( { cv.Optional(CONF_VERSION): cv.string_strict, - cv.Optional(CONF_ADVANCED, default={}): cv.Schema( + cv.Optional( + CONF_ADVANCED, default={}, visibility=cv.Visibility.YAML_ONLY + ): cv.Schema( { cv.Optional( CONF_ENABLE_OTA_ROLLBACK, default=True diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index f98cde7968..9dccd822bc 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -240,8 +240,12 @@ ARDUINO_FRAMEWORK_SCHEMA = cv.All( cv.Schema( { cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict, - cv.Optional(CONF_SOURCE): cv.string_strict, - cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version, + cv.Optional( + CONF_SOURCE, visibility=cv.Visibility.YAML_ONLY + ): cv.string_strict, + cv.Optional( + CONF_PLATFORM_VERSION, visibility=cv.Visibility.YAML_ONLY + ): _parse_platform_version, } ), _arduino_check_versions, diff --git a/esphome/core/config.py b/esphome/core/config.py index b925f0b7d9..24489c82ac 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -283,14 +283,24 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_COMMENT): cv.All( cv.string, cv.ByteLength(max=COMMENT_MAX_LEN) ), - cv.Required(CONF_BUILD_PATH): cv.string, - cv.Optional(CONF_PLATFORMIO_OPTIONS, default={}): cv.Schema( + cv.Required(CONF_BUILD_PATH, visibility=cv.Visibility.YAML_ONLY): cv.string, + cv.Optional( + CONF_PLATFORMIO_OPTIONS, + default={}, + visibility=cv.Visibility.YAML_ONLY, + ): cv.Schema( { cv.string_strict: cv.Any([cv.string], cv.string), } ), - cv.Optional(CONF_BUILD_FLAGS, default=[]): cv.ensure_list(cv.string_strict), - cv.Optional(CONF_ENVIRONMENT_VARIABLES, default={}): cv.Schema( + cv.Optional( + CONF_BUILD_FLAGS, default=[], visibility=cv.Visibility.YAML_ONLY + ): cv.ensure_list(cv.string_strict), + cv.Optional( + CONF_ENVIRONMENT_VARIABLES, + default={}, + visibility=cv.Visibility.YAML_ONLY, + ): cv.Schema( { cv.string_strict: cv.string, } @@ -312,11 +322,19 @@ CONFIG_SCHEMA = cv.All( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LoopTrigger), } ), - cv.Optional(CONF_INCLUDES, default=[]): cv.ensure_list(valid_include), - cv.Optional(CONF_INCLUDES_C, default=[]): cv.ensure_list(valid_include), - cv.Optional(CONF_LIBRARIES, default=[]): cv.ensure_list(cv.string_strict), + cv.Optional( + CONF_INCLUDES, default=[], visibility=cv.Visibility.YAML_ONLY + ): cv.ensure_list(valid_include), + cv.Optional( + CONF_INCLUDES_C, default=[], visibility=cv.Visibility.YAML_ONLY + ): cv.ensure_list(valid_include), + cv.Optional( + CONF_LIBRARIES, default=[], visibility=cv.Visibility.YAML_ONLY + ): cv.ensure_list(cv.string_strict), cv.Optional(CONF_NAME_ADD_MAC_SUFFIX, default=False): cv.boolean, - cv.Optional(CONF_DEBUG_SCHEDULER, default=False): cv.boolean, + cv.Optional( + CONF_DEBUG_SCHEDULER, default=False, visibility=cv.Visibility.YAML_ONLY + ): cv.boolean, cv.Optional(CONF_PROJECT): cv.Schema( { cv.Required(CONF_NAME): cv.All( @@ -336,11 +354,15 @@ CONFIG_SCHEMA = cv.All( ), } ), - cv.Optional(CONF_MIN_VERSION, default=ESPHOME_VERSION): cv.All( - cv.version_number, cv.validate_esphome_version - ), cv.Optional( - CONF_COMPILE_PROCESS_LIMIT, default=_compile_process_limit_default + CONF_MIN_VERSION, + default=ESPHOME_VERSION, + visibility=cv.Visibility.ADVANCED, + ): cv.All(cv.version_number, cv.validate_esphome_version), + cv.Optional( + CONF_COMPILE_PROCESS_LIMIT, + default=_compile_process_limit_default, + visibility=cv.Visibility.ADVANCED, ): cv.int_range(min=1, max=get_usable_cpu_count()), cv.Optional(CONF_AREAS, default=[]): cv.ensure_list(AREA_SCHEMA), cv.Optional(CONF_DEVICES, default=[]): cv.ensure_list(DEVICE_SCHEMA), diff --git a/tests/component_tests/esp32/test_esp32.py b/tests/component_tests/esp32/test_esp32.py index bdba981c44..5623952dc4 100644 --- a/tests/component_tests/esp32/test_esp32.py +++ b/tests/component_tests/esp32/test_esp32.py @@ -534,3 +534,37 @@ def test_network_wifi_ble_coexistence_reconciles_end_to_end( assert sdkconfig.get("CONFIG_LWIP_DHCPS") is False # WiFi present alongside BT -> WiFi stack must stay enabled. assert "CONFIG_ESP_WIFI_ENABLED" not in sdkconfig + + +def test_esp32_build_internals_are_yaml_only() -> None: + """ESP32 raw framework / build inputs are ``YAML_ONLY``. + + The framework block's PlatformIO package pins, raw ESP-IDF + sdkconfig options, the low-level ``advanced`` block, extra IDF + component sources, plus the partition table and toolchain override + on the main schema are build internals — never UI form fields. + User-facing choices (framework type/version, board, variant, …) + stay on the main form. + """ + from esphome.components.esp32 import CONFIG_SCHEMA, FRAMEWORK_SCHEMA + + fw_markers = {str(k): k for k in FRAMEWORK_SCHEMA.schema} + for field in ( + "release", + "source", + "platform_version", + "sdkconfig_options", + "advanced", + "components", + ): + assert fw_markers[field].visibility is cv.Visibility.YAML_ONLY, field + # Framework type/version remain user-facing. + assert fw_markers["type"].visibility is None + assert fw_markers["version"].visibility is None + + main_markers = {str(k): k for k in CONFIG_SCHEMA.validators[0].schema} + assert main_markers["partitions"].visibility is cv.Visibility.YAML_ONLY + # toolchain is a real but rarely-touched override -> advanced disclosure. + assert main_markers["toolchain"].visibility is cv.Visibility.ADVANCED + assert main_markers["board"].visibility is None + assert main_markers["flash_size"].visibility is None diff --git a/tests/unit_tests/core/test_config.py b/tests/unit_tests/core/test_config.py index e2b34d92d8..30f47ea7ca 100644 --- a/tests/unit_tests/core/test_config.py +++ b/tests/unit_tests/core/test_config.py @@ -1284,3 +1284,34 @@ async def test_to_code_adds_libraries(yaml_file: Callable[[str], Path]) -> None: mock_cg.add_library.assert_any_call( "noise-c", None, "https://github.com/esphome/noise-c.git" ) + + +def test_esphome_build_internals_are_yaml_only() -> None: + """Raw build-system inputs in the ``esphome:`` block are ``YAML_ONLY``. + + These knobs (compiler flags, raw PlatformIO options, C/C++ includes, + libraries, build host parallelism, the min-version gate, …) are not + meaningful as visual-editor form fields and a wrong value breaks the + build, so they must never render in a schema-aware UI. + """ + # CONFIG_SCHEMA is cv.All(cv.Schema({...}), validate_hostname). + inner = config.CONFIG_SCHEMA.validators[0].schema + markers = {str(k): k for k in inner} + yaml_only_fields = { + CONF_BUILD_PATH, + "platformio_options", + "build_flags", + "environment_variables", + "includes", + "includes_c", + "libraries", + "debug_scheduler", + } + for field in yaml_only_fields: + assert markers[field].visibility is cv.Visibility.YAML_ONLY, field + # Packaging / build-host knobs are real but rarely-touched overrides: + # surface them under the editor's advanced disclosure, not yaml-only. + for field in ("min_version", "compile_process_limit"): + assert markers[field].visibility is cv.Visibility.ADVANCED, field + # A regular device-config field stays on the main form. + assert markers[CONF_NAME_ADD_MAC_SUFFIX].visibility is None