Files
esphome/tests/component_tests/epaper_spi/test_init.py
2026-06-15 12:10:58 +10:00

457 lines
14 KiB
Python

"""Tests for epaper_spi configuration validation."""
from collections.abc import Callable
from pathlib import Path
import re
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
from esphome.const import (
CONF_BUSY_PIN,
CONF_CS_PIN,
CONF_DC_PIN,
CONF_DIMENSIONS,
CONF_ENABLE_PIN,
CONF_HEIGHT,
CONF_INIT_SEQUENCE,
CONF_RESET_PIN,
CONF_WIDTH,
PlatformFramework,
)
from esphome.types import ConfigType
from tests.component_tests.types import SetCoreConfigCallable
# Pin options whose values must be valid on the chosen ESP32 variant.
_PIN_CONF_KEYS = (
CONF_CS_PIN,
CONF_DC_PIN,
CONF_RESET_PIN,
CONF_BUSY_PIN,
CONF_ENABLE_PIN,
)
def _pins_for(model: Any, config: ConfigType) -> list:
"""Collect every GPIO the config will actually use (model defaults or injected)."""
pins: list = []
for key in _PIN_CONF_KEYS:
# An injected value in the config takes precedence over the model default.
value = config[key] if key in config else model.get_default(key)
if not value: # get_default returns False for pins the model omits
continue
if isinstance(value, list):
pins.extend(value)
else:
pins.append(value)
return pins
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],
choose_variant_with_pins: Callable[[list], None],
) -> None:
"""Test all predefined epaper models validate successfully with appropriate defaults."""
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})
# Test all models, providing default values where necessary
for name, model in MODELS.items():
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
# Select an ESP32 variant on which all of this model's pins are valid
# (some models default to high-numbered pins only present on the S3).
choose_variant_with_pins(_pins_for(model, config))
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],
choose_variant_with_pins: Callable[[list], None],
) -> None:
"""Test each epaper model individually to ensure it validates correctly."""
model = MODELS[model_name]
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: 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
# Select an ESP32 variant on which all of this model's pins are valid
# (some models default to high-numbered pins only present on the S3).
choose_variant_with_pins(_pins_for(model, config))
# 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
def test_enable_pin_single(
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
) -> None:
"""Test that a single enable_pin is accepted and normalised to a list of output pins."""
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,
"enable_pin": 25,
"dimensions": {
"width": 200,
"height": 200,
},
}
)
# A single pin is normalised to a list by cv.ensure_list
assert CONF_ENABLE_PIN in result
enable_pins = result[CONF_ENABLE_PIN]
assert isinstance(enable_pins, list)
assert len(enable_pins) == 1
# enable pins are configured as outputs
assert enable_pins[0]["mode"]["output"] is True
def test_enable_pin_multiple(
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
) -> None:
"""Test that a list of enable_pins is accepted."""
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,
"enable_pin": [25, 26],
"dimensions": {
"width": 200,
"height": 200,
},
}
)
assert CONF_ENABLE_PIN in result
enable_pins = result[CONF_ENABLE_PIN]
assert isinstance(enable_pins, list)
assert len(enable_pins) == 2
assert all(pin["mode"]["output"] is True for pin in enable_pins)
def test_enable_pin_code_generation(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
"""Test that enable_pins are wired up in the generated C++ code."""
main_cpp = generate_main(component_config_path("enable_pin_test.yaml"))
# Derive the auto-generated pin variable names from the set_pin() lines
# rather than hard-coding them, so the test does not break when unrelated
# codegen details shift the generated IDs.
def pin_var_for(gpio_num: int) -> str:
match = re.search(rf"(\w+)->set_pin\(::GPIO_NUM_{gpio_num}\);", main_cpp)
assert match is not None, (
f"GPIO_NUM_{gpio_num} pin not set up in generated code"
)
return match.group(1)
pin_25 = pin_var_for(25)
pin_26 = pin_var_for(26)
# Both pin objects must be passed to the display via set_enable_pins() as a
# std::vector initializer list, in the configured order.
assert f"set_enable_pins({{{pin_25}, {pin_26}}});" in main_cpp