mirror of
https://github.com/esphome/esphome.git
synced 2025-04-19 13:17:19 +00:00
[mapping] Implement yaml-configured maps (#8333)
This commit is contained in:
parent
55e099450c
commit
f10bc73d31
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
134
esphome/components/mapping/__init__.py
Normal file
134
esphome/components/mapping/__init__.py
Normal 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
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
71
tests/components/mapping/common.yaml
Normal file
71
tests/components/mapping/common.yaml
Normal 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]);
|
17
tests/components/mapping/test.esp32-ard.yaml
Normal file
17
tests/components/mapping/test.esp32-ard.yaml
Normal 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
|
17
tests/components/mapping/test.esp32-c3-ard.yaml
Normal file
17
tests/components/mapping/test.esp32-c3-ard.yaml
Normal 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
|
17
tests/components/mapping/test.esp32-c3-idf.yaml
Normal file
17
tests/components/mapping/test.esp32-c3-idf.yaml
Normal 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
|
17
tests/components/mapping/test.esp32-idf.yaml
Normal file
17
tests/components/mapping/test.esp32-idf.yaml
Normal 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
|
17
tests/components/mapping/test.esp8266-ard.yaml
Normal file
17
tests/components/mapping/test.esp8266-ard.yaml
Normal 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
|
12
tests/components/mapping/test.host.yaml
Normal file
12
tests/components/mapping/test.host.yaml
Normal 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
|
17
tests/components/mapping/test.rp2040-ard.yaml
Normal file
17
tests/components/mapping/test.rp2040-ard.yaml
Normal 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
|
Loading…
x
Reference in New Issue
Block a user