[mapping] Implement yaml-configured maps (#8333)

This commit is contained in:
Clyde Stubbs 2025-04-17 11:18:48 +10:00 committed by GitHub
parent 55e099450c
commit f10bc73d31
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 336 additions and 2 deletions

View File

@ -250,6 +250,7 @@ esphome/components/ltr501/* @latonita
esphome/components/ltr_als_ps/* @latonita
esphome/components/lvgl/* @clydebarrow
esphome/components/m5stack_8angle/* @rnauber
esphome/components/mapping/* @clydebarrow
esphome/components/matrix_keypad/* @ssieb
esphome/components/max17043/* @blacknell
esphome/components/max31865/* @DAVe3283

View File

@ -3,6 +3,8 @@ from esphome.const import CONF_BLUE, CONF_GREEN, CONF_ID, CONF_RED, CONF_WHITE
ColorStruct = cg.esphome_ns.struct("Color")
INSTANCE_TYPE = ColorStruct
MULTI_CONF = True
CONF_RED_INT = "red_int"

View File

@ -291,6 +291,8 @@ SOURCE_WEB = "web"
Image_ = image_ns.class_("Image")
INSTANCE_TYPE = Image_
def compute_local_image_path(value) -> Path:
url = value[CONF_URL] if isinstance(value, dict) else value

View File

@ -0,0 +1,134 @@
import difflib
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_FROM, CONF_ID, CONF_TO
from esphome.core import CORE
from esphome.cpp_generator import MockObj, VariableDeclarationExpression, add_global
from esphome.loader import get_component
CODEOWNERS = ["@clydebarrow"]
MULTI_CONF = True
map_ = cg.std_ns.class_("map")
CONF_ENTRIES = "entries"
CONF_CLASS = "class"
class IndexType:
"""
Represents a type of index in a map.
"""
def __init__(self, validator, data_type, conversion):
self.validator = validator
self.data_type = data_type
self.conversion = conversion
INDEX_TYPES = {
"int": IndexType(cv.int_, cg.int_, int),
"string": IndexType(cv.string, cg.std_string, str),
}
def to_schema(value):
"""
Generate a schema for the 'to' field of a map. This can be either one of the index types or a class name.
:param value:
:return:
"""
return cv.Any(
cv.one_of(*INDEX_TYPES, lower=True),
cv.one_of(*CORE.id_classes.keys()),
)(value)
BASE_SCHEMA = cv.Schema(
{
cv.Required(CONF_ID): cv.declare_id(map_),
cv.Required(CONF_FROM): cv.one_of(*INDEX_TYPES, lower=True),
cv.Required(CONF_TO): cv.string,
},
extra=cv.ALLOW_EXTRA,
)
def get_object_type(to_):
"""
Get the object type from a string. Possible formats:
xxx The name of a component which defines INSTANCE_TYPE
esphome::xxx::yyy A C++ class name defined in a component
xxx::yyy A C++ class name defined in a component
yyy A C++ class name defined in the core
"""
if cls := CORE.id_classes.get(to_):
return cls
if cls := CORE.id_classes.get(to_.removeprefix("esphome::")):
return cls
# get_component will throw a wobbly if we don't check this first.
if "." in to_:
return None
if component := get_component(to_):
return component.instance_type
return None
def map_schema(config):
config = BASE_SCHEMA(config)
if CONF_ENTRIES not in config or not isinstance(config[CONF_ENTRIES], dict):
raise cv.Invalid("an entries list is required for a map")
entries = config[CONF_ENTRIES]
if len(entries) == 0:
raise cv.Invalid("Map must have at least one entry")
to_ = config[CONF_TO]
if to_ in INDEX_TYPES:
value_type = INDEX_TYPES[to_].validator
else:
value_type = get_object_type(to_)
if value_type is None:
matches = difflib.get_close_matches(to_, CORE.id_classes)
raise cv.Invalid(
f"No known mappable class name matches '{to_}'; did you mean one of {', '.join(matches)}?"
)
value_type = cv.use_id(value_type)
config[CONF_ENTRIES] = {k: value_type(v) for k, v in entries.items()}
return config
CONFIG_SCHEMA = map_schema
async def to_code(config):
entries = config[CONF_ENTRIES]
from_ = config[CONF_FROM]
to_ = config[CONF_TO]
index_conversion = INDEX_TYPES[from_].conversion
index_type = INDEX_TYPES[from_].data_type
if to_ in INDEX_TYPES:
value_conversion = INDEX_TYPES[to_].conversion
value_type = INDEX_TYPES[to_].data_type
entries = {
index_conversion(key): value_conversion(value)
for key, value in entries.items()
}
else:
entries = {
index_conversion(key): await cg.get_variable(value)
for key, value in entries.items()
}
value_type = get_object_type(to_)
if list(entries.values())[0].op != ".":
value_type = value_type.operator("ptr")
varid = config[CONF_ID]
varid.type = map_.template(index_type, value_type)
var = MockObj(varid, ".")
decl = VariableDeclarationExpression(varid.type, "", varid)
add_global(decl)
CORE.register_variable(varid, var)
for key, value in entries.items():
cg.add(var.insert((key, value)))
return var

View File

@ -518,6 +518,8 @@ class EsphomeCore:
self.verbose = False
# Whether ESPHome was started in quiet mode
self.quiet = False
# A list of all known ID classes
self.id_classes = {}
def reset(self):
from esphome.pins import PIN_SCHEMA_REGISTRY

View File

@ -789,13 +789,17 @@ class MockObj(Expression):
def class_(self, name: str, *parents: "MockObjClass") -> "MockObjClass":
op = "" if self.op == "" else "::"
return MockObjClass(f"{self.base}{op}{name}", ".", parents=parents)
result = MockObjClass(f"{self.base}{op}{name}", ".", parents=parents)
CORE.id_classes[str(result)] = result
return result
def struct(self, name: str) -> "MockObjClass":
return self.class_(name)
def enum(self, name: str, is_class: bool = False) -> "MockObj":
return MockObjEnum(enum=name, is_class=is_class, base=self.base, op=self.op)
result = MockObjEnum(enum=name, is_class=is_class, base=self.base, op=self.op)
CORE.id_classes[str(result)] = result
return result
def operator(self, name: str) -> "MockObj":
"""Various other operations.

View File

@ -91,6 +91,10 @@ class ComponentManifest:
def codeowners(self) -> list[str]:
return getattr(self.module, "CODEOWNERS", [])
@property
def instance_type(self) -> list[str]:
return getattr(self.module, "INSTANCE_TYPE", None)
@property
def final_validate_schema(self) -> Optional[Callable[[ConfigType], None]]:
"""Components can declare a `FINAL_VALIDATE_SCHEMA` cv.Schema that gets called

View File

@ -0,0 +1,71 @@
image:
grayscale:
alpha_channel:
- file: ../../pnglogo.png
id: image_1
resize: 50x50
- file: ../../pnglogo.png
id: image_2
resize: 50x50
mapping:
- id: weather_map
from: string
to: "image::Image"
entries:
clear-night: image_1
sunny: image_2
- id: weather_map_1
from: string
to: esphome::image::Image
entries:
clear-night: image_1
sunny: image_2
- id: weather_map_2
from: string
to: image
entries:
clear-night: image_1
sunny: image_2
- id: int_map
from: int
to: string
entries:
1: "one"
2: "two"
3: "three"
77: "seventy-seven"
- id: string_map
from: string
to: int
entries:
one: 1
two: 2
three: 3
seventy-seven: 77
- id: color_map
from: string
to: color
entries:
red: red_id
blue: blue_id
green: green_id
color:
- id: red_id
red: 1.0
green: 0.0
blue: 0.0
- id: green_id
red: 0.0
green: 1.0
blue: 0.0
- id: blue_id
red: 0.0
green: 0.0
blue: 1.0
display:
lambda: |-
it.image(0, 0, id(weather_map)[0]);
it.image(0, 100, id(weather_map)[1]);

View File

@ -0,0 +1,17 @@
spi:
- id: spi_main_lcd
clk_pin: 16
mosi_pin: 17
miso_pin: 15
display:
- platform: ili9xxx
id: main_lcd
model: ili9342
cs_pin: 12
dc_pin: 13
reset_pin: 21
invert_colors: false
packages:
map: !include common.yaml

View File

@ -0,0 +1,17 @@
spi:
- id: spi_main_lcd
clk_pin: 6
mosi_pin: 7
miso_pin: 5
display:
- platform: ili9xxx
id: main_lcd
model: ili9342
cs_pin: 8
dc_pin: 9
reset_pin: 10
invert_colors: false
packages:
map: !include common.yaml

View File

@ -0,0 +1,17 @@
spi:
- id: spi_main_lcd
clk_pin: 6
mosi_pin: 7
miso_pin: 5
display:
- platform: ili9xxx
id: main_lcd
model: ili9342
cs_pin: 8
dc_pin: 9
reset_pin: 10
invert_colors: false
packages:
map: !include common.yaml

View File

@ -0,0 +1,17 @@
spi:
- id: spi_main_lcd
clk_pin: 16
mosi_pin: 17
miso_pin: 15
display:
- platform: ili9xxx
id: main_lcd
model: ili9342
cs_pin: 12
dc_pin: 13
reset_pin: 21
invert_colors: false
packages:
map: !include common.yaml

View File

@ -0,0 +1,17 @@
spi:
- id: spi_main_lcd
clk_pin: 14
mosi_pin: 13
miso_pin: 12
display:
- platform: ili9xxx
id: main_lcd
model: ili9342
cs_pin: 5
dc_pin: 15
reset_pin: 16
invert_colors: false
packages:
map: !include common.yaml

View File

@ -0,0 +1,12 @@
display:
- platform: sdl
id: sdl_display
update_interval: 1s
auto_clear_enabled: false
show_test_card: true
dimensions:
width: 450
height: 600
packages:
map: !include common.yaml

View File

@ -0,0 +1,17 @@
spi:
- id: spi_main_lcd
clk_pin: 2
mosi_pin: 3
miso_pin: 4
display:
- platform: ili9xxx
id: main_lcd
model: ili9342
cs_pin: 20
dc_pin: 21
reset_pin: 22
invert_colors: false
packages:
map: !include common.yaml