mirror of
https://github.com/esphome/esphome.git
synced 2026-06-24 14:16:47 +00:00
457 lines
14 KiB
Python
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
|