"""Tests for epaper_spi configuration validation.""" from collections.abc import Callable from typing import Any import pytest from esphome import config_validation as cv from esphome.components.epaper_spi.display import ( CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA, MODELS, ) from esphome.components.esp32 import ( KEY_BOARD, KEY_VARIANT, VARIANT_ESP32, VARIANT_ESP32S3, ) from esphome.const import ( CONF_BUSY_PIN, CONF_CS_PIN, CONF_DC_PIN, CONF_DIMENSIONS, CONF_HEIGHT, CONF_INIT_SEQUENCE, CONF_RESET_PIN, CONF_WIDTH, PlatformFramework, ) from esphome.types import ConfigType from tests.component_tests.types import SetCoreConfigCallable def run_schema_validation( config: ConfigType, with_final_validate: bool = False ) -> None: """Run schema validation on a configuration. Args: config: The configuration to validate with_final_validate: If True, also run final validation (requires full config setup) """ result = CONFIG_SCHEMA(config) if with_final_validate: FINAL_VALIDATE_SCHEMA(result) return result @pytest.mark.parametrize( ("config", "error_match"), [ pytest.param( "a string", "expected a dictionary", id="invalid_string_config", ), pytest.param( {"id": "display_id"}, r"required key not provided @ data\['model'\]", id="missing_model", ), pytest.param( { "id": "display_id", "model": "ssd1677", "dimensions": {"width": 200, "height": 200}, }, r"required key not provided @ data\['dc_pin'\]", id="missing_dc_pin", ), ], ) def test_basic_configuration_errors( config: str | ConfigType, error_match: str, set_core_config: SetCoreConfigCallable, ) -> None: """Test basic configuration validation errors""" set_core_config( PlatformFramework.ESP32_IDF, platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, ) with pytest.raises(cv.Invalid, match=error_match): CONFIG_SCHEMA(config) def test_all_predefined_models( set_core_config: SetCoreConfigCallable, set_component_config: Callable[[str, Any], None], ) -> None: """Test all predefined epaper models validate successfully with appropriate defaults.""" # Test all models, providing default values where necessary for name, model in MODELS.items(): # SEEED models are designed for ESP32-S3 hardware if name in ("SEEED-EE04-MONO-4.26", "SEEED-RETERMINAL-E1002"): set_core_config( PlatformFramework.ESP32_IDF, platform_data={ KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3, }, ) else: set_core_config( PlatformFramework.ESP32_IDF, platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, ) # Configure SPI component which is required by epaper_spi set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19}) config = {"model": name} # Add ID field config["id"] = "test_display" # Add required fields that don't have defaults # Use safe GPIO pins that work on ESP32 (avoiding flash pins 6-11) if not model.get_default(CONF_DC_PIN): config[CONF_DC_PIN] = 21 # Add dimensions if not provided by model if not model.get_default(CONF_WIDTH): config[CONF_DIMENSIONS] = {CONF_HEIGHT: 240, CONF_WIDTH: 320} # Add init sequence if model doesn't provide one if model.initsequence is None: config[CONF_INIT_SEQUENCE] = [[0xA0, 0x01]] # Add other optional pins that some models might require if not model.get_default(CONF_BUSY_PIN): config[CONF_BUSY_PIN] = 22 if not model.get_default(CONF_RESET_PIN): config[CONF_RESET_PIN] = 23 if not model.get_default(CONF_CS_PIN): config[CONF_CS_PIN] = 5 run_schema_validation(config) @pytest.mark.parametrize( "model_name", [pytest.param(name, id=name.lower()) for name in sorted(MODELS.keys())], ) def test_individual_models( model_name: str, set_core_config: SetCoreConfigCallable, set_component_config: Callable[[str, Any], None], ) -> None: """Test each epaper model individually to ensure it validates correctly.""" # SEEED models are designed for ESP32-S3 hardware if model_name in ("SEEED-EE04-MONO-4.26", "SEEED-RETERMINAL-E1002"): set_core_config( PlatformFramework.ESP32_IDF, platform_data={ KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3, }, ) else: set_core_config( PlatformFramework.ESP32_IDF, platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, ) # Configure SPI component which is required by epaper_spi set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19}) model = MODELS[model_name] config: dict[str, Any] = {"model": model_name, "id": "test_display"} # Add required fields based on model defaults # Use safe GPIO pins that work on ESP32 if not model.get_default(CONF_DC_PIN): config[CONF_DC_PIN] = 21 if not model.get_default(CONF_WIDTH): config[CONF_DIMENSIONS] = {CONF_HEIGHT: 240, CONF_WIDTH: 320} if model.initsequence is None: config[CONF_INIT_SEQUENCE] = [[0xA0, 0x01]] if not model.get_default(CONF_BUSY_PIN): config[CONF_BUSY_PIN] = 22 if not model.get_default(CONF_RESET_PIN): config[CONF_RESET_PIN] = 23 if not model.get_default(CONF_CS_PIN): config[CONF_CS_PIN] = 5 # This should not raise any exceptions run_schema_validation(config) def test_model_with_explicit_dimensions( set_core_config: SetCoreConfigCallable, set_component_config: Callable[[str, Any], None], ) -> None: """Test model configuration with explicitly provided dimensions.""" set_core_config( PlatformFramework.ESP32_IDF, platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, ) # Configure SPI component which is required by epaper_spi set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19}) run_schema_validation( { "id": "test_display", "model": "ssd1677", "dc_pin": 21, "busy_pin": 22, "reset_pin": 23, "cs_pin": 5, "dimensions": { "width": 200, "height": 200, }, } ) def test_model_with_transform( set_core_config: SetCoreConfigCallable, set_component_config: Callable[[str, Any], None], ) -> None: """Test model configuration with transform options.""" set_core_config( PlatformFramework.ESP32_IDF, platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, ) # Configure SPI component which is required by epaper_spi set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19}) run_schema_validation( { "id": "test_display", "model": "ssd1677", "dc_pin": 21, "busy_pin": 22, "reset_pin": 23, "cs_pin": 5, "dimensions": { "width": 200, "height": 200, }, "transform": { "mirror_x": True, "mirror_y": False, }, } ) def test_model_with_full_update_every( set_core_config: SetCoreConfigCallable, set_component_config: Callable[[str, Any], None], ) -> None: """Test model configuration with full_update_every option.""" set_core_config( PlatformFramework.ESP32_IDF, platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, ) # Configure SPI component which is required by epaper_spi set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19}) run_schema_validation( { "id": "test_display", "model": "ssd1677", "dc_pin": 21, "busy_pin": 22, "reset_pin": 23, "cs_pin": 5, "dimensions": { "width": 200, "height": 200, }, "full_update_every": 10, } ) def test_busy_pin_input_mode_ssd1677( set_core_config: SetCoreConfigCallable, set_component_config: Callable[[str, Any], None], ) -> None: """Test that busy_pin has input mode and cs/dc/reset pins have output mode for ssd1677.""" set_core_config( PlatformFramework.ESP32_IDF, platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, ) # Configure SPI component which is required by epaper_spi set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19}) result = run_schema_validation( { "id": "test_display", "model": "ssd1677", "dc_pin": 21, "busy_pin": 22, "reset_pin": 23, "cs_pin": 5, "dimensions": { "width": 200, "height": 200, }, } ) # Verify that busy_pin has input mode set assert CONF_BUSY_PIN in result busy_pin_config = result[CONF_BUSY_PIN] assert "mode" in busy_pin_config assert busy_pin_config["mode"]["input"] is True # Verify that cs_pin has output mode set assert CONF_CS_PIN in result cs_pin_config = result[CONF_CS_PIN] assert "mode" in cs_pin_config assert cs_pin_config["mode"]["output"] is True # Verify that dc_pin has output mode set assert CONF_DC_PIN in result dc_pin_config = result[CONF_DC_PIN] assert "mode" in dc_pin_config assert dc_pin_config["mode"]["output"] is True # Verify that reset_pin has output mode set assert CONF_RESET_PIN in result reset_pin_config = result[CONF_RESET_PIN] assert "mode" in reset_pin_config assert reset_pin_config["mode"]["output"] is True