diff --git a/esphome/components/mapping/__init__.py b/esphome/components/mapping/__init__.py index a36b414fd..3c7d78a27 100644 --- a/esphome/components/mapping/__init__.py +++ b/esphome/components/mapping/__init__.py @@ -1,18 +1,27 @@ +from collections.abc import Callable import difflib import esphome.codegen as cg +from esphome.components.const import KEY_METADATA 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.core import CORE, ID +from esphome.cpp_generator import ( + MockObj, + MockObjClass, + VariableDeclarationExpression, + add_global, +) from esphome.loader import get_component CODEOWNERS = ["@clydebarrow"] MULTI_CONF = True +DOMAIN = "mapping" mapping_ns = cg.esphome_ns.namespace("mapping") mapping_class = mapping_ns.class_("Mapping") +CONF_DEFAULT_VALUE = "default_value" CONF_ENTRIES = "entries" CONF_CLASS = "class" @@ -22,11 +31,18 @@ class IndexType: Represents a type of index in a map. """ - def __init__(self, validator, data_type, conversion): + def __init__( + self, validator: Callable, data_type: MockObj, conversion: Callable = None + ) -> None: self.validator = validator self.data_type = data_type self.conversion = conversion + async def convert_value(self, value): + if self.conversion: + return self.conversion(value) + return await cg.get_variable(value) + INDEX_TYPES = { "int": IndexType(cv.int_, cg.int_, int), @@ -38,6 +54,12 @@ INDEX_TYPES = { } +class MappingMetaData: + def __init__(self, from_: IndexType, to_: IndexType) -> None: + self.from_ = from_ + self.to_ = to_ + + 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. @@ -60,7 +82,7 @@ BASE_SCHEMA = cv.Schema( ) -def get_object_type(to_): +def get_object_type(to_) -> MockObjClass | None: """ Get the object type from a string. Possible formats: xxx The name of a component which defines INSTANCE_TYPE @@ -81,25 +103,60 @@ def get_object_type(to_): return None +def get_all_mapping_metadata() -> dict[str, MappingMetaData]: + """Get all mapping metadata.""" + return CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_METADATA, {}) + + +def get_mapping_metadata(mapping_id: str) -> MappingMetaData: + """Get mapping metadata by ID for use by other components.""" + return get_all_mapping_metadata()[mapping_id] + + +def add_metadata( + mapping_id: ID, + from_: IndexType, + to_: IndexType, +) -> None: + get_all_mapping_metadata()[mapping_id.id] = MappingMetaData(from_, to_) + + 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") + raise cv.Invalid("an entries dictionary is required for a mapping") entries = config[CONF_ENTRIES] if len(entries) == 0: - raise cv.Invalid("Map must have at least one entry") + raise cv.Invalid("A mapping must have at least one entry") to_ = config[CONF_TO] if to_ in INDEX_TYPES: - value_type = INDEX_TYPES[to_].validator + value_type = INDEX_TYPES[to_] else: - value_type = get_object_type(to_) - if value_type is None: + object_type = get_object_type(to_) + if object_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()} + validator = cv.use_id(object_type) + value_type = IndexType(validator, object_type) + config[CONF_ENTRIES] = {k: value_type.validator(v) for k, v in entries.items()} + if (default_value := config.get(CONF_DEFAULT_VALUE)) is not None: + config[CONF_DEFAULT_VALUE] = value_type.validator(default_value) + unexpected_keys = config.keys() - { + CONF_ENTRIES, + CONF_TO, + CONF_FROM, + CONF_ID, + CONF_DEFAULT_VALUE, + } + if unexpected_keys: + errors = [ + cv.Invalid(f"Unexpected key '{k}'", path=[k]) for k in unexpected_keys + ] + raise cv.MultipleInvalid(errors) + + add_metadata(config[CONF_ID], INDEX_TYPES[config[CONF_FROM]], value_type) return config @@ -107,29 +164,19 @@ 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] + metadata = get_mapping_metadata(varid.id) + entries = { + metadata.from_.conversion(key): await metadata.to_.convert_value(value) + for key, value in config[CONF_ENTRIES].items() + } + value_type = metadata.to_.data_type + # entries guaranteed to be non-empty here. + value_0 = list(entries.values())[0] + if isinstance(value_0, MockObj) and value_0.op != ".": + value_type = value_type.operator("ptr") varid.type = mapping_class.template( - index_type, + metadata.from_.data_type, value_type, ) var = MockObj(varid, ".") @@ -139,4 +186,6 @@ async def to_code(config): for key, value in entries.items(): cg.add(var.set(key, value)) + if (default_value := config.get(CONF_DEFAULT_VALUE)) is not None: + cg.add(var.set_default_value(await metadata.to_.convert_value(default_value))) return var diff --git a/esphome/components/mapping/mapping.h b/esphome/components/mapping/mapping.h index 2b8f0d39b..d6790caa3 100644 --- a/esphome/components/mapping/mapping.h +++ b/esphome/components/mapping/mapping.h @@ -40,6 +40,9 @@ template class Mapping { if (it != this->map_.end()) { return V{it->second}; } + if (this->default_value_.has_value()) { + return this->default_value_.value(); + } if constexpr (std::is_pointer_v) { esph_log_e(TAG, "Key '%p' not found in mapping", key); } else if constexpr (std::is_same_v) { @@ -69,11 +72,17 @@ template class Mapping { if (it != this->map_.end()) { return it->second.c_str(); // safe since value remains in map } + if (this->default_value_.has_value()) { + return this->default_value_.value(); + } return ""; } + void set_default_value(const V &default_value) { this->default_value_ = default_value; } + protected: std::map, RAMAllocator>> map_; + std::optional default_value_{}; }; } // namespace esphome::mapping diff --git a/esphome/loader.py b/esphome/loader.py index 9390b8094..2405fa6f8 100644 --- a/esphome/loader.py +++ b/esphome/loader.py @@ -14,6 +14,7 @@ from typing import Any from esphome.const import SOURCE_FILE_EXTENSIONS from esphome.core import CORE import esphome.core.config +from esphome.cpp_generator import MockObjClass from esphome.types import ConfigType _LOGGER = logging.getLogger(__name__) @@ -93,7 +94,7 @@ class ComponentManifest: return getattr(self.module, "CODEOWNERS", []) @property - def instance_type(self) -> list[str]: + def instance_type(self) -> MockObjClass | None: return getattr(self.module, "INSTANCE_TYPE", None) @property diff --git a/tests/components/mapping/common.yaml b/tests/components/mapping/common.yaml index 7ffcfa4f6..b3db9d54e 100644 --- a/tests/components/mapping/common.yaml +++ b/tests/components/mapping/common.yaml @@ -21,6 +21,7 @@ mapping: entries: clear-night: image_1 sunny: image_2 + default_value: image_1 - id: weather_map_2 from: string to: image @@ -35,6 +36,7 @@ mapping: 2: "two" 3: "three" 77: "seventy-seven" + default_value: unknown - id: string_map from: string to: int diff --git a/tests/components/mapping/test.esp32-idf.yaml b/tests/components/mapping/test.esp32-idf.yaml index a35b6940c..93adcf998 100644 --- a/tests/components/mapping/test.esp32-idf.yaml +++ b/tests/components/mapping/test.esp32-idf.yaml @@ -4,7 +4,7 @@ packages: display: spi_id: spi_bus - platform: ili9xxx + platform: mipi_spi id: main_lcd model: ili9342 cs_pin: 12 diff --git a/tests/components/mapping/test.esp8266-ard.yaml b/tests/components/mapping/test.esp8266-ard.yaml index c59821a21..6a308b67d 100644 --- a/tests/components/mapping/test.esp8266-ard.yaml +++ b/tests/components/mapping/test.esp8266-ard.yaml @@ -4,7 +4,7 @@ packages: display: spi_id: spi_bus - platform: ili9xxx + platform: mipi_spi id: main_lcd model: ili9342 cs_pin: 5 diff --git a/tests/components/mapping/test.rp2040-ard.yaml b/tests/components/mapping/test.rp2040-ard.yaml index fdfed5f6a..01b83c4ab 100644 --- a/tests/components/mapping/test.rp2040-ard.yaml +++ b/tests/components/mapping/test.rp2040-ard.yaml @@ -4,7 +4,7 @@ packages: display: spi_id: spi_bus - platform: ili9xxx + platform: mipi_spi id: main_lcd model: ili9342 data_rate: 31.25MHz