From 9bf53e0ab83e39e594c9151c0bc617972825098a Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Tue, 7 Apr 2026 23:17:58 -0400 Subject: [PATCH 1/7] [esp32_hosted] Add SPI transport and SDIO 1-bit bus width support (#15551) --- esphome/components/esp32_hosted/__init__.py | 264 ++++++++++++++---- .../test-sdio-1bit.esp32-p4-idf.yaml | 13 + .../esp32_hosted/test-spi.esp32-p4-idf.yaml | 15 + 3 files changed, 235 insertions(+), 57 deletions(-) create mode 100644 tests/components/esp32_hosted/test-sdio-1bit.esp32-p4-idf.yaml create mode 100644 tests/components/esp32_hosted/test-spi.esp32-p4-idf.yaml diff --git a/esphome/components/esp32_hosted/__init__.py b/esphome/components/esp32_hosted/__init__.py index 3f9185745dd..1619a845d84 100644 --- a/esphome/components/esp32_hosted/__init__.py +++ b/esphome/components/esp32_hosted/__init__.py @@ -4,95 +4,245 @@ from pathlib import Path from esphome import pins from esphome.components import esp32 import esphome.config_validation as cv -from esphome.const import CONF_CLK_PIN, CONF_RESET_PIN, CONF_VARIANT +from esphome.const import ( + CONF_CLK_PIN, + CONF_CS_PIN, + CONF_FREQUENCY, + CONF_MISO_PIN, + CONF_MOSI_PIN, + CONF_RESET_PIN, + CONF_TYPE, + CONF_VARIANT, +) from esphome.cpp_generator import add_define CODEOWNERS = ["@swoboda1337"] CONF_ACTIVE_HIGH = "active_high" +CONF_BUS_WIDTH = "bus_width" CONF_CMD_PIN = "cmd_pin" CONF_D0_PIN = "d0_pin" CONF_D1_PIN = "d1_pin" CONF_D2_PIN = "d2_pin" CONF_D3_PIN = "d3_pin" -CONF_SLOT = "slot" +CONF_DATA_READY_ACTIVE_HIGH = "data_ready_active_high" +CONF_DATA_READY_PIN = "data_ready_pin" +CONF_HANDSHAKE_ACTIVE_HIGH = "handshake_active_high" +CONF_HANDSHAKE_PIN = "handshake_pin" CONF_SDIO_FREQUENCY = "sdio_frequency" +CONF_SLOT = "slot" +CONF_SPI_MODE = "spi_mode" -CONFIG_SCHEMA = cv.All( - cv.Schema( - { - cv.Required(CONF_VARIANT): cv.one_of(*esp32.VARIANTS, upper=True), - cv.Required(CONF_ACTIVE_HIGH): cv.boolean, - cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_number, - cv.Required(CONF_CMD_PIN): pins.internal_gpio_output_pin_number, - cv.Required(CONF_D0_PIN): pins.internal_gpio_output_pin_number, - cv.Required(CONF_D1_PIN): pins.internal_gpio_output_pin_number, - cv.Required(CONF_D2_PIN): pins.internal_gpio_output_pin_number, - cv.Required(CONF_D3_PIN): pins.internal_gpio_output_pin_number, - cv.Required(CONF_RESET_PIN): pins.internal_gpio_output_pin_number, - cv.Optional(CONF_SLOT, default=1): cv.int_range(min=0, max=1), - cv.Optional(CONF_SDIO_FREQUENCY, default="40MHz"): cv.All( - cv.frequency, cv.Range(min=400e3, max=50e6) - ), - } - ), +# Shared fields for both transport modes +BASE_SCHEMA = cv.Schema( + { + cv.Required(CONF_VARIANT): cv.one_of(*esp32.VARIANTS, upper=True), + cv.Required(CONF_ACTIVE_HIGH): cv.boolean, + cv.Required(CONF_RESET_PIN): pins.internal_gpio_output_pin_number, + } +) + +SDIO_SCHEMA = BASE_SCHEMA.extend( + { + cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_CMD_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_D0_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_D1_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_D2_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_D3_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_BUS_WIDTH, default=4): cv.one_of(1, 4, int=True), + cv.Optional(CONF_SLOT, default=1): cv.int_range(min=0, max=1), + cv.Optional(CONF_SDIO_FREQUENCY, default="40MHz"): cv.All( + cv.frequency, cv.Range(min=400e3, max=50e6) + ), + } ) -async def to_code(config): - add_define("USE_ESP32_HOSTED") - if config[CONF_ACTIVE_HIGH]: - esp32.add_idf_sdkconfig_option( - "CONFIG_ESP_HOSTED_SDIO_RESET_ACTIVE_HIGH", - True, +def _validate_sdio(config): + if config[CONF_BUS_WIDTH] == 4: + for pin in (CONF_D1_PIN, CONF_D2_PIN, CONF_D3_PIN): + if pin not in config: + raise cv.Invalid( + f"{pin} is required when bus_width is 4", + path=[pin], + ) + return config + + +# SPI variant-dependent defaults and limits +_SPI_VARIANT_DEFAULTS = { + "ESP32": {"spi_mode": 2, "frequency": 10, "max_frequency": 10}, + "ESP32C6": {"spi_mode": 3, "frequency": 26, "max_frequency": 40}, +} +_SPI_DEFAULT = {"spi_mode": 3, "frequency": 40, "max_frequency": 40} + +SPI_SCHEMA = BASE_SCHEMA.extend( + { + cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_MOSI_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_MISO_PIN): pins.internal_gpio_input_pin_number, + cv.Required(CONF_CS_PIN): pins.internal_gpio_output_pin_number, + cv.Required(CONF_HANDSHAKE_PIN): pins.internal_gpio_input_pin_number, + cv.Required(CONF_DATA_READY_PIN): pins.internal_gpio_input_pin_number, + cv.Optional(CONF_SPI_MODE): cv.int_range(min=0, max=3), + cv.Optional(CONF_FREQUENCY): cv.All(cv.frequency, cv.Range(min=1e6, max=40e6)), + cv.Optional(CONF_HANDSHAKE_ACTIVE_HIGH, default=True): cv.boolean, + cv.Optional(CONF_DATA_READY_ACTIVE_HIGH, default=True): cv.boolean, + } +) + + +def _validate_spi(config): + variant = config[CONF_VARIANT] + defaults = _SPI_VARIANT_DEFAULTS.get(variant, _SPI_DEFAULT) + + if CONF_SPI_MODE not in config: + config[CONF_SPI_MODE] = defaults["spi_mode"] + + if CONF_FREQUENCY not in config: + config[CONF_FREQUENCY] = float(defaults["frequency"] * 1e6) + + freq_mhz = int(config[CONF_FREQUENCY] // 1e6) + if freq_mhz > defaults["max_frequency"]: + raise cv.Invalid( + f"SPI frequency {freq_mhz}MHz exceeds maximum {defaults['max_frequency']}MHz for {variant}", + path=[CONF_FREQUENCY], ) + return config + + +CONFIG_SCHEMA = cv.typed_schema( + { + "sdio": cv.All(SDIO_SCHEMA, _validate_sdio), + "spi": cv.All(SPI_SCHEMA, _validate_spi), + }, + default_type="sdio", +) + + +def _configure_sdio(config): + slot = config[CONF_SLOT] + esp32.add_idf_sdkconfig_option( + f"CONFIG_ESP_HOSTED_SDIO_SLOT_{slot}", + True, + ) + if config[CONF_BUS_WIDTH] == 1: + esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_SDIO_1_BIT_BUS", True) else: - esp32.add_idf_sdkconfig_option( - "CONFIG_ESP_HOSTED_SDIO_RESET_ACTIVE_LOW", - True, - ) + esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_SDIO_4_BIT_BUS", True) esp32.add_idf_sdkconfig_option( - "CONFIG_ESP_HOSTED_SDIO_GPIO_RESET_SLAVE", # NOLINT - config[CONF_RESET_PIN], - ) - esp32.add_idf_sdkconfig_option( - f"CONFIG_SLAVE_IDF_TARGET_{config[CONF_VARIANT]}", # NOLINT - True, - ) - esp32.add_idf_sdkconfig_option( - f"CONFIG_ESP_HOSTED_SDIO_SLOT_{config[CONF_SLOT]}", - True, - ) - esp32.add_idf_sdkconfig_option( - f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CLK_SLOT_{config[CONF_SLOT]}", + f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CLK_SLOT_{slot}", config[CONF_CLK_PIN], ) esp32.add_idf_sdkconfig_option( - f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CMD_SLOT_{config[CONF_SLOT]}", + f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CMD_SLOT_{slot}", config[CONF_CMD_PIN], ) esp32.add_idf_sdkconfig_option( - f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D0_SLOT_{config[CONF_SLOT]}", + f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D0_SLOT_{slot}", config[CONF_D0_PIN], ) - esp32.add_idf_sdkconfig_option( - f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D1_4BIT_BUS_SLOT_{config[CONF_SLOT]}", - config[CONF_D1_PIN], - ) - esp32.add_idf_sdkconfig_option( - f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D2_4BIT_BUS_SLOT_{config[CONF_SLOT]}", - config[CONF_D2_PIN], - ) - esp32.add_idf_sdkconfig_option( - f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D3_4BIT_BUS_SLOT_{config[CONF_SLOT]}", - config[CONF_D3_PIN], - ) + if config[CONF_BUS_WIDTH] == 4: + esp32.add_idf_sdkconfig_option( + f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D1_4BIT_BUS_SLOT_{slot}", + config[CONF_D1_PIN], + ) + esp32.add_idf_sdkconfig_option( + f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D2_4BIT_BUS_SLOT_{slot}", + config[CONF_D2_PIN], + ) + esp32.add_idf_sdkconfig_option( + f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D3_4BIT_BUS_SLOT_{slot}", + config[CONF_D3_PIN], + ) esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_CUSTOM_SDIO_PINS", True) esp32.add_idf_sdkconfig_option( "CONFIG_ESP_HOSTED_SDIO_CLOCK_FREQ_KHZ", int(config[CONF_SDIO_FREQUENCY] // 1000), ) + +def _configure_spi(config): + esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_SPI_HOST_INTERFACE", True) + # SPI mode is set via per-variant choice options + variant = config[CONF_VARIANT] + mode = config[CONF_SPI_MODE] + suffix = "ESP32" if variant == "ESP32" else "ESP32XX" + esp32.add_idf_sdkconfig_option( + f"CONFIG_ESP_HOSTED_SPI_PRIV_MODE_{mode}_{suffix}", + True, + ) + # Frequency is set via per-variant options + freq_mhz = int(config[CONF_FREQUENCY] // 1e6) + if variant == "ESP32": + esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_SPI_FREQ_ESP32", freq_mhz) + elif variant == "ESP32C6": + esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_SPI_FREQ_ESP32C6", freq_mhz) + else: + esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_SPI_FREQ_ESP32XX", freq_mhz) + # Pin configuration (use HSPI variant as P4/H2 hosts don't have VSPI) + esp32.add_idf_sdkconfig_option( + "CONFIG_ESP_HOSTED_SPI_HSPI_GPIO_MOSI", config[CONF_MOSI_PIN] + ) + esp32.add_idf_sdkconfig_option( + "CONFIG_ESP_HOSTED_SPI_HSPI_GPIO_MISO", config[CONF_MISO_PIN] + ) + esp32.add_idf_sdkconfig_option( + "CONFIG_ESP_HOSTED_SPI_HSPI_GPIO_CLK", config[CONF_CLK_PIN] + ) + esp32.add_idf_sdkconfig_option( + "CONFIG_ESP_HOSTED_SPI_HSPI_GPIO_CS", config[CONF_CS_PIN] + ) + esp32.add_idf_sdkconfig_option( + "CONFIG_ESP_HOSTED_SPI_GPIO_HANDSHAKE", config[CONF_HANDSHAKE_PIN] + ) + esp32.add_idf_sdkconfig_option( + "CONFIG_ESP_HOSTED_SPI_GPIO_DATA_READY", config[CONF_DATA_READY_PIN] + ) + # Handshake and data_ready polarity + if config[CONF_HANDSHAKE_ACTIVE_HIGH]: + esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_HS_ACTIVE_HIGH", True) + else: + esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_HS_ACTIVE_LOW", True) + if config[CONF_DATA_READY_ACTIVE_HIGH]: + esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_DR_ACTIVE_HIGH", True) + else: + esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_DR_ACTIVE_LOW", True) + + +async def to_code(config): + add_define("USE_ESP32_HOSTED") + transport = config[CONF_TYPE] + transport_prefix = "SDIO" if transport == "sdio" else "SPI" + + # Reset polarity + if config[CONF_ACTIVE_HIGH]: + esp32.add_idf_sdkconfig_option( + f"CONFIG_ESP_HOSTED_{transport_prefix}_RESET_ACTIVE_HIGH", True + ) + else: + esp32.add_idf_sdkconfig_option( + f"CONFIG_ESP_HOSTED_{transport_prefix}_RESET_ACTIVE_LOW", True + ) + # Reset GPIO + esp32.add_idf_sdkconfig_option( + f"CONFIG_ESP_HOSTED_{transport_prefix}_GPIO_RESET_SLAVE", # NOLINT + config[CONF_RESET_PIN], + ) + # Slave variant # NOLINT + esp32.add_idf_sdkconfig_option( + f"CONFIG_SLAVE_IDF_TARGET_{config[CONF_VARIANT]}", # NOLINT + True, + ) + + # Transport-specific configuration + if transport == "sdio": + _configure_sdio(config) + else: + _configure_spi(config) + + # Library versions idf_ver = esp32.idf_version() os.environ["ESP_IDF_VERSION"] = f"{idf_ver.major}.{idf_ver.minor}" if idf_ver >= cv.Version(5, 5, 0): diff --git a/tests/components/esp32_hosted/test-sdio-1bit.esp32-p4-idf.yaml b/tests/components/esp32_hosted/test-sdio-1bit.esp32-p4-idf.yaml new file mode 100644 index 00000000000..80f166c0575 --- /dev/null +++ b/tests/components/esp32_hosted/test-sdio-1bit.esp32-p4-idf.yaml @@ -0,0 +1,13 @@ +esp32_hosted: + variant: ESP32C6 + slot: 1 + bus_width: 1 + active_high: true + reset_pin: GPIO15 + cmd_pin: GPIO13 + clk_pin: GPIO12 + d0_pin: GPIO11 + +wifi: + ssid: MySSID + password: password1 diff --git a/tests/components/esp32_hosted/test-spi.esp32-p4-idf.yaml b/tests/components/esp32_hosted/test-spi.esp32-p4-idf.yaml new file mode 100644 index 00000000000..a4423140de0 --- /dev/null +++ b/tests/components/esp32_hosted/test-spi.esp32-p4-idf.yaml @@ -0,0 +1,15 @@ +esp32_hosted: + type: spi + variant: ESP32C6 + active_high: true + reset_pin: GPIO15 + handshake_pin: GPIO54 + data_ready_pin: GPIO14 + miso_pin: GPIO10 + mosi_pin: GPIO11 + clk_pin: GPIO9 + cs_pin: GPIO53 + +wifi: + ssid: MySSID + password: password1 From 43183c33ba3e5cea7e7fb10e67ff0d932584052d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Apr 2026 19:27:16 -1000 Subject: [PATCH 2/7] [esp32_ble_tracker] Wrap continuous bool via cg.templatable for TemplatableFn --- esphome/components/esp32_ble_tracker/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/esp32_ble_tracker/__init__.py b/esphome/components/esp32_ble_tracker/__init__.py index b9c4c28ccfd..d758b400c4f 100644 --- a/esphome/components/esp32_ble_tracker/__init__.py +++ b/esphome/components/esp32_ble_tracker/__init__.py @@ -378,7 +378,8 @@ async def esp32_ble_tracker_start_scan_action_to_code( ): paren = await cg.get_variable(config[CONF_ID]) var = cg.new_Pvariable(action_id, template_arg, paren) - cg.add(var.set_continuous(config[CONF_CONTINUOUS])) + template_ = await cg.templatable(config[CONF_CONTINUOUS], args, cg.bool_) + cg.add(var.set_continuous(template_)) return var From 03d0ff18618e7e6e7a4bda938183a6dc766fddaf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Apr 2026 19:27:31 -1000 Subject: [PATCH 3/7] Update esphome/cpp_generator.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/cpp_generator.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index f9330508c8c..c41171257b9 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -823,9 +823,11 @@ async def templatable( """Generate code for a templatable config option. If `value` is a templated value, the lambda expression is returned. - For std::string output, constants are returned as-is (with PROGMEM wrapping). + For std::string output, constants are returned as-is (with PROGMEM wrapping), + using the std::string-specific TemplatableValue specialization. For all other output types, constants are wrapped in stateless lambdas - so that TemplatableValue can store them as function pointers. + so that TemplatableFn-backed macro-generated fields can store them as + function pointers. :param value: The value to process. :param args: The arguments for the lambda expression. From e7ab67c0312837fc4476764e9a22855d67a6ec5c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Apr 2026 19:43:04 -1000 Subject: [PATCH 4/7] [multiple] Fix raw value codegen for TEMPLATABLE_VALUE setters - max7219digit: wrap bool state via cg.templatable - number: wrap operation enum and cycle bool in deprecated CONF_MODE path - select: wrap operation enum and cycle bool in deprecated CONF_MODE path - datetime: wrap ESPTime struct initializers via cg.templatable - display: wrap DisplayPage* pointer via cg.templatable - speaker/media_player: wrap AudioFile* pointer via cg.templatable --- esphome/components/datetime/__init__.py | 9 ++++++--- esphome/components/display/__init__.py | 3 ++- esphome/components/max7219digit/display.py | 9 ++++++--- esphome/components/number/__init__.py | 8 ++++++-- esphome/components/select/__init__.py | 8 ++++++-- esphome/components/speaker/media_player/__init__.py | 3 ++- 6 files changed, 28 insertions(+), 12 deletions(-) diff --git a/esphome/components/datetime/__init__.py b/esphome/components/datetime/__init__.py index 90835624bf1..895ac4e243e 100644 --- a/esphome/components/datetime/__init__.py +++ b/esphome/components/datetime/__init__.py @@ -204,7 +204,8 @@ async def datetime_date_set_to_code(config, action_id, template_arg, args): ("month", date_config[CONF_MONTH]), ("year", date_config[CONF_YEAR]), ) - cg.add(action_var.set_date(date_struct)) + template_ = await cg.templatable(date_struct, args, cg.ESPTime) + cg.add(action_var.set_date(template_)) return action_var @@ -236,7 +237,8 @@ async def datetime_time_set_to_code(config, action_id, template_arg, args): ("minute", time_config[CONF_MINUTE]), ("hour", time_config[CONF_HOUR]), ) - cg.add(action_var.set_time(time_struct)) + template_ = await cg.templatable(time_struct, args, cg.ESPTime) + cg.add(action_var.set_time(template_)) return action_var @@ -271,5 +273,6 @@ async def datetime_datetime_set_to_code(config, action_id, template_arg, args): ("month", datetime_config[CONF_MONTH]), ("year", datetime_config[CONF_YEAR]), ) - cg.add(action_var.set_datetime(datetime_struct)) + template_ = await cg.templatable(datetime_struct, args, cg.ESPTime) + cg.add(action_var.set_datetime(template_)) return action_var diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index 67d76a59d9d..744b5d16c49 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -207,7 +207,8 @@ async def display_page_show_to_code(config, action_id, template_arg, args): cg.add(var.set_page(template_)) else: paren = await cg.get_variable(config[CONF_ID]) - cg.add(var.set_page(paren)) + template_ = await cg.templatable(paren, args, DisplayPagePtr) + cg.add(var.set_page(template_)) return var diff --git a/esphome/components/max7219digit/display.py b/esphome/components/max7219digit/display.py index eb751b995d1..df2423b0d0e 100644 --- a/esphome/components/max7219digit/display.py +++ b/esphome/components/max7219digit/display.py @@ -147,7 +147,8 @@ MAX7219_ON_ACTION_SCHEMA = automation.maybe_simple_id( async def max7219digit_invert_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - cg.add(var.set_state(config[CONF_STATE])) + template_ = await cg.templatable(config[CONF_STATE], args, cg.bool_) + cg.add(var.set_state(template_)) return var @@ -166,7 +167,8 @@ async def max7219digit_invert_to_code(config, action_id, template_arg, args): async def max7219digit_visible_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - cg.add(var.set_state(config[CONF_STATE])) + template_ = await cg.templatable(config[CONF_STATE], args, cg.bool_) + cg.add(var.set_state(template_)) return var @@ -185,7 +187,8 @@ async def max7219digit_visible_to_code(config, action_id, template_arg, args): async def max7219digit_reverse_to_code(config, action_id, template_arg, args): var = cg.new_Pvariable(action_id, template_arg) await cg.register_parented(var, config[CONF_ID]) - cg.add(var.set_state(config[CONF_STATE])) + template_ = await cg.templatable(config[CONF_STATE], args, cg.bool_) + cg.add(var.set_state(template_)) return var diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 9fbaff68601..c8441002586 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -448,7 +448,11 @@ async def number_to_to_code(config, action_id, template_arg, args): template_ = await cg.templatable(cycle, args, bool) cg.add(var.set_cycle(template_)) if (mode := config.get(CONF_MODE)) is not None: - cg.add(var.set_operation(NUMBER_OPERATION_OPTIONS[mode])) + template_ = await cg.templatable( + NUMBER_OPERATION_OPTIONS[mode], args, NumberOperation + ) + cg.add(var.set_operation(template_)) if (cycle := config.get(CONF_CYCLE)) is not None: - cg.add(var.set_cycle(cycle)) + template_ = await cg.templatable(cycle, args, cg.bool_) + cg.add(var.set_cycle(template_)) return var diff --git a/esphome/components/select/__init__.py b/esphome/components/select/__init__.py index b2c17f59ac1..8c7c8f00fa1 100644 --- a/esphome/components/select/__init__.py +++ b/esphome/components/select/__init__.py @@ -282,7 +282,11 @@ async def select_operation_to_code(config, action_id, template_arg, args): template_ = await cg.templatable(cycle, args, bool) cg.add(var.set_cycle(template_)) if (mode := config.get(CONF_MODE)) is not None: - cg.add(var.set_operation(SELECT_OPERATION_OPTIONS[mode])) + template_ = await cg.templatable( + SELECT_OPERATION_OPTIONS[mode], args, SelectOperation + ) + cg.add(var.set_operation(template_)) if (cycle := config.get(CONF_CYCLE)) is not None: - cg.add(var.set_cycle(cycle)) + template_ = await cg.templatable(cycle, args, cg.bool_) + cg.add(var.set_cycle(template_)) return var diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index b16f882cbad..320e96c8979 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -516,7 +516,8 @@ async def play_on_device_media_media_action(config, action_id, template_arg, arg announcement = await cg.templatable(config[CONF_ANNOUNCEMENT], args, cg.bool_) enqueue = await cg.templatable(config[CONF_ENQUEUE], args, cg.bool_) - cg.add(var.set_audio_file(media_file)) + template_ = await cg.templatable(media_file, args, audio.AudioFile.operator("ptr")) + cg.add(var.set_audio_file(template_)) cg.add(var.set_announcement(announcement)) cg.add(var.set_enqueue(enqueue)) return var From e22ded38036074dac72a2ce3a156bfd573179765 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Apr 2026 19:47:43 -1000 Subject: [PATCH 5/7] more fixes --- .../esp32_ble_server/ble_server_automations.h | 8 ++++++-- esphome/core/automation.h | 10 +++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/esphome/components/esp32_ble_server/ble_server_automations.h b/esphome/components/esp32_ble_server/ble_server_automations.h index 0bbfdffd5bd..0c39a40dbb4 100644 --- a/esphome/components/esp32_ble_server/ble_server_automations.h +++ b/esphome/components/esp32_ble_server/ble_server_automations.h @@ -69,7 +69,8 @@ class BLECharacteristicSetValueActionManager { template class BLECharacteristicSetValueAction : public Action { public: BLECharacteristicSetValueAction(BLECharacteristic *characteristic) : parent_(characteristic) {} - TEMPLATABLE_VALUE(std::vector, buffer) + // TemplatableValue (not TemplatableFn) — also set from C++ with raw values (initializer_list, ByteBuffer) + template void set_buffer(V buffer) { this->buffer_ = buffer; } void set_buffer(std::initializer_list buffer) { this->buffer_ = std::vector(buffer); } void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); } void play(const Ts &...x) override { @@ -90,6 +91,7 @@ template class BLECharacteristicSetValueAction : public Action, Ts...> buffer_{}; }; #endif // USE_ESP32_BLE_SERVER_SET_VALUE_ACTION @@ -115,13 +117,15 @@ template class BLECharacteristicNotifyAction : public Action class BLEDescriptorSetValueAction : public Action { public: BLEDescriptorSetValueAction(BLEDescriptor *descriptor) : parent_(descriptor) {} - TEMPLATABLE_VALUE(std::vector, buffer) + // TemplatableValue (not TemplatableFn) — also set from C++ with raw values (initializer_list, ByteBuffer) + template void set_buffer(V buffer) { this->buffer_ = buffer; } void set_buffer(std::initializer_list buffer) { this->buffer_ = std::vector(buffer); } void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); } void play(const Ts &...x) override { this->parent_->set_value(this->buffer_.value(x...)); } protected: BLEDescriptor *parent_; + TemplatableValue, Ts...> buffer_{}; }; #endif // USE_ESP32_BLE_SERVER_DESCRIPTOR_SET_VALUE_ACTION diff --git a/esphome/core/automation.h b/esphome/core/automation.h index 7d5981c3b85..9f62f8fc4ee 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -80,11 +80,11 @@ template class TemplatableFn { // Forward declaration for TemplatableValue (string specialization needs it) template class TemplatableValue; -/// Selects TemplatableFn (4 bytes) for non-string types, TemplatableValue (8 bytes) for std::string. -/// std::string needs TemplatableValue for const char*, __FlashStringHelper*, and PROGMEM support. -template -using TemplatableStorage = - std::conditional_t, TemplatableValue, TemplatableFn>; +/// TemplatableStorage uses TemplatableValue (8 bytes) for the TEMPLATABLE_VALUE macro. +/// Many components pass raw constants to macro-generated setters from codegen, so the +/// macro must accept both raw values and function pointers. Components that want the +/// 4-byte savings can use TemplatableFn directly instead of the macro. +template using TemplatableStorage = TemplatableValue; #define TEMPLATABLE_VALUE_(type, name) \ protected: \ From ee83f525c41b152f3fa293abe8b8732e5e5f1d82 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Apr 2026 19:51:52 -1000 Subject: [PATCH 6/7] [core] Use is_trivially_copyable_v for TemplatableStorage selection TemplatableStorage now selects TemplatableFn (4 bytes) for trivially copyable types and TemplatableValue (8 bytes) for non-trivial types. This automatically handles std::string (PROGMEM support) and std::vector (raw value assignment from C++) without special-casing. Reverts esp32_ble_server back to TEMPLATABLE_VALUE macro since std::vector now correctly gets TemplatableValue. --- .../esp32_ble_server/ble_server_automations.h | 8 ++------ esphome/core/automation.h | 11 ++++++----- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/esphome/components/esp32_ble_server/ble_server_automations.h b/esphome/components/esp32_ble_server/ble_server_automations.h index 0c39a40dbb4..0bbfdffd5bd 100644 --- a/esphome/components/esp32_ble_server/ble_server_automations.h +++ b/esphome/components/esp32_ble_server/ble_server_automations.h @@ -69,8 +69,7 @@ class BLECharacteristicSetValueActionManager { template class BLECharacteristicSetValueAction : public Action { public: BLECharacteristicSetValueAction(BLECharacteristic *characteristic) : parent_(characteristic) {} - // TemplatableValue (not TemplatableFn) — also set from C++ with raw values (initializer_list, ByteBuffer) - template void set_buffer(V buffer) { this->buffer_ = buffer; } + TEMPLATABLE_VALUE(std::vector, buffer) void set_buffer(std::initializer_list buffer) { this->buffer_ = std::vector(buffer); } void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); } void play(const Ts &...x) override { @@ -91,7 +90,6 @@ template class BLECharacteristicSetValueAction : public Action, Ts...> buffer_{}; }; #endif // USE_ESP32_BLE_SERVER_SET_VALUE_ACTION @@ -117,15 +115,13 @@ template class BLECharacteristicNotifyAction : public Action class BLEDescriptorSetValueAction : public Action { public: BLEDescriptorSetValueAction(BLEDescriptor *descriptor) : parent_(descriptor) {} - // TemplatableValue (not TemplatableFn) — also set from C++ with raw values (initializer_list, ByteBuffer) - template void set_buffer(V buffer) { this->buffer_ = buffer; } + TEMPLATABLE_VALUE(std::vector, buffer) void set_buffer(std::initializer_list buffer) { this->buffer_ = std::vector(buffer); } void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); } void play(const Ts &...x) override { this->parent_->set_value(this->buffer_.value(x...)); } protected: BLEDescriptor *parent_; - TemplatableValue, Ts...> buffer_{}; }; #endif // USE_ESP32_BLE_SERVER_DESCRIPTOR_SET_VALUE_ACTION diff --git a/esphome/core/automation.h b/esphome/core/automation.h index 9f62f8fc4ee..5a29d61857c 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -80,11 +80,12 @@ template class TemplatableFn { // Forward declaration for TemplatableValue (string specialization needs it) template class TemplatableValue; -/// TemplatableStorage uses TemplatableValue (8 bytes) for the TEMPLATABLE_VALUE macro. -/// Many components pass raw constants to macro-generated setters from codegen, so the -/// macro must accept both raw values and function pointers. Components that want the -/// 4-byte savings can use TemplatableFn directly instead of the macro. -template using TemplatableStorage = TemplatableValue; +/// Selects TemplatableFn (4 bytes) for trivially copyable types, TemplatableValue (8 bytes) otherwise. +/// Non-trivial types (std::string, std::vector, etc.) need TemplatableValue for raw value +/// storage, PROGMEM/FlashStringHelper support (strings), and proper copy/move/destruction. +template +using TemplatableStorage = + std::conditional_t, TemplatableFn, TemplatableValue>; #define TEMPLATABLE_VALUE_(type, name) \ protected: \ From 49b2882f2b9aad9039447d7e586c8f35b46c9756 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Apr 2026 19:56:41 -1000 Subject: [PATCH 7/7] [core] Fix TemplatableValue union for non-trivially-constructible types Add explicit Storage union with trivial ctor/dtor so that TemplatableValue works with non-trivially-constructible types like std::vector. The union's value_ lifetime is managed externally via placement new and destroy_(). Reverts esp32_ble_server back to TEMPLATABLE_VALUE macro. --- esphome/core/automation.h | 39 +++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/esphome/core/automation.h b/esphome/core/automation.h index 5a29d61857c..a574872bb23 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -105,11 +105,13 @@ template class TemplatableValue { // Accept raw constants template TemplatableValue(V value) requires(!std::invocable) : tag_(VALUE) { - new (&this->value_) T(static_cast(std::move(value))); + new (&this->storage_.value_) T(static_cast(std::move(value))); } // Accept stateless lambdas (convertible to function pointer) - template TemplatableValue(F f) requires std::convertible_to : tag_(FN) { this->f_ = f; } + template TemplatableValue(F f) requires std::convertible_to : tag_(FN) { + this->storage_.f_ = f; + } // Convertible return type (e.g., int -> uint8_t) — casting trampoline template @@ -117,7 +119,7 @@ template class TemplatableValue { "codegen")]] TemplatableValue(F) requires(!std::convertible_to) && std::invocable &&std::convertible_to, T> &&std::is_empty_v &&std::default_initializable : tag_(FN) { - this->f_ = [](X... x) -> T { return static_cast(F{}(x...)); }; + this->storage_.f_ = [](X... x) -> T { return static_cast(F{}(x...)); }; } // Reject any callable that didn't match the above @@ -129,18 +131,18 @@ template class TemplatableValue { TemplatableValue(const TemplatableValue &other) : tag_(other.tag_) { if (this->tag_ == VALUE) { - new (&this->value_) T(other.value_); + new (&this->storage_.value_) T(other.storage_.value_); } else if (this->tag_ == FN) { - this->f_ = other.f_; + this->storage_.f_ = other.storage_.f_; } } TemplatableValue(TemplatableValue &&other) noexcept : tag_(other.tag_) { if (this->tag_ == VALUE) { - new (&this->value_) T(std::move(other.value_)); + new (&this->storage_.value_) T(std::move(other.storage_.value_)); other.destroy_(); } else if (this->tag_ == FN) { - this->f_ = other.f_; + this->storage_.f_ = other.storage_.f_; } other.tag_ = NONE; } @@ -150,9 +152,9 @@ template class TemplatableValue { this->destroy_(); this->tag_ = other.tag_; if (this->tag_ == VALUE) { - new (&this->value_) T(other.value_); + new (&this->storage_.value_) T(other.storage_.value_); } else if (this->tag_ == FN) { - this->f_ = other.f_; + this->storage_.f_ = other.storage_.f_; } } return *this; @@ -163,10 +165,10 @@ template class TemplatableValue { this->destroy_(); this->tag_ = other.tag_; if (this->tag_ == VALUE) { - new (&this->value_) T(std::move(other.value_)); + new (&this->storage_.value_) T(std::move(other.storage_.value_)); other.destroy_(); } else if (this->tag_ == FN) { - this->f_ = other.f_; + this->storage_.f_ = other.storage_.f_; } other.tag_ = NONE; } @@ -179,9 +181,9 @@ template class TemplatableValue { T value(X... x) const { if (this->tag_ == FN) - return this->f_(x...); + return this->storage_.f_(x...); if (this->tag_ == VALUE) - return this->value_; + return this->storage_.value_; return T{}; } @@ -201,15 +203,20 @@ template class TemplatableValue { void destroy_() { if constexpr (!std::is_trivially_destructible_v) { if (this->tag_ == VALUE) - this->value_.~T(); + this->storage_.value_.~T(); } } enum Tag : uint8_t { NONE, VALUE, FN } tag_{NONE}; - union { + // Union with explicit ctor/dtor to support non-trivially-constructible/destructible T + // (e.g., std::vector). Lifetime of value_ is managed externally via + // placement new and destroy_(). + union Storage { + constexpr Storage() : f_(nullptr) {} + constexpr ~Storage() {} T value_; T (*f_)(X...); - }; + } storage_; }; /// Specialization for std::string: supports VALUE, STATIC_STRING, FLASH_STRING,