diff --git a/esphome/components/debug/sensor.py b/esphome/components/debug/sensor.py index 4669095d5d..4484f15935 100644 --- a/esphome/components/debug/sensor.py +++ b/esphome/components/debug/sensor.py @@ -1,6 +1,7 @@ import esphome.codegen as cg from esphome.components import sensor from esphome.components.esp32 import CONF_CPU_FREQUENCY +from esphome.components.psram import DOMAIN as PSRAM_DOMAIN import esphome.config_validation as cv from esphome.const import ( CONF_BLOCK, @@ -54,7 +55,7 @@ CONFIG_SCHEMA = { ), cv.Optional(CONF_PSRAM): cv.All( cv.only_on_esp32, - cv.requires_component("psram"), + cv.requires_component(PSRAM_DOMAIN), sensor.sensor_schema( unit_of_measurement=UNIT_BYTES, icon=ICON_COUNTER, diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index 8d72ff3685..05a79553a4 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -76,6 +76,7 @@ CONF_ASSERTION_LEVEL = "assertion_level" CONF_COMPILER_OPTIMIZATION = "compiler_optimization" CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features" CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert" +CONF_EXECUTE_FROM_PSRAM = "execute_from_psram" CONF_RELEASE = "release" ASSERTION_LEVELS = { @@ -519,32 +520,59 @@ def _detect_variant(value): def final_validate(config): - if not ( - pio_options := fv.full_config.get()[CONF_ESPHOME].get(CONF_PLATFORMIO_OPTIONS) - ): - # Not specified or empty - return config - - pio_flash_size_key = "board_upload.flash_size" - pio_partitions_key = "board_build.partitions" - if CONF_PARTITIONS in config and pio_partitions_key in pio_options: - raise cv.Invalid( - f"Do not specify '{pio_partitions_key}' in '{CONF_PLATFORMIO_OPTIONS}' with '{CONF_PARTITIONS}' in esp32" - ) - - if pio_flash_size_key in pio_options: - raise cv.Invalid( - f"Please specify {CONF_FLASH_SIZE} within esp32 configuration only" - ) + # Imported locally to avoid circular import issues + from esphome.components.psram import DOMAIN as PSRAM_DOMAIN + errs = [] + full_config = fv.full_config.get() + if pio_options := full_config[CONF_ESPHOME].get(CONF_PLATFORMIO_OPTIONS): + pio_flash_size_key = "board_upload.flash_size" + pio_partitions_key = "board_build.partitions" + if CONF_PARTITIONS in config and pio_partitions_key in pio_options: + errs.append( + cv.Invalid( + f"Do not specify '{pio_partitions_key}' in '{CONF_PLATFORMIO_OPTIONS}' with '{CONF_PARTITIONS}' in esp32" + ) + ) + if pio_flash_size_key in pio_options: + errs.append( + cv.Invalid( + f"Please specify {CONF_FLASH_SIZE} within esp32 configuration only" + ) + ) if ( config[CONF_VARIANT] != VARIANT_ESP32 and CONF_ADVANCED in (conf_fw := config[CONF_FRAMEWORK]) and CONF_IGNORE_EFUSE_MAC_CRC in conf_fw[CONF_ADVANCED] ): - raise cv.Invalid( - f"{CONF_IGNORE_EFUSE_MAC_CRC} is not supported on {config[CONF_VARIANT]}" + errs.append( + cv.Invalid( + f"'{CONF_IGNORE_EFUSE_MAC_CRC}' is not supported on {config[CONF_VARIANT]}", + path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_IGNORE_EFUSE_MAC_CRC], + ) ) + if ( + config.get(CONF_FRAMEWORK, {}) + .get(CONF_ADVANCED, {}) + .get(CONF_EXECUTE_FROM_PSRAM) + ): + if config[CONF_VARIANT] != VARIANT_ESP32S3: + errs.append( + cv.Invalid( + f"'{CONF_EXECUTE_FROM_PSRAM}' is only supported on {VARIANT_ESP32S3} variant", + path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_EXECUTE_FROM_PSRAM], + ) + ) + if PSRAM_DOMAIN not in full_config: + errs.append( + cv.Invalid( + f"'{CONF_EXECUTE_FROM_PSRAM}' requires PSRAM to be configured", + path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_EXECUTE_FROM_PSRAM], + ) + ) + + if errs: + raise cv.MultipleInvalid(errs) return config @@ -627,6 +655,7 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All( cv.Optional( CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, default=True ): cv.boolean, + cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean, } ), cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( @@ -792,6 +821,9 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False) if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False): add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0) + if advanced.get(CONF_EXECUTE_FROM_PSRAM, False): + add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True) + add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True) # Apply LWIP core locking for better socket performance # This is already enabled by default in Arduino framework, where it provides diff --git a/esphome/components/lvgl/__init__.py b/esphome/components/lvgl/__init__.py index 0cd65d298f..a37f4570f3 100644 --- a/esphome/components/lvgl/__init__.py +++ b/esphome/components/lvgl/__init__.py @@ -4,6 +4,7 @@ from esphome.automation import build_automation, register_action, validate_autom import esphome.codegen as cg from esphome.components.const import CONF_COLOR_DEPTH, CONF_DRAW_ROUNDING from esphome.components.display import Display +from esphome.components.psram import DOMAIN as PSRAM_DOMAIN import esphome.config_validation as cv from esphome.const import ( CONF_AUTO_CLEAR_ENABLED, @@ -219,7 +220,7 @@ def final_validation(configs): draw_rounding, config[CONF_DRAW_ROUNDING] ) buffer_frac = config[CONF_BUFFER_SIZE] - if CORE.is_esp32 and buffer_frac > 0.5 and "psram" not in global_config: + if CORE.is_esp32 and buffer_frac > 0.5 and PSRAM_DOMAIN not in global_config: LOGGER.warning("buffer_size: may need to be reduced without PSRAM") for image_id in lv_images_used: path = global_config.get_path_for_id(image_id)[:-1] diff --git a/esphome/components/mipi_spi/display.py b/esphome/components/mipi_spi/display.py index cb2de6c3d7..e891e2daad 100644 --- a/esphome/components/mipi_spi/display.py +++ b/esphome/components/mipi_spi/display.py @@ -25,6 +25,7 @@ from esphome.components.mipi import ( power_of_two, requires_buffer, ) +from esphome.components.psram import DOMAIN as PSRAM_DOMAIN from esphome.components.spi import TYPE_OCTAL, TYPE_QUAD, TYPE_SINGLE import esphome.config_validation as cv from esphome.config_validation import ALLOW_EXTRA @@ -292,7 +293,7 @@ def _final_validate(config): # If no drawing methods are configured, and LVGL is not enabled, show a test card config[CONF_SHOW_TEST_CARD] = True - if "psram" not in global_config and CONF_BUFFER_SIZE not in config: + if PSRAM_DOMAIN not in global_config and CONF_BUFFER_SIZE not in config: if not requires_buffer(config): return config # No buffer needed, so no need to set a buffer size # If PSRAM is not enabled, choose a small buffer size by default diff --git a/esphome/components/psram/__init__.py b/esphome/components/psram/__init__.py index 9299cdcd0e..fd7e70a055 100644 --- a/esphome/components/psram/__init__.py +++ b/esphome/components/psram/__init__.py @@ -28,12 +28,13 @@ from esphome.core import CORE import esphome.final_validate as fv CODEOWNERS = ["@esphome/core"] +DOMAIN = "psram" DEPENDENCIES = [PLATFORM_ESP32] _LOGGER = logging.getLogger(__name__) -psram_ns = cg.esphome_ns.namespace("psram") +psram_ns = cg.esphome_ns.namespace(DOMAIN) PsramComponent = psram_ns.class_("PsramComponent", cg.Component) TYPE_QUAD = "quad" diff --git a/esphome/config.py b/esphome/config.py index 670cbe7233..cf7a232d8e 100644 --- a/esphome/config.py +++ b/esphome/config.py @@ -329,6 +329,28 @@ class ConfigValidationStep(abc.ABC): def run(self, result: Config) -> None: ... # noqa: E704 +class LoadTargetPlatformValidationStep(ConfigValidationStep): + """Load target platform step.""" + + def __init__(self, domain: str, conf: ConfigType): + self.domain = domain + self.conf = conf + + def run(self, result: Config) -> None: + if self.conf is None: + result[self.domain] = self.conf = {} + result.add_output_path([self.domain], self.domain) + component = get_component(self.domain) + + result[self.domain] = self.conf + path = [self.domain] + CORE.loaded_integrations.add(self.domain) + + result.add_validation_step( + SchemaValidationStep(self.domain, path, self.conf, component) + ) + + class LoadValidationStep(ConfigValidationStep): """Load step, this step is called once for each domain config fragment. @@ -582,16 +604,18 @@ class MetadataValidationStep(ConfigValidationStep): ) return for i, part_conf in enumerate(self.conf): + path = self.path + [i] result.add_validation_step( - SchemaValidationStep( - self.domain, self.path + [i], part_conf, self.comp - ) + SchemaValidationStep(self.domain, path, part_conf, self.comp) ) + result.add_validation_step(FinalValidateValidationStep(path, self.comp)) + return result.add_validation_step( SchemaValidationStep(self.domain, self.path, self.conf, self.comp) ) + result.add_validation_step(FinalValidateValidationStep(self.path, self.comp)) class SchemaValidationStep(ConfigValidationStep): @@ -628,7 +652,6 @@ class SchemaValidationStep(ConfigValidationStep): result.set_by_path(self.path, validated) path_context.reset(token) - result.add_validation_step(FinalValidateValidationStep(self.path, self.comp)) class IDPassValidationStep(ConfigValidationStep): @@ -909,7 +932,7 @@ def validate_config( # First run platform validation steps result.add_validation_step( - LoadValidationStep(target_platform, config[target_platform]) + LoadTargetPlatformValidationStep(target_platform, config[target_platform]) ) result.run_validation_steps() diff --git a/tests/component_tests/conftest.py b/tests/component_tests/conftest.py index b269e23cd6..2045b03502 100644 --- a/tests/component_tests/conftest.py +++ b/tests/component_tests/conftest.py @@ -65,6 +65,7 @@ def set_core_config() -> Generator[SetCoreConfigCallable]: *, core_data: ConfigType | None = None, platform_data: ConfigType | None = None, + full_config: dict[str, ConfigType] | None = None, ) -> None: platform, framework = platform_framework.value @@ -83,7 +84,7 @@ def set_core_config() -> Generator[SetCoreConfigCallable]: CORE.data[platform.value] = platform_data config.path_context.set([]) - final_validate.full_config.set(Config()) + final_validate.full_config.set(full_config or Config()) yield setter diff --git a/tests/component_tests/esp32/test_esp32.py b/tests/component_tests/esp32/test_esp32.py index fe031c653f..91e96f24d6 100644 --- a/tests/component_tests/esp32/test_esp32.py +++ b/tests/component_tests/esp32/test_esp32.py @@ -8,10 +8,13 @@ import pytest from esphome.components.esp32 import VARIANTS import esphome.config_validation as cv -from esphome.const import PlatformFramework +from esphome.const import CONF_ESPHOME, PlatformFramework +from tests.component_tests.types import SetCoreConfigCallable -def test_esp32_config(set_core_config) -> None: +def test_esp32_config( + set_core_config: SetCoreConfigCallable, +) -> None: set_core_config(PlatformFramework.ESP32_IDF) from esphome.components.esp32 import CONFIG_SCHEMA @@ -60,14 +63,49 @@ def test_esp32_config(set_core_config) -> None: r"Option 'variant' does not match selected board. @ data\['variant'\]", id="mismatched_board_variant_config", ), + pytest.param( + { + "variant": "esp32s2", + "framework": { + "type": "esp-idf", + "advanced": {"execute_from_psram": True}, + }, + }, + r"'execute_from_psram' is only supported on ESP32S3 variant @ data\['framework'\]\['advanced'\]\['execute_from_psram'\]", + id="execute_from_psram_invalid_for_variant_config", + ), + pytest.param( + { + "variant": "esp32s3", + "framework": { + "type": "esp-idf", + "advanced": {"execute_from_psram": True}, + }, + }, + r"'execute_from_psram' requires PSRAM to be configured @ data\['framework'\]\['advanced'\]\['execute_from_psram'\]", + id="execute_from_psram_requires_psram_config", + ), + pytest.param( + { + "variant": "esp32s3", + "framework": { + "type": "esp-idf", + "advanced": {"ignore_efuse_mac_crc": True}, + }, + }, + r"'ignore_efuse_mac_crc' is not supported on ESP32S3 @ data\['framework'\]\['advanced'\]\['ignore_efuse_mac_crc'\]", + id="ignore_efuse_mac_crc_only_on_esp32", + ), ], ) def test_esp32_configuration_errors( config: Any, error_match: str, + set_core_config: SetCoreConfigCallable, ) -> None: + set_core_config(PlatformFramework.ESP32_IDF, full_config={CONF_ESPHOME: {}}) """Test detection of invalid configuration.""" - from esphome.components.esp32 import CONFIG_SCHEMA + from esphome.components.esp32 import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA with pytest.raises(cv.Invalid, match=error_match): - CONFIG_SCHEMA(config) + FINAL_VALIDATE_SCHEMA(CONFIG_SCHEMA(config)) diff --git a/tests/component_tests/types.py b/tests/component_tests/types.py index 72b8be4503..ee9d317339 100644 --- a/tests/component_tests/types.py +++ b/tests/component_tests/types.py @@ -18,4 +18,5 @@ class SetCoreConfigCallable(Protocol): *, core_data: ConfigType | None = None, platform_data: ConfigType | None = None, + full_config: dict[str, ConfigType] | None = None, ) -> None: ... diff --git a/tests/components/esp32/test.esp32-s3-idf.yaml b/tests/components/esp32/test.esp32-s3-idf.yaml new file mode 100644 index 0000000000..1d5a5e52a4 --- /dev/null +++ b/tests/components/esp32/test.esp32-s3-idf.yaml @@ -0,0 +1,12 @@ +esp32: + variant: esp32s3 + framework: + type: esp-idf + advanced: + execute_from_psram: true + +psram: + mode: octal + speed: 80MHz + +logger: