[mipi][mipi_spi][mipi_dsi][mipi_rgb] Transform cleanup (#17405)

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Clyde Stubbs
2026-07-10 22:06:40 -04:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 83aaed71e1
commit e1719cd85d
38 changed files with 630 additions and 130 deletions
+61 -12
View File
@@ -31,11 +31,16 @@ from esphome.const import (
CONF_TRANSFORM,
CONF_WIDTH,
)
from esphome.core import TimePeriod
from esphome.core import CORE, TimePeriod
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
LOGGER = cv.logging.getLogger(__name__)
CONF_TRANSFORMS = "transforms"
# All axis transforms a model may support, in the order they appear in the schema.
ALL_TRANSFORMS = (CONF_MIRROR_X, CONF_MIRROR_Y, CONF_SWAP_XY)
ColorOrder = display_ns.enum("ColorMode")
NOP = 0x00
@@ -302,7 +307,8 @@ class DriverChip:
"""
A class representing a MIPI DBI driver chip model.
The parameters supplied as defaults will be used to provide default values for the display configuration.
Setting swap_xy to cv.UNDEFINED will indicate that the model does not support swapping X and Y axes.
Pass a ``transforms`` set to restrict which axis transforms (mirror_x, mirror_y, swap_xy) the model
supports; by default all three are available.
"""
models: dict[str, Self] = {}
@@ -387,11 +393,15 @@ class DriverChip:
"""
Return the available transforms for this model.
"""
if (transforms := self.get_default(CONF_TRANSFORMS, None)) is not None:
return transforms
if self.get_default("no_transform", False):
return set()
if self.get_default(CONF_SWAP_XY) != cv.UNDEFINED:
return {CONF_MIRROR_X, CONF_MIRROR_Y, CONF_SWAP_XY}
return {CONF_MIRROR_X, CONF_MIRROR_Y}
raise ValueError(
"Setting 'swap_xy' to 'cv.UNDEFINED' is no longer supported; set 'transforms' instead"
)
def has_hardware_transform(self, config) -> bool:
"""
@@ -533,17 +543,31 @@ class DriverChip:
transform[CONF_TRANSFORM] = self.rotation_as_transform(config)
return transform
def swap_xy_schema(self):
uses_swap = self.get_default(CONF_SWAP_XY, None) != cv.UNDEFINED
def transform_schema(self):
"""
Build the schema for the ``transform`` config option of this model.
def validator(value):
if value:
raise cv.Invalid("Axis swapping not supported by this model")
return cv.boolean(value)
Each transform the model supports is a required boolean. A transform the model does not
support may be omitted or set to ``false``; setting it to ``true`` reports a clear error
naming the unsupported transform instead of a generic "extra keys not allowed".
"""
supported = self.transforms
if uses_swap:
return {cv.Required(CONF_SWAP_XY): cv.boolean}
return {cv.Optional(CONF_SWAP_XY, default=False): validator}
def unsupported(name):
def validator(value):
if cv.boolean(value):
raise cv.Invalid(f"'{name}' is not supported by this model")
return False
return validator
schema = {}
for name in ALL_TRANSFORMS:
if name in supported:
schema[cv.Required(name)] = cv.boolean
else:
schema[cv.Optional(name, default=False)] = unsupported(name)
return cv.Any(cv.Schema(schema), cv.one_of(CONF_DISABLED, lower=True))
def get_madctl(self, transform: dict, config: dict) -> int:
"""
@@ -618,6 +642,31 @@ class DriverChip:
# or the delay flag inserted where needed
return flatten_sequence(sequence)
def check_requirements(self) -> None:
"""
Raise a friendly error if any component this model requires is not configured.
This runs during schema validation (before ID references are resolved) so that a
model whose default pins live on a pin expander reports the missing expander clearly
instead of a cryptic "Couldn't find ID" from the unresolved pin reference.
"""
requirements = self.get_default("requires", set())
if not requirements:
return
# ``raw_config`` is populated before any component schema runs during a real
# validation, so presence of a required component is simply a top-level key.
# When it is absent (e.g. a unit test that invokes the schema directly) there
# is no config to check against, so skip.
global_config = CORE.raw_config
if global_config is None:
return
missing = {x for x in requirements if x not in global_config}
if missing:
reqstr = ", ".join(f"'{x}'" for x in sorted(missing))
raise cv.Invalid(
f"{self.name} requires component{'s' if len(missing) > 1 else ''} {reqstr} to be configured"
)
def requires_buffer(config) -> bool:
"""
+4 -18
View File
@@ -41,24 +41,21 @@ from esphome.const import (
CONF_AUTO_CLEAR_ENABLED,
CONF_COLOR_ORDER,
CONF_DIMENSIONS,
CONF_DISABLED,
CONF_ENABLE_PIN,
CONF_ID,
CONF_INIT_SEQUENCE,
CONF_INVERT_COLORS,
CONF_LAMBDA,
CONF_MIRROR_X,
CONF_MIRROR_Y,
CONF_MODEL,
CONF_RESET_PIN,
CONF_ROTATION,
CONF_SWAP_XY,
CONF_TRANSFORM,
CONF_WIDTH,
)
from esphome.final_validate import full_config
from . import mipi_dsi_ns, models
from .models import DsiDriverChip
# Currently only ESP32-P4 is supported, so esp_ldo and psram are required
DEPENDENCIES = ["esp32", "esp_ldo", "psram"]
@@ -73,7 +70,7 @@ ColorBitness = display.display_ns.enum("ColorBitness")
CONF_LANE_BIT_RATE = "lane_bit_rate"
CONF_LANES = "lanes"
DriverChip("CUSTOM")
DsiDriverChip("CUSTOM")
# Import all models dynamically from the models package
@@ -90,19 +87,7 @@ COLOR_DEPTHS = {
def model_schema(config):
model = MODELS[config[CONF_MODEL].upper()]
model.defaults[CONF_SWAP_XY] = cv.UNDEFINED
transform = cv.Any(
cv.Schema(
{
cv.Required(CONF_MIRROR_X): cv.boolean,
cv.Required(CONF_MIRROR_Y): cv.boolean,
cv.Optional(CONF_SWAP_XY): cv.invalid(
"Axis swapping not supported by DSI displays"
),
}
),
cv.one_of(CONF_DISABLED, lower=True),
)
transform = model.transform_schema()
# CUSTOM model will need to provide a custom init sequence
iseqconf = (
cv.Required(CONF_INIT_SEQUENCE)
@@ -172,6 +157,7 @@ def _config_schema(config):
)(config)
config = model_schema(config)(config)
model = MODELS[config[CONF_MODEL].upper()]
model.check_requirements()
width, height, _offset_width, _offset_height, _pad_width, _pad_height = (
model.get_dimensions(config)
)
@@ -0,0 +1,14 @@
from esphome.components.mipi import DriverChip
from esphome.const import CONF_SWAP_XY
class DsiDriverChip(DriverChip):
"""A driver chip for MIPI DSI displays."""
@property
def transforms(self) -> set[str]:
"""
Return the set of transformations supported by this driver chip.
DSI displays do not support axis swapping, so this method removes CONF_SWAP_XY
"""
return super().transforms - {CONF_SWAP_XY}
@@ -1,8 +1,7 @@
from esphome.components.mipi import DriverChip
import esphome.config_validation as cv
from . import DsiDriverChip
# fmt: off
DriverChip(
DsiDriverChip(
"JC1060P470",
width=1024,
height=600,
@@ -14,7 +13,6 @@ DriverChip(
vsync_front_porch=12,
pclk_frequency="54MHz",
lane_bit_rate="750Mbps",
swap_xy=cv.UNDEFINED,
color_order="RGB",
initsequence=[
(0x30, 0x00), (0xF7, 0x49, 0x61, 0x02, 0x00), (0x30, 0x01), (0x04, 0x0C), (0x05, 0x00), (0x06, 0x00),
@@ -46,7 +44,7 @@ DriverChip(
# * Horizontal Timing (hsync_pulse_width=12, hsync_back_porch=42, hsync_front_porch=42)
# * Vertical Timing (vsync_pulse_width=2, vsync_back_porch=8, vsync_front_porch=166)
# ----------------------------------------------------------------------------------------------------------------------
DriverChip(
DsiDriverChip(
"JC4880P443",
width=480,
height=800,
@@ -58,7 +56,6 @@ DriverChip(
vsync_front_porch=166,
pclk_frequency="34MHz",
lane_bit_rate="500Mbps",
swap_xy=cv.UNDEFINED,
color_order="RGB",
reset_pin=5,
initsequence=[
@@ -111,7 +108,7 @@ DriverChip(
# * Horizontal Timing (hsync_pulse_width=20, hsync_back_porch=20, hsync_front_porch=40)
# * Vertical Timing (vsync_pulse_width=4, vsync_back_porch=8, vsync_front_porch=20)
# ----------------------------------------------------------------------------------------------------------------------
DriverChip(
DsiDriverChip(
"JC8012P4A1",
width=800,
height=1280,
@@ -123,7 +120,6 @@ DriverChip(
vsync_front_porch=20,
pclk_frequency="60MHz",
lane_bit_rate="1Gbps",
swap_xy=cv.UNDEFINED,
color_order="RGB",
reset_pin=27,
initsequence=[
@@ -1,8 +1,7 @@
from esphome.components.mipi import DriverChip
import esphome.config_validation as cv
from . import DsiDriverChip
# fmt: off
DriverChip(
DsiDriverChip(
"M5STACK-TAB5",
height=1280,
width=720,
@@ -14,7 +13,6 @@ DriverChip(
vsync_front_porch=20,
pclk_frequency="60MHz",
lane_bit_rate="730Mbps",
swap_xy=cv.UNDEFINED,
color_order="RGB",
initsequence=[
(0xFF, 0x98, 0x81, 0x01), # Select Page 1
@@ -56,7 +54,7 @@ DriverChip(
],
)
DriverChip(
DsiDriverChip(
"M5STACK-TAB5-V2",
height=1280,
width=720,
@@ -68,7 +66,6 @@ DriverChip(
vsync_front_porch=220,
pclk_frequency="80MHz",
lane_bit_rate="960Mbps",
swap_xy=cv.UNDEFINED,
color_order="RGB",
initsequence=[
(0x01,),
+3 -4
View File
@@ -1,9 +1,8 @@
from esphome.components.mipi import DriverChip
import esphome.config_validation as cv
from . import DsiDriverChip
# Standalone display
# Product page: https://www.seeedstudio.com/reTerminal-D1001-p-6729.html
DriverChip(
DsiDriverChip(
"SEEED-RETERMINAL-D1001",
height=1280,
width=800,
@@ -15,10 +14,10 @@ DriverChip(
vsync_front_porch=30,
pclk_frequency="80MHz",
lane_bit_rate="1.5Gbps",
swap_xy=cv.UNDEFINED,
color_order="RGB",
enable_pin=[{"xl9535": None, "number": 0}, {"xl9535": None, "number": 7}],
reset_pin={"xl9535": None, "number": 2},
requires={"psram", "xl9535"},
initsequence=(
(0xE0, 0x00),
(0xE1, 0x93),
@@ -1,12 +1,11 @@
from esphome.components.mipi import DriverChip
import esphome.config_validation as cv
from . import DsiDriverChip
# fmt: off
# Source for parameters and initsequence:
# https://github.com/waveshareteam/Waveshare-ESP32-components/tree/master/display/lcd/esp_lcd_jd9365_10_1
# Product page: https://www.waveshare.com/wiki/ESP32-P4-Nano-StartPage
JD9365_10_1_DSI_TOUCH_A = DriverChip(
JD9365_10_1_DSI_TOUCH_A = DsiDriverChip(
"WAVESHARE-P4-NANO-10.1",
height=1280,
width=800,
@@ -18,7 +17,6 @@ JD9365_10_1_DSI_TOUCH_A = DriverChip(
vsync_front_porch=30,
pclk_frequency="80MHz",
lane_bit_rate="1.5Gbps",
swap_xy=cv.UNDEFINED,
color_order="RGB",
initsequence=[
(0xE0, 0x00), # select userpage
@@ -65,7 +63,7 @@ JD9365_10_1_DSI_TOUCH_A.extend(
# Source for parameters and initsequence:
# https://github.com/espressif/esp-iot-solution/tree/master/components/display/lcd/esp_lcd_st7703
# Product page: https://www.waveshare.com/wiki/ESP32-P4-86-Panel-ETH-2RO
DriverChip(
DsiDriverChip(
"WAVESHARE-P4-86-PANEL",
height=720,
width=720,
@@ -77,7 +75,6 @@ DriverChip(
vsync_front_porch=20,
pclk_frequency="38MHz",
lane_bit_rate="480Mbps",
swap_xy=cv.UNDEFINED,
color_order="RGB",
reset_pin=27,
initsequence=[
@@ -109,7 +106,7 @@ DriverChip(
# Source for parameters and initsequence:
# https://github.com/espressif/esp-iot-solution/tree/master/components/display/lcd/esp_lcd_ek79007
# Product page: https://www.waveshare.com/wiki/ESP32-P4-WIFI6-Touch-LCD-7B
DriverChip(
DsiDriverChip(
"WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-7B",
height=600,
width=1024,
@@ -139,7 +136,7 @@ DriverChip(
# Source for parameters and initsequence:
# https://github.com/waveshareteam/Waveshare-ESP32-components/tree/master/display/lcd/esp_lcd_jd9365
# Product page: https://www.waveshare.com/wiki/ESP32-P4-WIFI6-Touch-LCD-3.4C
JD9365_3_4_DSI_TOUCH_C = DriverChip(
JD9365_3_4_DSI_TOUCH_C = DsiDriverChip(
"WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-3.4C",
height=800,
width=800,
@@ -151,7 +148,6 @@ JD9365_3_4_DSI_TOUCH_C = DriverChip(
vsync_front_porch=24,
pclk_frequency="80MHz",
lane_bit_rate="1.5Gbps",
swap_xy=cv.UNDEFINED,
color_order="RGB",
initsequence=[
(0xE0, 0x00), # select userpage
@@ -197,7 +193,7 @@ JD9365_3_4_DSI_TOUCH_C.extend(
# Source for parameters and initsequence:
# https://github.com/waveshareteam/Waveshare-ESP32-components/tree/master/display/lcd/esp_lcd_jd9365
# Product page: https://www.waveshare.com/wiki/ESP32-P4-WIFI6-Touch-LCD-4C
JD9365_4_DSI_TOUCH_C = DriverChip(
JD9365_4_DSI_TOUCH_C = DsiDriverChip(
"WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-4C",
height=720,
width=720,
@@ -209,7 +205,6 @@ JD9365_4_DSI_TOUCH_C = DriverChip(
vsync_front_porch=24,
pclk_frequency="80MHz",
lane_bit_rate="1.5Gbps",
swap_xy=cv.UNDEFINED,
color_order="RGB",
initsequence=[
(0xE0, 0x00), # select userpage
@@ -255,7 +250,7 @@ JD9365_4_DSI_TOUCH_C.extend(
# Source for parameters and initsequence:
# https://github.com/waveshareteam/Waveshare-ESP32-components/tree/master/display/lcd/esp_lcd_jd9365
# Product page: https://www.waveshare.com/wiki/8-DSI-TOUCH-A
DriverChip(
DsiDriverChip(
"WAVESHARE-8-DSI-TOUCH-A",
height=1280,
width=800,
@@ -267,7 +262,6 @@ DriverChip(
vsync_front_porch=30,
pclk_frequency="80MHz",
lane_bit_rate="1.5Gbps",
swap_xy=cv.UNDEFINED,
color_order="RGB",
initsequence=[
(0xE0, 0x00), # select userpage
@@ -304,7 +298,7 @@ DriverChip(
# Source for parameters and initsequence:
# https://github.com/waveshareteam/Waveshare-ESP32-components/tree/master/display/lcd/esp_lcd_ili9881c
# Product page: https://www.waveshare.com/wiki/7-DSI-TOUCH-A
DriverChip(
DsiDriverChip(
"WAVESHARE-7-DSI-TOUCH-A",
height=1280,
width=720,
+12 -19
View File
@@ -18,6 +18,8 @@ from esphome.components.mipi import (
CONF_HSYNC_BACK_PORCH,
CONF_HSYNC_FRONT_PORCH,
CONF_HSYNC_PULSE_WIDTH,
CONF_PCLK_FREQUENCY,
CONF_PCLK_INVERTED,
CONF_PCLK_PIN,
CONF_PIXEL_MODE,
CONF_USE_AXIS_FLIPS,
@@ -34,9 +36,11 @@ from esphome.components.mipi import (
power_of_two,
requires_buffer,
)
from esphome.components.rpi_dpi_rgb.display import (
CONF_PCLK_FREQUENCY,
CONF_PCLK_INVERTED,
from esphome.components.spi import (
CONF_SPI_MODE,
SPI_DATA_RATE_SCHEMA,
SPI_MODE_OPTIONS,
SPIComponent,
)
import esphome.config_validation as cv
from esphome.const import (
@@ -48,7 +52,6 @@ from esphome.const import (
CONF_DATA_RATE,
CONF_DC_PIN,
CONF_DIMENSIONS,
CONF_DISABLED,
CONF_ENABLE_PIN,
CONF_GREEN,
CONF_HSYNC_PIN,
@@ -57,8 +60,6 @@ from esphome.const import (
CONF_INIT_SEQUENCE,
CONF_INVERT_COLORS,
CONF_LAMBDA,
CONF_MIRROR_X,
CONF_MIRROR_Y,
CONF_MODEL,
CONF_NUMBER,
CONF_RED,
@@ -72,10 +73,10 @@ from esphome.const import (
)
from esphome.final_validate import full_config
from ..spi import CONF_SPI_MODE, SPI_DATA_RATE_SCHEMA, SPI_MODE_OPTIONS, SPIComponent
from . import models
from .models import RgbDriverChip
DEPENDENCIES = ["esp32", "psram"]
DEPENDENCIES = ["esp32"]
mipi_rgb_ns = cg.esphome_ns.namespace("mipi_rgb")
mipi_rgb = mipi_rgb_ns.class_("MipiRgb", display.Display, cg.Component)
@@ -86,7 +87,7 @@ ColorOrder = display.display_ns.enum("ColorMode")
DATA_PIN_SCHEMA = pins.internal_gpio_output_pin_schema
DriverChip("CUSTOM")
RgbDriverChip("CUSTOM")
# Import all models dynamically from the models package
@@ -120,16 +121,7 @@ def data_pin_set(length):
def model_schema(config):
model = MODELS[config[CONF_MODEL].upper()]
transform = cv.Any(
cv.Schema(
{
cv.Required(CONF_MIRROR_X): cv.boolean,
cv.Required(CONF_MIRROR_Y): cv.boolean,
**model.swap_xy_schema(),
}
),
cv.one_of(CONF_DISABLED, lower=True),
)
transform = model.transform_schema()
# RPI model does not use an init sequence, indicates with empty list
if model.initsequence is None:
# Custom model requires an init sequence
@@ -235,6 +227,7 @@ def _config_schema(config):
only_on_variant(supported=[VARIANT_ESP32S3, VARIANT_ESP32P4]),
)(config)
model = MODELS[config[CONF_MODEL].upper()]
model.check_requirements()
width, height, _offset_width, _offset_height, _pad_width, _pad_height = (
model.get_dimensions(config)
)
@@ -0,0 +1,14 @@
from esphome.components.mipi import DriverChip
from esphome.const import CONF_SWAP_XY
class RgbDriverChip(DriverChip):
"""A driver chip for MIPI RGB displays."""
@property
def transforms(self) -> set[str]:
"""
Return the set of transformations supported by this driver chip.
RGB displays do not support axis swapping, so this method removes CONF_SWAP_XY
"""
return super().transforms - {CONF_SWAP_XY}
@@ -5,6 +5,7 @@ st7701s.extend(
width=480,
height=480,
data_rate="2MHz",
requires={"psram"},
cs_pin=39,
de_pin=18,
hsync_pin=16,
+2 -4
View File
@@ -1,5 +1,3 @@
from esphome.config_validation import UNDEFINED
from .st7701s import ST7701S
# fmt: off
@@ -8,10 +6,10 @@ ST7701S(
width=480,
height=480,
invert_colors=False,
swap_xy=UNDEFINED,
spi_mode="MODE3",
cs_pin={"xl9535": None, "number": 17},
reset_pin={"xl9535": None, "number": 5},
requires={"psram", "xl9535"},
hsync_pin=39,
vsync_pin=40,
pclk_pin=41,
@@ -57,9 +55,9 @@ t_rgb = ST7701S(
height=480,
pixel_mode="18bit",
invert_colors=False,
swap_xy=UNDEFINED,
spi_mode="MODE3",
cs_pin={"xl9535": None, "number": 3},
requires={"psram", "xl9535"},
de_pin=45,
hsync_pin=47,
vsync_pin=41,
+2 -4
View File
@@ -1,9 +1,7 @@
from esphome.components.mipi import DriverChip
from esphome.config_validation import UNDEFINED
from . import RgbDriverChip
# A driver chip for Raspberry Pi MIPI RGB displays. These require no init sequence
DriverChip(
RgbDriverChip(
"RPI",
swap_xy=UNDEFINED,
initsequence=(),
)
+8 -10
View File
@@ -1,17 +1,12 @@
from esphome.components.mipi import (
MADCTL,
MADCTL_ML,
MADCTL_XFLIP,
MODE_BGR,
DriverChip,
)
from esphome.config_validation import UNDEFINED
from esphome.components.mipi import MADCTL, MADCTL_ML, MADCTL_XFLIP, MODE_BGR
from esphome.const import CONF_COLOR_ORDER, CONF_HEIGHT, CONF_MIRROR_X, CONF_MIRROR_Y
from . import RgbDriverChip
SDIR_CMD = 0xC7
class ST7701S(DriverChip):
class ST7701S(RgbDriverChip):
# The ST7701s does not use the standard MADCTL bits for x/y mirroring
def add_madctl(self, sequence: list, config: dict):
transform = self.get_transform(config)
@@ -45,7 +40,6 @@ st7701s = ST7701S(
"ST7701S",
width=480,
height=864,
swap_xy=UNDEFINED,
hsync_front_porch=20,
hsync_back_porch=10,
hsync_pulse_width=10,
@@ -85,6 +79,7 @@ st7701s.extend(
height=480,
invert_colors=True,
pixel_mode="18bit",
requires={"psram"},
cs_pin=1,
de_pin={
"number": 45,
@@ -117,6 +112,7 @@ st7701s.extend(
vsync_pulse_width=8,
vsync_back_porch=20,
cs_pin={"pca9554": None, "number": 4},
requires={"psram", "pca9554"},
de_pin=18,
hsync_pin=16,
vsync_pin=17,
@@ -134,6 +130,7 @@ st7701s.extend(
width=480,
height=480,
pixel_mode="18bit",
requires={"psram"},
cs_pin=18,
reset_pin=8,
de_pin=17,
@@ -177,6 +174,7 @@ st7701s.extend(
width=480,
height=480,
pixel_mode="18bit",
requires={"psram"},
cs_pin=21,
de_pin=39,
vsync_pin=48,
+3 -5
View File
@@ -1,14 +1,13 @@
from esphome.components.mipi import DriverChip
from esphome.config_validation import UNDEFINED
from . import RgbDriverChip
# fmt: off
sunton = DriverChip(
sunton = RgbDriverChip(
"ESP32-8048S070",
swap_xy=UNDEFINED,
initsequence=(),
width=800,
height=480,
pclk_frequency="12.5MHz",
requires={"psram"},
de_pin=41,
hsync_pin=39,
vsync_pin=40,
@@ -28,7 +27,6 @@ sunton = DriverChip(
sunton.extend(
"ESP32-8048S050",
swap_xy=UNDEFINED,
initsequence=(),
width=800,
height=480,
@@ -1,18 +1,18 @@
from esphome.components.mipi import DriverChip, delay
from esphome.config_validation import UNDEFINED
from esphome.components.mipi import delay
from . import RgbDriverChip
from .st7701s import st7701s
# fmt: off
wave_4_3 = DriverChip(
wave_4_3 = RgbDriverChip(
"ESP32-S3-TOUCH-LCD-4.3",
swap_xy=UNDEFINED,
initsequence=(),
width=800,
height=480,
pclk_frequency="16MHz",
reset_pin={"ch422g": None, "number": 3},
enable_pin={"ch422g": None, "number": 2},
requires={"psram", "ch422g"},
de_pin=5,
hsync_pin={"number": 46, "ignore_strapping_warning": True},
vsync_pin={"number": 3, "ignore_strapping_warning": True},
@@ -69,6 +69,7 @@ st7701s.extend(
pclk_pin=41,
pclk_frequency="12MHz",
pclk_inverted=False,
requires={"psram"},
data_pins={
"red": [46, 3, 8, 18, 17],
"green": [14, 13, 12, 11, 10, 9],
@@ -80,6 +81,7 @@ st7701s.extend(
"WAVESHARE-3.16-320X820",
width=320,
height=820,
requires={"psram"},
de_pin=40,
hsync_pin=38,
vsync_pin=39,
+2 -13
View File
@@ -41,14 +41,11 @@ from esphome.const import (
CONF_DATA_RATE,
CONF_DC_PIN,
CONF_DIMENSIONS,
CONF_DISABLED,
CONF_ENABLE_PIN,
CONF_ID,
CONF_INIT_SEQUENCE,
CONF_INVERT_COLORS,
CONF_LAMBDA,
CONF_MIRROR_X,
CONF_MIRROR_Y,
CONF_MODEL,
CONF_RESET_PIN,
CONF_ROTATION,
@@ -138,16 +135,7 @@ def denominator(config):
def model_schema(config):
model = MODELS[config[CONF_MODEL]]
bus_mode = config[CONF_BUS_MODE]
transform = cv.Any(
cv.Schema(
{
cv.Required(CONF_MIRROR_X): cv.boolean,
cv.Required(CONF_MIRROR_Y): cv.boolean,
**model.swap_xy_schema(),
}
),
cv.one_of(CONF_DISABLED, lower=True),
)
transform = model.transform_schema()
# CUSTOM model will need to provide a custom init sequence
iseqconf = (
cv.Required(CONF_INIT_SEQUENCE)
@@ -265,6 +253,7 @@ def customise_schema(config):
extra=ALLOW_EXTRA,
)(config)
model = MODELS[config[CONF_MODEL]]
model.check_requirements()
bus_modes = (TYPE_SINGLE, TYPE_QUAD, TYPE_OCTAL)
config = cv.Schema(
{
@@ -13,6 +13,7 @@ ST7789V.extend(
mirror_x=True,
mirror_y=True,
data_rate="80MHz",
requires={"psram"},
)
ST7789V.extend(
@@ -25,4 +26,5 @@ ST7789V.extend(
dc_pin=39,
reset_pin=40,
invert_colors=True,
requires={"psram"},
)
+5 -2
View File
@@ -16,7 +16,7 @@ from esphome.components.mipi import (
delay,
)
from esphome.components.spi import TYPE_QUAD
from esphome.config_validation import UNDEFINED
from esphome.const import CONF_MIRROR_X, CONF_MIRROR_Y
DriverChip(
"T-DISPLAY-S3-AMOLED",
@@ -29,6 +29,7 @@ DriverChip(
brightness=0xD0,
color_order=MODE_RGB,
no_slpout=True, # SLPOUT is in the init sequence, early
requires={"psram"},
initsequence=(SLPOUT,),
)
@@ -43,6 +44,7 @@ DriverChip(
data_rate="40MHz",
brightness=0xD0,
color_order=MODE_RGB,
requires={"psram"},
initsequence=(
(PAGESEL, 4),
(0x6A, 0x00),
@@ -90,6 +92,7 @@ T4_S3_AMOLED = RM690B0.extend(
reset_pin=13,
enable_pin=9,
bus_mode=TYPE_QUAD,
requires={"psram"},
)
CO5300 = DriverChip(
@@ -98,7 +101,7 @@ CO5300 = DriverChip(
color_order=MODE_RGB,
bus_mode=TYPE_QUAD,
no_slpout=True,
swap_xy=UNDEFINED,
transforms={CONF_MIRROR_X, CONF_MIRROR_Y},
width=480,
height=480,
initsequence=(
@@ -314,6 +314,7 @@ DriverChip(
data_rate="40MHz",
dc_pin=4,
cs_pin=5,
requires={"psram"},
# reset_pin={CONF_INVERTED: True, CONF_NUMBER: 48},
initsequence=(
(0xEF, 0x03, 0x80, 0x02),
@@ -379,6 +380,7 @@ DriverChip(
cs_pin=5,
dc_pin=4,
reset_pin=48,
requires={"psram"},
initsequence=(
(0xEF, 0x03, 0x80, 0x02),
(0xCF, 0x00, 0xC1, 0x30),
@@ -711,6 +713,7 @@ ST7796.extend(
reset_pin=4,
dc_pin={"number": 0, "ignore_strapping_warning": True},
invert_colors=True,
requires={"psram"},
)
ST7789V.extend(
+11 -3
View File
@@ -1,12 +1,16 @@
from esphome.components.mipi import MODE_RGB, DriverChip
from esphome.components.spi import TYPE_QUAD
import esphome.config_validation as cv
from esphome.const import CONF_IGNORE_STRAPPING_WARNING, CONF_NUMBER
from esphome.const import (
CONF_IGNORE_STRAPPING_WARNING,
CONF_MIRROR_X,
CONF_MIRROR_Y,
CONF_NUMBER,
)
AXS15231 = DriverChip(
"AXS15231",
draw_rounding=8,
swap_xy=cv.UNDEFINED,
transforms={CONF_MIRROR_X, CONF_MIRROR_Y},
color_order=MODE_RGB,
bus_mode=TYPE_QUAD,
initsequence=(
@@ -22,6 +26,7 @@ AXS15231.extend(
height=480,
cs_pin={CONF_NUMBER: 45, CONF_IGNORE_STRAPPING_WARNING: True},
data_rate="40MHz",
requires={"psram"},
)
DriverChip(
@@ -36,6 +41,7 @@ DriverChip(
color_order=MODE_RGB,
bus_mode=TYPE_QUAD,
data_rate="40MHz",
requires={"psram"},
initsequence=(
(0xF0, 0x08),
(0xF2, 0x08),
@@ -267,6 +273,7 @@ DriverChip(
color_order=MODE_RGB,
bus_mode=TYPE_QUAD,
data_rate="40MHz",
requires={"psram"},
initsequence=(
(0xF0, 0x28),
(0xF2, 0x28),
@@ -495,6 +502,7 @@ DriverChip(
color_order=MODE_RGB,
bus_mode=TYPE_QUAD,
data_rate="20MHz",
requires={"psram"},
initsequence=(
(0xFF, 0xA5),
(0x41, 0x03),
@@ -10,4 +10,5 @@ ST7789V.extend(
cs_pin=22,
dc_pin=21,
reset_pin=18,
requires={"psram"},
)
@@ -15,6 +15,7 @@ ST7789V.extend(
dc_pin=13,
reset_pin=9,
data_rate="80MHz",
requires={"psram"},
)
ST7789V.extend(
@@ -42,6 +43,7 @@ ST7789V.extend(
enable_pin=[9, 15],
data_rate="10MHz",
bus_mode=TYPE_OCTAL,
requires={"psram"},
)
ST7796.extend(
@@ -55,4 +57,5 @@ ST7796.extend(
dc_pin=9,
backlight_pin=48,
invert_colors=True,
requires={"psram"},
)
@@ -49,6 +49,7 @@ ILI9341.extend(
invert_colors=True,
pixel_mode="18bit",
data_rate="40MHz",
requires={"psram"},
)
GC9107 = ST7789V.extend(
@@ -68,4 +69,5 @@ GC9107.extend(
reset_pin=48,
dc_pin=42,
cs_pin=14,
requires={"psram"},
)
@@ -12,7 +12,7 @@ from esphome.components.mipi import (
PWSET,
DriverChip,
)
import esphome.config_validation as cv
from esphome.const import CONF_MIRROR_X, CONF_MIRROR_Y
from .amoled import CO5300
from .ili import ILI9488_A, ST7789V
@@ -155,7 +155,7 @@ ST7789P = DriverChip(
ILI9488_A.extend(
"PICO-RESTOUCH-LCD-3.5",
swap_xy=cv.UNDEFINED,
transforms={CONF_MIRROR_X, CONF_MIRROR_Y},
spi_16=True,
pixel_mode="16bit",
mirror_x=True,
@@ -175,6 +175,7 @@ CO5300.extend(
offset_width=6,
cs_pin=12,
reset_pin=39,
requires={"psram"},
)
# Waveshare ESP32-S3 Touch AMOLED 2.16" (CO5300 controller)
@@ -189,6 +190,7 @@ CO5300.extend(
cs_pin=12,
reset_pin=39,
data_rate="40MHz",
requires={"psram"},
)
AXS15231.extend(
@@ -198,6 +200,7 @@ AXS15231.extend(
data_rate="80MHz",
cs_pin=9,
reset_pin=21,
requires={"psram"},
)
# Waveshare 1.83-v2
@@ -281,6 +284,7 @@ ST7789V.extend(
offset_height=40,
invert_colors=True,
data_rate="40MHz",
requires={"psram"},
)
CO5300.extend(
@@ -291,4 +295,5 @@ CO5300.extend(
cs_pin=9,
reset_pin=21,
enable_pin=1,
requires={"psram"},
)
+4
View File
@@ -574,6 +574,9 @@ class EsphomeCore:
self.build_path: Path | None = None
# The validated configuration, this is None until the config has been validated
self.config: ConfigType | None = None
# The raw configuration as read from YAML (after packages/substitutions),
# available during validation before the config is fully validated
self.raw_config: ConfigType | None = None
# YAML frontmatter loaded from user YAML files. Frontmatter is a leading
# YAML document separated by `---` from the actual configuration. It is
# ignored by config validation and code generation, but kept here so it
@@ -650,6 +653,7 @@ class EsphomeCore:
self.config_path = None
self.build_path = None
self.config = None
self.raw_config = None
self.frontmatter = {}
self.event_loop = _FakeEventLoop()
self.task_counter = 0
@@ -24,6 +24,9 @@ spi:
mosi_pin: 6
clk_pin: 7
psram:
mode: quad
display:
- platform: mipi_spi
id: lcd_display
@@ -19,6 +19,9 @@ spi:
mosi_pin: 6
clk_pin: 7
psram:
mode: quad
display:
- platform: mipi_spi
id: lcd_display
@@ -4,6 +4,9 @@ esphome:
esp32:
board: esp32s3box
psram:
mode: octal
image:
defaults:
type: rgb565
@@ -71,6 +71,18 @@ def test_configuration_errors(set_core_config: SetCoreConfigCallable) -> None:
}
)
# DSI displays cannot swap axes; enabling swap_xy reports a clear error.
with pytest.raises(cv.Invalid, match="'swap_xy' is not supported by this model"):
CONFIG_SCHEMA(
{
"model": "custom",
"init_sequence": [[0xA0, 0x01]],
"lane_bit_rate": "1.5Gbps",
"dimensions": {"width": 320, "height": 240},
"transform": {"mirror_x": True, "mirror_y": True, "swap_xy": True},
}
)
def test_configuration_success(set_core_config: SetCoreConfigCallable) -> None:
"""Test successful configuration validation."""
@@ -116,6 +128,33 @@ def test_configuration_success(set_core_config: SetCoreConfigCallable) -> None:
CONFIG_SCHEMA(config)
def test_metadata_records_rotation(set_core_config: SetCoreConfigCallable) -> None:
"""A configured display rotation is recorded in the metadata.
LVGL relies on this to flag a rotation set in the display config (see the
mipi_spi tests for the end-to-end LVGL rejection).
"""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32-p4-evboard", KEY_VARIANT: VARIANT_ESP32P4},
)
from esphome.components.display import get_display_metadata
from esphome.components.mipi_dsi.display import CONFIG_SCHEMA
base = {
"model": "custom",
"init_sequence": [[0xA0, 0x01]],
"lane_bit_rate": "1.5Gbps",
"dimensions": {"width": 320, "height": 240},
}
config = CONFIG_SCHEMA({**base, "id": "rotated", "rotation": 90})
assert get_display_metadata(config["id"]).rotation == 90
config = CONFIG_SCHEMA({**base, "id": "unrotated"})
assert get_display_metadata(config["id"]).rotation == 0
def test_code_generation(
generate_main: Callable[[str | Path], str],
component_fixture_path: Callable[[str], Path],
@@ -0,0 +1,89 @@
"""Tests for mipi_rgb configuration validation, in particular the per-model
``requires`` component check (see esphome.components.mipi.DriverChip.check_requirements)."""
from __future__ import annotations
from typing import Any
import pytest
from esphome import config_validation as cv
from esphome.components.esp32 import KEY_BOARD, KEY_VARIANT, VARIANT_ESP32S3
from esphome.components.mipi_rgb.display import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA
# Importing pca9554 registers its pin schema with pins.PIN_SCHEMA_REGISTRY so that
# models (e.g. SEEED-INDICATOR-D1) that reference pca9554-backed pins in their
# defaults can be validated by the mipi_rgb CONFIG_SCHEMA in this test.
import esphome.components.pca9554 # noqa: F401
from esphome.const import PlatformFramework
from esphome.core import CORE
from esphome.types import ConfigType
from tests.component_tests.types import SetCoreConfigCallable
def _validated(config: ConfigType) -> ConfigType:
"""Run the component config schema followed by the final validation."""
config = CONFIG_SCHEMA(config)
FINAL_VALIDATE_SCHEMA(config)
return config
def test_model_requires_psram(set_core_config: SetCoreConfigCallable) -> None:
"""A model known to have PSRAM on its board rejects a config without it.
RGB parallel displays always need a full framebuffer, so every model in this
component is expected to carry ``requires={"psram", ...}``. This board has no
other requirements, so its check is exercised in isolation here.
"""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3},
)
CORE.raw_config = {}
with pytest.raises(
cv.Invalid,
match=r"ESP32-8048S070 requires component 'psram' to be configured",
):
_validated({"model": "ESP32-8048S070"})
def test_model_requires_psram_satisfied(
set_core_config: SetCoreConfigCallable,
set_component_config: Any,
) -> None:
"""The same board model validates once PSRAM is configured."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3},
)
set_component_config("psram", True)
CORE.raw_config = {"psram": True}
config = _validated({"model": "ESP32-8048S070"})
assert config["model"] == "ESP32-8048S070"
def test_model_requires_psram_and_expander(
set_core_config: SetCoreConfigCallable,
set_component_config: Any,
) -> None:
"""A model that also depends on an I2C GPIO expander lists both when missing."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3},
)
# Only satisfy one of the two requirements.
set_component_config("psram", True)
CORE.raw_config = {"psram": True}
with pytest.raises(
cv.Invalid,
match=r"SEEED-INDICATOR-D1 requires component 'pca9554' to be configured",
):
_validated(
{
"model": "SEEED-INDICATOR-D1",
"spi_id": "spi_bus",
}
)
@@ -0,0 +1,137 @@
"""Tests for mipi_rgb configuration validation."""
import pytest
from esphome import config_validation as cv
# Importing these registers their pin schemas with pins.PIN_SCHEMA_REGISTRY so that
# models referencing IO-expander-backed pins in their defaults (e.g. the LilyGO
# T-RGB boards via xl9535, SEEED-INDICATOR-D1 via pca9554, or the Waveshare panels
# via ch422g) can be validated by the mipi_rgb CONFIG_SCHEMA in this test.
import esphome.components.ch422g # noqa: F401
from esphome.components.display import get_display_metadata
from esphome.components.esp32 import KEY_BOARD, VARIANT_ESP32S3
import esphome.components.pca9554 # noqa: F401
import esphome.components.xl9535 # noqa: F401
from esphome.const import (
CONF_BLUE,
CONF_DIMENSIONS,
CONF_GREEN,
CONF_HEIGHT,
CONF_INIT_SEQUENCE,
CONF_MIRROR_X,
CONF_MIRROR_Y,
CONF_RED,
CONF_SWAP_XY,
CONF_WIDTH,
KEY_VARIANT,
PlatformFramework,
)
from tests.component_tests.types import SetCoreConfigCallable
# A generic set of data pins so that models without a default pin assignment
# (e.g. CUSTOM and RPI) still validate.
DATA_PINS = {
CONF_RED: [1, 2, 3, 4, 5],
CONF_GREEN: [6, 7, 8, 9, 10, 11],
CONF_BLUE: [12, 13, 14, 15, 16],
}
def _set_s3(set_core_config: SetCoreConfigCallable) -> None:
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={
KEY_BOARD: "esp32-s3-devkitc-1",
KEY_VARIANT: VARIANT_ESP32S3,
},
)
def test_configuration_success(set_core_config: SetCoreConfigCallable) -> None:
"""Every predefined model validates once required defaults are supplied."""
_set_s3(set_core_config)
from esphome.components.mipi_rgb.display import CONFIG_SCHEMA, MODELS
for name, model in MODELS.items():
config = {"model": name, "data_pins": DATA_PINS, "pclk_pin": 21}
if model.initsequence is None:
config[CONF_INIT_SEQUENCE] = [[0xA0, 0x01]]
if not model.get_default(CONF_WIDTH):
config[CONF_DIMENSIONS] = {CONF_WIDTH: 480, CONF_HEIGHT: 480}
CONFIG_SCHEMA(config)
def test_transform_matches_model_support(
set_core_config: SetCoreConfigCallable,
) -> None:
"""The transform schema only accepts the axes a model actually supports."""
_set_s3(set_core_config)
from esphome.components.mipi_rgb.display import CONFIG_SCHEMA, MODELS
# ESP32-8048S070 supports both mirror axes but not swap_xy (RGB displays
# never support axis swapping).
model = MODELS["ESP32-8048S070"]
assert model.transforms == {CONF_MIRROR_X, CONF_MIRROR_Y}
base = {"model": "ESP32-8048S070", "data_pins": DATA_PINS, "pclk_pin": 21}
CONFIG_SCHEMA({**base, "transform": {"mirror_x": True, "mirror_y": False}})
# An unsupported axis may be explicitly disabled (a harmless no-op)...
CONFIG_SCHEMA(
{**base, "transform": {"mirror_x": True, "mirror_y": False, "swap_xy": False}}
)
# ...but enabling it reports a clear, model-specific error.
with pytest.raises(cv.Invalid, match="'swap_xy' is not supported by this model"):
CONFIG_SCHEMA(
{
**base,
"transform": {"mirror_x": True, "mirror_y": False, "swap_xy": True},
}
)
def test_st7701s_only_supports_mirror_x(
set_core_config: SetCoreConfigCallable,
) -> None:
"""ST7701S panels shorter than full height only expose mirror_x.
mirror_y only works at full height (864px), so the LilyGO 480px panels must
reject a mirror_y transform.
"""
_set_s3(set_core_config)
from esphome.components.mipi_rgb.display import CONFIG_SCHEMA, MODELS
model = MODELS["T-RGB-2.1"]
assert model.transforms == {CONF_MIRROR_X}
assert CONF_SWAP_XY not in model.transforms
base = {"model": "T-RGB-2.1"}
CONFIG_SCHEMA({**base, "transform": {"mirror_x": True}})
with pytest.raises(cv.Invalid, match="'mirror_y' is not supported by this model"):
CONFIG_SCHEMA({**base, "transform": {"mirror_x": True, "mirror_y": True}})
def test_metadata_records_rotation(
set_core_config: SetCoreConfigCallable,
) -> None:
"""A configured display rotation is recorded in the metadata.
LVGL relies on this to flag a rotation set in the display config (see the
mipi_spi tests for the end-to-end LVGL rejection).
"""
_set_s3(set_core_config)
from esphome.components.mipi_rgb.display import CONFIG_SCHEMA
base = {"model": "ESP32-8048S070", "data_pins": DATA_PINS, "pclk_pin": 21}
config = CONFIG_SCHEMA({**base, "id": "rotated", "rotation": 90})
assert get_display_metadata(config["id"]).rotation == 90
config = CONFIG_SCHEMA({**base, "id": "unrotated"})
assert get_display_metadata(config["id"]).rotation == 0
@@ -3,6 +3,9 @@
from collections.abc import Callable
from pathlib import Path
import pytest
from esphome import config_validation as cv
from esphome.components.const import BYTE_ORDER_BIG
from esphome.components.display import get_all_display_metadata, get_display_metadata
from esphome.components.esp32 import (
@@ -13,6 +16,7 @@ from esphome.components.esp32 import (
)
from esphome.components.mipi_spi.display import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA
from esphome.const import PlatformFramework
from esphome.core import ID
from tests.component_tests.types import SetCoreConfigCallable
@@ -23,6 +27,18 @@ def validated_config(config):
return config
def _lvgl_config(display_id: str) -> dict:
"""Build a minimal LVGL config dict referencing the given display id."""
return {
"displays": [ID(display_id, True)],
"log_level": "WARN",
"color_depth": 16,
"transparency_key": 0x000400,
"draw_rounding": 2,
"buffer_size": 0,
}
def test_metadata_native_quad_default_test_card(
set_core_config: SetCoreConfigCallable,
) -> None:
@@ -91,7 +107,7 @@ def test_metadata_no_swap_xy_not_full_hardware_rotation(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32-s3-devkitc-1", KEY_VARIANT: VARIANT_ESP32S3},
)
# JC3248W535 has swap_xy=cv.UNDEFINED -> transforms={mirror_x, mirror_y} only
# JC3248W535 has transforms={mirror_x, mirror_y} only
config = CONFIG_SCHEMA({"model": "JC3248W535", "id": "jc3248w535"})
meta = get_display_metadata(config["id"])
assert meta is not None
@@ -166,3 +182,69 @@ def test_metadata_via_code_generation_lvgl(
assert meta.height == 160
assert meta.has_hardware_rotation is True
assert meta.byte_order == BYTE_ORDER_BIG
def test_metadata_records_rotation(
set_core_config: SetCoreConfigCallable,
) -> None:
"""A configured display rotation is recorded in the metadata."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
config = CONFIG_SCHEMA(
{"model": "ST7735", "dc_pin": 18, "id": "rotated", "rotation": 90}
)
meta = get_display_metadata(config["id"])
assert meta is not None
assert meta.rotation == 90
def test_metadata_rotation_defaults_to_zero(
set_core_config: SetCoreConfigCallable,
) -> None:
"""A display without a rotation reports rotation 0 in its metadata."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
config = CONFIG_SCHEMA({"model": "ST7735", "dc_pin": 18, "id": "unrotated"})
meta = get_display_metadata(config["id"])
assert meta is not None
assert meta.rotation == 0
def test_rotation_flagged_when_used_with_lvgl(
set_core_config: SetCoreConfigCallable,
) -> None:
"""A display with a rotation is rejected when driven by LVGL.
LVGL manages its own rotation, so a rotation set in the display config must be
flagged and the user directed to configure it in the LVGL block instead. This
exercises the full chain: the mipi_spi schema records the rotation in the
display metadata, and LVGL's final validation reports it.
"""
from esphome.components.lvgl import final_validation
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
CONFIG_SCHEMA({"model": "ST7735", "dc_pin": 18, "id": "rotated", "rotation": 90})
with pytest.raises(cv.Invalid, match="rotation.*not compatible with LVGL"):
final_validation([_lvgl_config("rotated")])
def test_no_rotation_accepted_with_lvgl(
set_core_config: SetCoreConfigCallable,
) -> None:
"""A display without a rotation validates cleanly when driven by LVGL."""
from esphome.components.lvgl import final_validation
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
CONFIG_SCHEMA({"model": "ST7735", "dc_pin": 18, "id": "unrotated"})
# Should not raise.
final_validation([_lvgl_config("unrotated")])
@@ -6,10 +6,13 @@ from typing import Any
import pytest
from esphome import config_validation as cv
from esphome.components.display import CONF_SHOW_TEST_CARD
from esphome.components.esp32 import KEY_BOARD, KEY_VARIANT, VARIANT_ESP32
from esphome.components.mipi import DriverChip
from esphome.components.mipi_spi.display import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA
from esphome.const import CONF_BUFFER_SIZE, PlatformFramework
from esphome.core import CORE
from esphome.types import ConfigType
from tests.component_tests.types import SetCoreConfigCallable
@@ -183,3 +186,77 @@ def test_buffer_size_selected_when_lvgl_with_test_card(
)
assert config[CONF_BUFFER_SIZE] == pytest.approx(1.0 / 4)
def test_requires_missing_single_component_raises() -> None:
"""A model that requires a single component raises when it is absent."""
CORE.raw_config = {}
chip = DriverChip("TEST-REQUIRES-PSRAM", requires={"psram"})
with pytest.raises(
cv.Invalid,
match=r"TEST-REQUIRES-PSRAM requires component 'psram' to be configured",
):
chip.check_requirements()
def test_requires_missing_multiple_components_raises() -> None:
"""A model that requires several components lists all the missing ones, pluralized."""
CORE.raw_config = {}
chip = DriverChip("TEST-REQUIRES-MULTI", requires={"psram", "pca9554"})
with pytest.raises(
cv.Invalid,
match=r"TEST-REQUIRES-MULTI requires components '.*' to be configured",
) as excinfo:
chip.check_requirements()
assert "psram" in str(excinfo.value)
assert "pca9554" in str(excinfo.value)
def test_requires_satisfied_does_not_raise() -> None:
"""No error is raised once all the required components are configured."""
CORE.raw_config = {"psram": True, "pca9554": []}
chip = DriverChip("TEST-REQUIRES-SATISFIED", requires={"psram", "pca9554"})
chip.check_requirements() # Should not raise
def test_requires_absent_does_not_raise() -> None:
"""Models without a requires set are unaffected by the check."""
CORE.raw_config = {}
chip = DriverChip("TEST-REQUIRES-NONE")
chip.check_requirements() # Should not raise
def test_predefined_model_requires_psram(
set_core_config: SetCoreConfigCallable,
) -> None:
"""A predefined board model known to have PSRAM rejects a config without it."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
CORE.raw_config = {}
with pytest.raises(
cv.Invalid, match=r"S3BOX requires component 'psram' to be configured"
):
_validated({"model": "s3box"})
def test_predefined_model_requires_psram_satisfied(
set_core_config: SetCoreConfigCallable,
set_component_config: Any,
) -> None:
"""The same board model validates once PSRAM is configured."""
set_core_config(
PlatformFramework.ESP32_IDF,
platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32},
)
set_component_config("psram", True)
CORE.raw_config = {"psram": True}
config = _validated({"model": "s3box"})
assert config["model"] == "S3BOX"
+1 -1
View File
@@ -136,7 +136,7 @@ def test_dimension_validation(
"model": "JC3248W535",
"transform": {"mirror_x": False, "mirror_y": True, "swap_xy": True},
},
"Axis swapping not supported by this model",
"'swap_xy' is not supported by this model",
id="axis_swapping_not_supported",
),
pytest.param(
@@ -4,6 +4,7 @@ from __future__ import annotations
from collections.abc import Callable
from pathlib import Path
from typing import Any
import pytest
@@ -222,6 +223,7 @@ class TestNewModelVariants:
def test_m5core2_with_native_dimensions(
self,
set_core_config: SetCoreConfigCallable,
set_component_config: Callable[[str, Any], None],
) -> None:
"""Test M5CORE2 variant with reset native_width and native_height."""
set_core_config(
@@ -231,6 +233,8 @@ class TestNewModelVariants:
KEY_VARIANT: VARIANT_ESP32S3,
},
)
# M5CORE2 has PSRAM on board and requires it to be configured
set_component_config("psram", True)
# M5CORE2 should validate successfully
config = validated_config({"model": "M5CORE2"})
@@ -24,6 +24,9 @@ spi:
mosi_pin: 6
clk_pin: 7
psram:
mode: quad
display:
- platform: mipi_spi
id: lcd_display
@@ -23,6 +23,9 @@ spi:
mosi_pin: 6
clk_pin: 7
psram:
mode: quad
display:
- platform: mipi_spi
id: lcd_display