Include model-driven display schemas in the language schema dump (#16872)

This commit is contained in:
J. Nick Koston
2026-06-07 17:30:43 -05:00
committed by GitHub
parent 64fc09646c
commit cbc3770b11
7 changed files with 87 additions and 1 deletions

View File

@@ -5,7 +5,11 @@ from esphome import core, pins
import esphome.codegen as cg
from esphome.components import display, spi
from esphome.components.display import CONF_SHOW_TEST_CARD, validate_rotation
from esphome.components.mipi import flatten_sequence, map_sequence
from esphome.components.mipi import (
flatten_sequence,
map_sequence,
model_schema_extractor,
)
import esphome.config_validation as cv
from esphome.config_validation import update_interval
from esphome.const import (
@@ -111,6 +115,7 @@ def model_schema(config):
)
@model_schema_extractor(MODELS, model_schema)
def customise_schema(config):
"""
Create a customised config schema for a specific model and validate the configuration.

View File

@@ -2,8 +2,12 @@
# Various configuration constants for MIPI displays
# Various utility functions for MIPI DBI configuration
from collections.abc import Callable
import functools
from typing import Any, Self
import voluptuous as vol
from esphome.components.const import CONF_COLOR_DEPTH
from esphome.components.display import CONF_SHOW_TEST_CARD, display_ns
import esphome.config_validation as cv
@@ -18,6 +22,7 @@ from esphome.const import (
CONF_LAMBDA,
CONF_MIRROR_X,
CONF_MIRROR_Y,
CONF_MODEL,
CONF_OFFSET_HEIGHT,
CONF_OFFSET_WIDTH,
CONF_PAGES,
@@ -27,6 +32,7 @@ from esphome.const import (
CONF_WIDTH,
)
from esphome.core import TimePeriod
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
LOGGER = cv.logging.getLogger(__name__)
@@ -239,6 +245,54 @@ def delay(ms):
return DELAY_FLAG, ms
# Generic placeholder model present in every DriverChip registry; skipped when
# choosing a representative model for schema extraction.
_CUSTOM_MODEL = "CUSTOM"
def model_schema_extractor(
models: dict[str, Any],
model_schema: Callable[[dict[str, Any]], Any],
extra: dict[str, Any] | None = None,
) -> Callable[[Callable[[Any], Any]], Callable[[Any], Any]]:
"""
Decorate a model-driven display CONFIG_SCHEMA so the language-schema dumper
can extract it.
The schema is generated per ``model`` at validation time, so the static
dumper has nothing to walk. When the dumper passes SCHEMA_EXTRACT, resolve a
representative schema for a real model (the generic "CUSTOM" placeholder
over-constrains fields like init_sequence) plus any *extra* keys the model
needs, e.g. a bus mode, and hand that back; runtime validation is untouched.
"""
def decorate(config_schema: Callable[[Any], Any]) -> Callable[[Any], Any]:
@schema_extractor("schema")
@functools.wraps(config_schema)
def wrapper(config: Any) -> Any:
if config is not SCHEMA_EXTRACT:
return config_schema(config)
names = sorted(models)
representative = next((n for n in names if n != _CUSTOM_MODEL), names[0])
schema = model_schema({CONF_MODEL: representative, **(extra or {})})
if isinstance(schema, vol.All):
schema = next(
(v for v in schema.validators if isinstance(v, vol.Schema)),
schema,
)
if isinstance(schema, vol.Schema):
# The resolved schema pins ``model`` to the representative; expose
# the full model list so the dumped enum offers every model.
schema = schema.extend(
{cv.Required(CONF_MODEL): cv.one_of(*names, upper=True)}
)
return schema
return wrapper
return decorate
class DriverChip:
"""
A class representing a MIPI DBI driver chip model.

View File

@@ -32,6 +32,7 @@ from esphome.components.mipi import (
dimension_schema,
get_color_depth,
map_sequence,
model_schema_extractor,
power_of_two,
requires_buffer,
)
@@ -161,6 +162,7 @@ def model_schema(config):
)
@model_schema_extractor(MODELS, model_schema)
def _config_schema(config):
config = cv.Schema(
{

View File

@@ -30,6 +30,7 @@ from esphome.components.mipi import (
DriverChip,
dimension_schema,
map_sequence,
model_schema_extractor,
power_of_two,
requires_buffer,
)
@@ -219,6 +220,7 @@ def model_schema(config):
return schema
@model_schema_extractor(MODELS, model_schema)
def _config_schema(config):
config = cv.Schema(
{

View File

@@ -22,6 +22,7 @@ from esphome.components.mipi import (
dimension_schema,
get_color_depth,
map_sequence,
model_schema_extractor,
power_of_two,
requires_buffer,
)
@@ -227,6 +228,7 @@ def model_schema(config):
return schema
@model_schema_extractor(MODELS, model_schema, extra={CONF_BUS_MODE: TYPE_SINGLE})
def customise_schema(config):
"""
Create a customised config schema for a specific model and validate the configuration.

View File

@@ -1002,6 +1002,10 @@ def convert(schema, config_var, path):
else:
config_var["use_id_type"] = str(data.base)
config_var[S_TYPE] = "use_id"
elif schema_type == "schema":
# A callable CONFIG_SCHEMA that returned a representative schema
# for extraction (model-driven components); walk it as usual.
convert(data, config_var, path)
else:
raise TypeError("Unknown extracted schema type")
elif config_var.get("key") == "GeneratedID":

View File

@@ -117,6 +117,23 @@ def test_convert_emits_explicit_sensitive_marker() -> None:
assert config_var["type"] == "string"
def test_convert_walks_callable_schema_extractor() -> None:
"""A callable schema tagged for "schema" extraction is resolved and walked."""
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
@schema_extractor("schema")
def dynamic_schema(value):
if value is SCHEMA_EXTRACT:
return cv.Schema({cv.Required("foo"): cv.string})
return value
config_var: dict = {}
_bls.convert(dynamic_schema, config_var, "/test")
assert config_var["type"] == "schema"
assert "foo" in config_var["schema"]["config_vars"]
def test_convert_keys_emits_heuristic_sensitive_marker() -> None:
converted: dict = {}
_bls.convert_keys(converted, {cv.Optional("password"): cv.string}, "/root")