1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-18 15:55:46 +00:00

Compare commits

...

19 Commits

Author SHA1 Message Date
J. Nick Koston
3636ab68f3 tidy 2025-10-29 15:06:20 -05:00
J. Nick Koston
b30c4e716f Revert "remove tests to get baseline"
This reverts commit 658c50e0c6.
2025-10-29 14:55:15 -05:00
J. Nick Koston
658c50e0c6 remove tests to get baseline 2025-10-29 14:45:50 -05:00
J. Nick Koston
399b86255a [template] Add regression tests for lambdas with captures (PR #11555) 2025-10-29 14:35:03 -05:00
J. Nick Koston
c38a558df8 fix template regression 2025-10-29 14:26:33 -05:00
J. Nick Koston
299c937e67 fix template regression 2025-10-29 14:24:02 -05:00
J. Nick Koston
b6516c687d fix template regression 2025-10-29 14:21:34 -05:00
Javier Peletier
f18c70a256 [core] Fix substitution id redefinition false positive (#11603) 2025-10-30 07:06:55 +13:00
Jonathan Swoboda
6fb490f49b [remote_transmitter] Add non-blocking mode (#11524) 2025-10-29 12:40:22 -04:00
J. Nick Koston
66cf7c3a3b [lvgl] Fix nested lambdas in automations unable to access parameters (#11583)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2025-10-29 17:07:48 +11:00
dependabot[bot]
f29021b5ef Bump ruff from 0.14.1 to 0.14.2 (#11519)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-10-29 05:23:42 +00:00
dependabot[bot]
7549ca4d39 Bump actions/download-artifact from 5.0.0 to 6.0.0 (#11521)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-29 00:20:13 -05:00
dependabot[bot]
33e7a2101b Bump actions/upload-artifact from 4.6.2 to 5.0.0 (#11520)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-29 00:20:05 -05:00
dependabot[bot]
59a216bfcb Bump github/codeql-action from 4.30.9 to 4.31.0 (#11522)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-29 00:19:47 -05:00
Jesse Hills
09d89000ad [core] Remove deprecated schema constants (#11591) 2025-10-29 00:14:02 -05:00
Kent Gibson
b6c9ece0e6 template_alarm_control_panel readability improvements (#11593) 2025-10-29 00:10:36 -05:00
dependabot[bot]
7169556722 Bump aioesphomeapi from 42.4.0 to 42.5.0 (#11597)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-29 04:46:47 +00:00
J. Nick Koston
f6e4c0cb52 [ci] Fix component tests not running when only test files change (#11580) 2025-10-29 16:22:28 +13:00
J. Nick Koston
f3634edc22 [select] Store options in flash to reduce RAM usage (#11514) 2025-10-29 15:28:16 +13:00
85 changed files with 674 additions and 377 deletions

View File

@@ -62,7 +62,7 @@ jobs:
run: git diff
- if: failure()
name: Archive artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: generated-proto-files
path: |

View File

@@ -849,7 +849,7 @@ jobs:
fi
- name: Upload memory analysis JSON
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: memory-analysis-target
path: memory-analysis-target.json
@@ -913,7 +913,7 @@ jobs:
--platform "$platform"
- name: Upload memory analysis JSON
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: memory-analysis-pr
path: memory-analysis-pr.json
@@ -943,13 +943,13 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Download target analysis JSON
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: memory-analysis-target
path: ./memory-analysis
continue-on-error: true
- name: Download PR analysis JSON
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: memory-analysis-pr
path: ./memory-analysis

View File

@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
uses: github/codeql-action/init@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
uses: github/codeql-action/analyze@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
with:
category: "/language:${{matrix.language}}"

View File

@@ -138,7 +138,7 @@ jobs:
# version: ${{ needs.init.outputs.tag }}
- name: Upload digests
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: digests-${{ matrix.platform.arch }}
path: /tmp/digests
@@ -171,7 +171,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Download digests
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
pattern: digests-*
path: /tmp/digests

View File

@@ -11,7 +11,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.14.1
rev: v0.14.2
hooks:
# Run the linter.
- id: ruff

View File

@@ -172,12 +172,6 @@ def alarm_control_panel_schema(
return _ALARM_CONTROL_PANEL_SCHEMA.extend(schema)
# Remove before 2025.11.0
ALARM_CONTROL_PANEL_SCHEMA = alarm_control_panel_schema(AlarmControlPanel)
ALARM_CONTROL_PANEL_SCHEMA.add_extra(
cv.deprecated_schema_constant("alarm_control_panel")
)
ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id(
{
cv.GenerateID(): cv.use_id(AlarmControlPanel),

View File

@@ -1143,7 +1143,7 @@ message ListEntitiesSelectResponse {
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
repeated string options = 6 [(container_pointer) = "std::vector"];
repeated string options = 6 [(container_pointer_no_template) = "FixedVector<const char *>"];
bool disabled_by_default = 7;
EntityCategory entity_category = 8;
uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];

View File

@@ -1475,8 +1475,8 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const {
#ifdef USE_ENTITY_ICON
buffer.encode_string(5, this->icon_ref_);
#endif
for (const auto &it : *this->options) {
buffer.encode_string(6, it, true);
for (const char *it : *this->options) {
buffer.encode_string(6, it, strlen(it), true);
}
buffer.encode_bool(7, this->disabled_by_default);
buffer.encode_uint32(8, static_cast<uint32_t>(this->entity_category));
@@ -1492,8 +1492,8 @@ void ListEntitiesSelectResponse::calculate_size(ProtoSize &size) const {
size.add_length(1, this->icon_ref_.size());
#endif
if (!this->options->empty()) {
for (const auto &it : *this->options) {
size.add_length_force(1, it.size());
for (const char *it : *this->options) {
size.add_length_force(1, strlen(it));
}
}
size.add_bool(1, this->disabled_by_default);

View File

@@ -1534,7 +1534,7 @@ class ListEntitiesSelectResponse final : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_select_response"; }
#endif
const std::vector<std::string> *options{};
const FixedVector<const char *> *options{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP

View File

@@ -88,6 +88,12 @@ static void dump_field(std::string &out, const char *field_name, StringRef value
out.append("\n");
}
static void dump_field(std::string &out, const char *field_name, const char *value, int indent = 2) {
append_field_prefix(out, field_name, indent);
out.append("'").append(value).append("'");
out.append("\n");
}
template<typename T> static void dump_field(std::string &out, const char *field_name, T value, int indent = 2) {
append_field_prefix(out, field_name, indent);
out.append(proto_enum_to_string<T>(value));

View File

@@ -548,11 +548,6 @@ def binary_sensor_schema(
return _BINARY_SENSOR_SCHEMA.extend(schema)
# Remove before 2025.11.0
BINARY_SENSOR_SCHEMA = binary_sensor_schema()
BINARY_SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("binary_sensor"))
async def setup_binary_sensor_core_(var, config):
await setup_entity(var, config, "binary_sensor")

View File

@@ -84,11 +84,6 @@ def button_schema(
return _BUTTON_SCHEMA.extend(schema)
# Remove before 2025.11.0
BUTTON_SCHEMA = button_schema(Button)
BUTTON_SCHEMA.add_extra(cv.deprecated_schema_constant("button"))
async def setup_button_core_(var, config):
await setup_entity(var, config, "button")

View File

@@ -270,11 +270,6 @@ def climate_schema(
return _CLIMATE_SCHEMA.extend(schema)
# Remove before 2025.11.0
CLIMATE_SCHEMA = climate_schema(Climate)
CLIMATE_SCHEMA.add_extra(cv.deprecated_schema_constant("climate"))
async def setup_climate_core_(var, config):
await setup_entity(var, config, "climate")

View File

@@ -1,10 +1,9 @@
import logging
from esphome import core
import esphome.codegen as cg
from esphome.components import climate, remote_base, sensor
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT
from esphome.const import CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT
from esphome.cpp_generator import MockObjClass
_LOGGER = logging.getLogger(__name__)
@@ -52,26 +51,6 @@ def climate_ir_with_receiver_schema(
)
# Remove before 2025.11.0
def deprecated_schema_constant(config):
type: str = "unknown"
if (id := config.get(CONF_ID)) is not None and isinstance(id, core.ID):
type = str(id.type).split("::", maxsplit=1)[0]
_LOGGER.warning(
"Using `climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA` is deprecated and will be removed in ESPHome 2025.11.0. "
"Please use `climate_ir.climate_ir_with_receiver_schema(...)` instead. "
"If you are seeing this, report an issue to the external_component author and ask them to update it. "
"https://developers.esphome.io/blog/2025/05/14/_schema-deprecations/. "
"Component using this schema: %s",
type,
)
return config
CLIMATE_IR_WITH_RECEIVER_SCHEMA = climate_ir_with_receiver_schema(ClimateIR)
CLIMATE_IR_WITH_RECEIVER_SCHEMA.add_extra(deprecated_schema_constant)
async def register_climate_ir(var, config):
await cg.register_component(var, config)
await remote_base.register_transmittable(var, config)

View File

@@ -151,11 +151,6 @@ def cover_schema(
return _COVER_SCHEMA.extend(schema)
# Remove before 2025.11.0
COVER_SCHEMA = cover_schema(Cover)
COVER_SCHEMA.add_extra(cv.deprecated_schema_constant("cover"))
async def setup_cover_core_(var, config):
await setup_entity(var, config, "cover")

View File

@@ -85,11 +85,6 @@ def event_schema(
return _EVENT_SCHEMA.extend(schema)
# Remove before 2025.11.0
EVENT_SCHEMA = event_schema()
EVENT_SCHEMA.add_extra(cv.deprecated_schema_constant("event"))
async def setup_event_core_(var, config, *, event_types: list[str]):
await setup_entity(var, config, "event")

View File

@@ -189,10 +189,6 @@ def fan_schema(
return _FAN_SCHEMA.extend(schema)
# Remove before 2025.11.0
FAN_SCHEMA = fan_schema(Fan)
FAN_SCHEMA.add_extra(cv.deprecated_schema_constant("fan"))
_PRESET_MODES_SCHEMA = cv.All(
cv.ensure_list(cv.string_strict),
cv.Length(min=1),

View File

@@ -91,11 +91,6 @@ def lock_schema(
return _LOCK_SCHEMA.extend(schema)
# Remove before 2025.11.0
LOCK_SCHEMA = lock_schema()
LOCK_SCHEMA.add_extra(cv.deprecated_schema_constant("lock"))
async def _setup_lock_core(var, config):
await setup_entity(var, config, "lock")

View File

@@ -5,6 +5,7 @@ Constants already defined in esphome.const are not duplicated here and must be i
"""
import logging
from typing import TYPE_CHECKING, Any
from esphome import codegen as cg, config_validation as cv
from esphome.const import CONF_ITEMS
@@ -12,6 +13,7 @@ from esphome.core import ID, Lambda
from esphome.cpp_generator import LambdaExpression, MockObj
from esphome.cpp_types import uint32
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.types import Expression, SafeExpType
from .helpers import requires_component
@@ -42,7 +44,13 @@ def static_cast(type, value):
def call_lambda(lamb: LambdaExpression):
expr = lamb.content.strip()
if expr.startswith("return") and expr.endswith(";"):
return expr[6:][:-1].strip()
return expr[6:-1].strip()
# If lambda has parameters, call it with those parameter names
# Parameter names come from hardcoded component code (like "x", "it", "event")
# not from user input, so they're safe to use directly
if lamb.parameters and lamb.parameters.parameters:
param_names = ", ".join(str(param.id) for param in lamb.parameters.parameters)
return f"{lamb}({param_names})"
return f"{lamb}()"
@@ -65,10 +73,20 @@ class LValidator:
return cv.returning_lambda(value)
return self.validator(value)
async def process(self, value, args=()):
async def process(
self, value: Any, args: list[tuple[SafeExpType, str]] | None = None
) -> Expression:
if value is None:
return None
if isinstance(value, Lambda):
# Local import to avoid circular import
from .lvcode import CodeContext, LambdaContext
if TYPE_CHECKING:
# CodeContext does not have get_automation_parameters
# so we need to assert the type here
assert isinstance(CodeContext.code_context, LambdaContext)
args = args or CodeContext.code_context.get_automation_parameters()
return cg.RawExpression(
call_lambda(
await cg.process_lambda(value, args, return_type=self.rtype)

View File

@@ -1,3 +1,5 @@
from typing import TYPE_CHECKING, Any
import esphome.codegen as cg
from esphome.components import image
from esphome.components.color import CONF_HEX, ColorStruct, from_rgbw
@@ -17,6 +19,7 @@ from esphome.cpp_generator import MockObj
from esphome.cpp_types import ESPTime, int32, uint32
from esphome.helpers import cpp_string_escape
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.types import Expression, SafeExpType
from . import types as ty
from .defines import (
@@ -388,11 +391,23 @@ class TextValidator(LValidator):
return value
return super().__call__(value)
async def process(self, value, args=()):
async def process(
self, value: Any, args: list[tuple[SafeExpType, str]] | None = None
) -> Expression:
# Local import to avoid circular import at module level
from .lvcode import CodeContext, LambdaContext
if TYPE_CHECKING:
# CodeContext does not have get_automation_parameters
# so we need to assert the type here
assert isinstance(CodeContext.code_context, LambdaContext)
args = args or CodeContext.code_context.get_automation_parameters()
if isinstance(value, dict):
if format_str := value.get(CONF_FORMAT):
args = [str(x) for x in value[CONF_ARGS]]
arg_expr = cg.RawExpression(",".join(args))
str_args = [str(x) for x in value[CONF_ARGS]]
arg_expr = cg.RawExpression(",".join(str_args))
format_str = cpp_string_escape(format_str)
return literal(f"str_sprintf({format_str}, {arg_expr}).c_str()")
if time_format := value.get(CONF_TIME_FORMAT):

View File

@@ -164,6 +164,9 @@ class LambdaContext(CodeContext):
code_text.append(text)
return code_text
def get_automation_parameters(self) -> list[tuple[SafeExpType, str]]:
return self.parameters
async def __aenter__(self):
await super().__aenter__()
add_line_marks(self.where)
@@ -178,9 +181,8 @@ class LvContext(LambdaContext):
added_lambda_count = 0
def __init__(self, args=None):
self.args = args or LVGL_COMP_ARG
super().__init__(parameters=self.args)
def __init__(self):
super().__init__(parameters=LVGL_COMP_ARG)
async def __aexit__(self, exc_type, exc_val, exc_tb):
await super().__aexit__(exc_type, exc_val, exc_tb)
@@ -189,6 +191,11 @@ class LvContext(LambdaContext):
cg.add(expression)
return expression
def get_automation_parameters(self) -> list[tuple[SafeExpType, str]]:
# When generating automations, we don't want the `lv_component` parameter to be passed
# to the lambda.
return []
def __call__(self, *args):
return self.add(*args)

View File

@@ -358,7 +358,7 @@ class LvSelectable : public LvCompound {
virtual void set_selected_index(size_t index, lv_anim_enable_t anim) = 0;
void set_selected_text(const std::string &text, lv_anim_enable_t anim);
std::string get_selected_text();
std::vector<std::string> get_options() { return this->options_; }
const std::vector<std::string> &get_options() { return this->options_; }
void set_options(std::vector<std::string> options);
protected:

View File

@@ -53,7 +53,17 @@ class LVGLSelect : public select::Select, public Component {
this->widget_->set_selected_text(value, this->anim_);
this->publish();
}
void set_options_() { this->traits.set_options(this->widget_->get_options()); }
void set_options_() {
// Widget uses std::vector<std::string>, SelectTraits uses FixedVector<const char*>
// Convert by extracting c_str() pointers
const auto &opts = this->widget_->get_options();
FixedVector<const char *> opt_ptrs;
opt_ptrs.init(opts.size());
for (size_t i = 0; i < opts.size(); i++) {
opt_ptrs[i] = opts[i].c_str();
}
this->traits.set_options(opt_ptrs);
}
LvSelectable *widget_;
lv_anim_enable_t anim_;

View File

@@ -5,7 +5,6 @@ from ..defines import CONF_WIDGET
from ..lvcode import (
API_EVENT,
EVENT_ARG,
LVGL_COMP_ARG,
UPDATE_EVENT,
LambdaContext,
LvContext,
@@ -30,7 +29,7 @@ async def to_code(config):
await wait_for_widgets()
async with LambdaContext(EVENT_ARG) as lamb:
lv_add(sensor.publish_state(widget.get_value()))
async with LvContext(LVGL_COMP_ARG):
async with LvContext():
lv_add(
lvgl_static.add_event_cb(
widget.obj,

View File

@@ -192,10 +192,6 @@ def media_player_schema(
return _MEDIA_PLAYER_SCHEMA.extend(schema)
# Remove before 2025.11.0
MEDIA_PLAYER_SCHEMA = media_player_schema(MediaPlayer)
MEDIA_PLAYER_SCHEMA.add_extra(cv.deprecated_schema_constant("media_player"))
MEDIA_PLAYER_ACTION_SCHEMA = automation.maybe_simple_id(
cv.Schema(
{

View File

@@ -28,7 +28,7 @@ void ModbusSelect::parse_and_publish(const std::vector<uint8_t> &data) {
if (map_it != this->mapping_.cend()) {
size_t idx = std::distance(this->mapping_.cbegin(), map_it);
new_state = this->traits.get_options()[idx];
new_state = std::string(this->option_at(idx));
ESP_LOGV(TAG, "Found option %s for value %lld", new_state->c_str(), value);
} else {
ESP_LOGE(TAG, "No option found for mapping %lld", value);
@@ -41,10 +41,12 @@ void ModbusSelect::parse_and_publish(const std::vector<uint8_t> &data) {
}
void ModbusSelect::control(const std::string &value) {
auto options = this->traits.get_options();
auto opt_it = std::find(options.cbegin(), options.cend(), value);
size_t idx = std::distance(options.cbegin(), opt_it);
optional<int64_t> mapval = this->mapping_[idx];
auto idx = this->index_of(value);
if (!idx.has_value()) {
ESP_LOGW(TAG, "Invalid option '%s'", value.c_str());
return;
}
optional<int64_t> mapval = this->mapping_[idx.value()];
ESP_LOGD(TAG, "Found value %lld for option '%s'", *mapval, value.c_str());
std::vector<uint16_t> data;

View File

@@ -238,11 +238,6 @@ def number_schema(
return _NUMBER_SCHEMA.extend(schema)
# Remove before 2025.11.0
NUMBER_SCHEMA = number_schema(Number)
NUMBER_SCHEMA.add_extra(cv.deprecated_schema_constant("number"))
async def setup_number_core_(
var, config, *, min_value: float, max_value: float, step: float
):

View File

@@ -1,3 +1,5 @@
import logging
from esphome import automation, pins
import esphome.codegen as cg
from esphome.components import esp32, esp32_rmt, remote_base
@@ -18,9 +20,12 @@ from esphome.const import (
)
from esphome.core import CORE
_LOGGER = logging.getLogger(__name__)
AUTO_LOAD = ["remote_base"]
CONF_EOT_LEVEL = "eot_level"
CONF_NON_BLOCKING = "non_blocking"
CONF_ON_TRANSMIT = "on_transmit"
CONF_ON_COMPLETE = "on_complete"
CONF_TRANSMITTER_ID = remote_base.CONF_TRANSMITTER_ID
@@ -65,11 +70,25 @@ CONFIG_SCHEMA = cv.Schema(
esp32_c6=48,
esp32_h2=48,
): cv.All(cv.only_on_esp32, cv.int_range(min=2)),
cv.Optional(CONF_NON_BLOCKING): cv.All(cv.only_on_esp32, cv.boolean),
cv.Optional(CONF_ON_TRANSMIT): automation.validate_automation(single=True),
cv.Optional(CONF_ON_COMPLETE): automation.validate_automation(single=True),
}
).extend(cv.COMPONENT_SCHEMA)
def _validate_non_blocking(config):
if CORE.is_esp32 and CONF_NON_BLOCKING not in config:
_LOGGER.warning(
"'non_blocking' is not set for 'remote_transmitter' and will default to 'true'.\n"
"The default behavior changed in 2025.11.0; previously blocking mode was used.\n"
"To silence this warning, explicitly set 'non_blocking: true' (or 'false')."
)
config[CONF_NON_BLOCKING] = True
FINAL_VALIDATE_SCHEMA = _validate_non_blocking
DIGITAL_WRITE_ACTION_SCHEMA = cv.maybe_simple_value(
{
cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(RemoteTransmitterComponent),
@@ -95,6 +114,7 @@ async def to_code(config):
if CORE.is_esp32:
var = cg.new_Pvariable(config[CONF_ID], pin)
cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS]))
cg.add(var.set_non_blocking(config[CONF_NON_BLOCKING]))
if CONF_CLOCK_RESOLUTION in config:
cg.add(var.set_clock_resolution(config[CONF_CLOCK_RESOLUTION]))
if CONF_USE_DMA in config:

View File

@@ -54,6 +54,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase,
#if defined(USE_ESP32)
void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; }
void set_eot_level(bool eot_level) { this->eot_level_ = eot_level; }
void set_non_blocking(bool non_blocking) { this->non_blocking_ = non_blocking; }
#endif
Trigger<> *get_transmit_trigger() const { return this->transmit_trigger_; };
@@ -74,6 +75,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase,
#ifdef USE_ESP32
void configure_rmt_();
void wait_for_rmt_();
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
RemoteTransmitterComponentStore store_{};
@@ -90,6 +92,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase,
esp_err_t error_code_{ESP_OK};
std::string error_string_{""};
bool inverted_{false};
bool non_blocking_{false};
#endif
uint8_t carrier_duty_percent_;

View File

@@ -196,12 +196,29 @@ void RemoteTransmitterComponent::configure_rmt_() {
}
}
void RemoteTransmitterComponent::wait_for_rmt_() {
esp_err_t error = rmt_tx_wait_all_done(this->channel_, -1);
if (error != ESP_OK) {
ESP_LOGW(TAG, "rmt_tx_wait_all_done failed: %s", esp_err_to_name(error));
this->status_set_warning();
}
this->complete_trigger_->trigger();
}
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
uint64_t total_duration = 0;
if (this->is_failed()) {
return;
}
// if the timeout was cancelled, block until the tx is complete
if (this->non_blocking_ && this->cancel_timeout("complete")) {
this->wait_for_rmt_();
}
if (this->current_carrier_frequency_ != this->temp_.get_carrier_frequency()) {
this->current_carrier_frequency_ = this->temp_.get_carrier_frequency();
this->configure_rmt_();
@@ -212,6 +229,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
// encode any delay at the start of the buffer to simplify the encoder callback
// this will be skipped the first time around
total_duration += send_wait * (send_times - 1);
send_wait = this->from_microseconds_(static_cast<uint32_t>(send_wait));
while (send_wait > 0) {
int32_t duration = std::min(send_wait, uint32_t(RMT_SYMBOL_DURATION_MAX));
@@ -229,6 +247,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
if (!level) {
value = -value;
}
total_duration += value * send_times;
value = this->from_microseconds_(static_cast<uint32_t>(value));
while (value > 0) {
int32_t duration = std::min(value, int32_t(RMT_SYMBOL_DURATION_MAX));
@@ -260,13 +279,12 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
} else {
this->status_clear_warning();
}
error = rmt_tx_wait_all_done(this->channel_, -1);
if (error != ESP_OK) {
ESP_LOGW(TAG, "rmt_tx_wait_all_done failed: %s", esp_err_to_name(error));
this->status_set_warning();
}
this->complete_trigger_->trigger();
if (this->non_blocking_) {
this->set_timeout("complete", total_duration / 1000, [this]() { this->wait_for_rmt_(); });
} else {
this->wait_for_rmt_();
}
}
#else
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {

View File

@@ -86,11 +86,6 @@ def select_schema(
return _SELECT_SCHEMA.extend(schema)
# Remove before 2025.11.0
SELECT_SCHEMA = select_schema(Select)
SELECT_SCHEMA.add_extra(cv.deprecated_schema_constant("select"))
async def setup_select_core_(var, config, *, options: list[str]):
await setup_entity(var, config, "select")

View File

@@ -1,5 +1,6 @@
#include "select.h"
#include "esphome/core/log.h"
#include <cstring>
namespace esphome {
namespace select {
@@ -35,7 +36,7 @@ size_t Select::size() const {
optional<size_t> Select::index_of(const std::string &option) const {
const auto &options = traits.get_options();
for (size_t i = 0; i < options.size(); i++) {
if (options[i] == option) {
if (strcmp(options[i], option.c_str()) == 0) {
return i;
}
}
@@ -53,11 +54,13 @@ optional<size_t> Select::active_index() const {
optional<std::string> Select::at(size_t index) const {
if (this->has_index(index)) {
const auto &options = traits.get_options();
return options.at(index);
return std::string(options.at(index));
} else {
return {};
}
}
const char *Select::option_at(size_t index) const { return traits.get_options().at(index); }
} // namespace select
} // namespace esphome

View File

@@ -56,6 +56,9 @@ class Select : public EntityBase {
/// Return the (optional) option value at the provided index offset.
optional<std::string> at(size_t index) const;
/// Return the option value at the provided index offset (as const char* from flash).
const char *option_at(size_t index) const;
void add_on_state_callback(std::function<void(std::string, size_t)> &&callback);
protected:

View File

@@ -3,9 +3,16 @@
namespace esphome {
namespace select {
void SelectTraits::set_options(std::vector<std::string> options) { this->options_ = std::move(options); }
void SelectTraits::set_options(const std::initializer_list<const char *> &options) { this->options_ = options; }
const std::vector<std::string> &SelectTraits::get_options() const { return this->options_; }
void SelectTraits::set_options(const FixedVector<const char *> &options) {
this->options_.init(options.size());
for (size_t i = 0; i < options.size(); i++) {
this->options_[i] = options[i];
}
}
const FixedVector<const char *> &SelectTraits::get_options() const { return this->options_; }
} // namespace select
} // namespace esphome

View File

@@ -1,18 +1,19 @@
#pragma once
#include <vector>
#include <string>
#include "esphome/core/helpers.h"
#include <initializer_list>
namespace esphome {
namespace select {
class SelectTraits {
public:
void set_options(std::vector<std::string> options);
const std::vector<std::string> &get_options() const;
void set_options(const std::initializer_list<const char *> &options);
void set_options(const FixedVector<const char *> &options);
const FixedVector<const char *> &get_options() const;
protected:
std::vector<std::string> options_;
FixedVector<const char *> options_;
};
} // namespace select

View File

@@ -369,11 +369,6 @@ def sensor_schema(
return _SENSOR_SCHEMA.extend(schema)
# Remove before 2025.11.0
SENSOR_SCHEMA = sensor_schema()
SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("sensor"))
@FILTER_REGISTRY.register("offset", OffsetFilter, cv.templatable(cv.float_))
async def offset_filter_to_code(config, filter_id):
template_ = await cg.templatable(config, [], float)

View File

@@ -139,11 +139,6 @@ def switch_schema(
return _SWITCH_SCHEMA.extend(schema)
# Remove before 2025.11.0
SWITCH_SCHEMA = switch_schema(Switch)
SWITCH_SCHEMA.add_extra(cv.deprecated_schema_constant("switch"))
async def setup_switch_core_(var, config):
await setup_entity(var, config, "switch")

View File

@@ -25,6 +25,20 @@ void TemplateAlarmControlPanel::add_sensor(binary_sensor::BinarySensor *sensor,
this->sensor_data_.push_back(sd);
this->sensor_map_[sensor].store_index = this->next_store_index_++;
};
static const LogString *sensor_type_to_string(AlarmSensorType type) {
switch (type) {
case ALARM_SENSOR_TYPE_INSTANT:
return LOG_STR("instant");
case ALARM_SENSOR_TYPE_DELAYED_FOLLOWER:
return LOG_STR("delayed_follower");
case ALARM_SENSOR_TYPE_INSTANT_ALWAYS:
return LOG_STR("instant_always");
case ALARM_SENSOR_TYPE_DELAYED:
default:
return LOG_STR("delayed");
}
}
#endif
void TemplateAlarmControlPanel::dump_config() {
@@ -46,35 +60,20 @@ void TemplateAlarmControlPanel::dump_config() {
" Supported Features: %" PRIu32,
(this->pending_time_ / 1000), (this->trigger_time_ / 1000), this->get_supported_features());
#ifdef USE_BINARY_SENSOR
for (auto sensor_info : this->sensor_map_) {
ESP_LOGCONFIG(TAG, " Binary Sensor:");
for (auto const &[sensor, info] : this->sensor_map_) {
ESP_LOGCONFIG(TAG,
" Binary Sensor:\n"
" Name: %s\n"
" Type: %s\n"
" Armed home bypass: %s\n"
" Armed night bypass: %s\n"
" Auto bypass: %s\n"
" Chime mode: %s",
sensor_info.first->get_name().c_str(),
TRUEFALSE(sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME),
TRUEFALSE(sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT),
TRUEFALSE(sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_AUTO),
TRUEFALSE(sensor_info.second.flags & BINARY_SENSOR_MODE_CHIME));
const char *sensor_type;
switch (sensor_info.second.type) {
case ALARM_SENSOR_TYPE_INSTANT:
sensor_type = "instant";
break;
case ALARM_SENSOR_TYPE_DELAYED_FOLLOWER:
sensor_type = "delayed_follower";
break;
case ALARM_SENSOR_TYPE_INSTANT_ALWAYS:
sensor_type = "instant_always";
break;
case ALARM_SENSOR_TYPE_DELAYED:
default:
sensor_type = "delayed";
}
ESP_LOGCONFIG(TAG, " Sensor type: %s", sensor_type);
sensor->get_name().c_str(), LOG_STR_ARG(sensor_type_to_string(info.type)),
TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME),
TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT),
TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO),
TRUEFALSE(info.flags & BINARY_SENSOR_MODE_CHIME));
}
#endif
}
@@ -123,39 +122,37 @@ void TemplateAlarmControlPanel::loop() {
bool instant_sensor_faulted = false;
#ifdef USE_BINARY_SENSOR
// Test all of the sensors in the list regardless of the alarm panel state
for (auto sensor_info : this->sensor_map_) {
// Test all of the sensors regardless of the alarm panel state
for (auto const &[sensor, info] : this->sensor_map_) {
// Check for chime zones
if ((sensor_info.second.flags & BINARY_SENSOR_MODE_CHIME)) {
if (info.flags & BINARY_SENSOR_MODE_CHIME) {
// Look for the transition from closed to open
if ((!this->sensor_data_[sensor_info.second.store_index].last_chime_state) && (sensor_info.first->state)) {
if ((!this->sensor_data_[info.store_index].last_chime_state) && (sensor->state)) {
// Must be disarmed to chime
if (this->current_state_ == ACP_STATE_DISARMED) {
this->chime_callback_.call();
}
}
// Record the sensor state change
this->sensor_data_[sensor_info.second.store_index].last_chime_state = sensor_info.first->state;
this->sensor_data_[info.store_index].last_chime_state = sensor->state;
}
// Check for faulted sensors
if (sensor_info.first->state) { // Sensor triggered?
if (sensor->state) {
// Skip if auto bypassed
if (std::count(this->bypassed_sensor_indicies_.begin(), this->bypassed_sensor_indicies_.end(),
sensor_info.second.store_index) == 1) {
info.store_index) == 1) {
continue;
}
// Skip if bypass armed home
if (this->current_state_ == ACP_STATE_ARMED_HOME &&
(sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME)) {
if ((this->current_state_ == ACP_STATE_ARMED_HOME) && (info.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME)) {
continue;
}
// Skip if bypass armed night
if (this->current_state_ == ACP_STATE_ARMED_NIGHT &&
(sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT)) {
if ((this->current_state_ == ACP_STATE_ARMED_NIGHT) && (info.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT)) {
continue;
}
switch (sensor_info.second.type) {
switch (info.type) {
case ALARM_SENSOR_TYPE_INSTANT_ALWAYS:
next_state = ACP_STATE_TRIGGERED;
[[fallthrough]];
@@ -247,11 +244,11 @@ void TemplateAlarmControlPanel::arm_(optional<std::string> code, alarm_control_p
void TemplateAlarmControlPanel::bypass_before_arming() {
#ifdef USE_BINARY_SENSOR
for (auto sensor_info : this->sensor_map_) {
for (auto const &[sensor, info] : this->sensor_map_) {
// Check for faulted bypass_auto sensors and remove them from monitoring
if ((sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (sensor_info.first->state)) {
ESP_LOGW(TAG, "'%s' is faulted and will be automatically bypassed", sensor_info.first->get_name().c_str());
this->bypassed_sensor_indicies_.push_back(sensor_info.second.store_index);
if ((info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (sensor->state)) {
ESP_LOGW(TAG, "'%s' is faulted and will be automatically bypassed", sensor->get_name().c_str());
this->bypassed_sensor_indicies_.push_back(info.store_index);
}
}
#endif

View File

@@ -6,17 +6,21 @@ namespace template_ {
static const char *const TAG = "template.binary_sensor";
void TemplateBinarySensor::setup() { this->loop(); }
void TemplateBinarySensor::setup() {
if (!this->f_.has_value()) {
this->disable_loop();
} else {
this->loop();
}
}
void TemplateBinarySensor::loop() {
if (!this->f_.has_value())
return;
auto s = (*this->f_)();
auto s = this->f_();
if (s.has_value()) {
this->publish_state(*s);
}
}
void TemplateBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Template Binary Sensor", this); }
} // namespace template_

View File

@@ -1,6 +1,7 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/template_lambda.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
namespace esphome {
@@ -8,7 +9,10 @@ namespace template_ {
class TemplateBinarySensor : public Component, public binary_sensor::BinarySensor {
public:
void set_template(optional<bool> (*f)()) { this->f_ = f; }
template<typename F> void set_template(F &&f) {
this->f_.set(std::forward<F>(f));
this->enable_loop();
}
void setup() override;
void loop() override;
@@ -17,7 +21,7 @@ class TemplateBinarySensor : public Component, public binary_sensor::BinarySenso
float get_setup_priority() const override { return setup_priority::HARDWARE; }
protected:
optional<optional<bool> (*)()> f_;
TemplateLambda<bool> f_;
};
} // namespace template_

View File

@@ -33,28 +33,27 @@ void TemplateCover::setup() {
break;
}
}
if (!this->state_f_.has_value() && !this->tilt_f_.has_value())
this->disable_loop();
}
void TemplateCover::loop() {
bool changed = false;
if (this->state_f_.has_value()) {
auto s = (*this->state_f_)();
if (s.has_value()) {
auto pos = clamp(*s, 0.0f, 1.0f);
if (pos != this->position) {
this->position = pos;
changed = true;
}
auto s = this->state_f_();
if (s.has_value()) {
auto pos = clamp(*s, 0.0f, 1.0f);
if (pos != this->position) {
this->position = pos;
changed = true;
}
}
if (this->tilt_f_.has_value()) {
auto s = (*this->tilt_f_)();
if (s.has_value()) {
auto tilt = clamp(*s, 0.0f, 1.0f);
if (tilt != this->tilt) {
this->tilt = tilt;
changed = true;
}
auto tilt = this->tilt_f_();
if (tilt.has_value()) {
auto tilt_val = clamp(*tilt, 0.0f, 1.0f);
if (tilt_val != this->tilt) {
this->tilt = tilt_val;
changed = true;
}
}
@@ -63,7 +62,6 @@ void TemplateCover::loop() {
}
void TemplateCover::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
void TemplateCover::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; }
void TemplateCover::set_state_lambda(optional<float> (*f)()) { this->state_f_ = f; }
float TemplateCover::get_setup_priority() const { return setup_priority::HARDWARE; }
Trigger<> *TemplateCover::get_open_trigger() const { return this->open_trigger_; }
Trigger<> *TemplateCover::get_close_trigger() const { return this->close_trigger_; }
@@ -124,7 +122,6 @@ CoverTraits TemplateCover::get_traits() {
}
Trigger<float> *TemplateCover::get_position_trigger() const { return this->position_trigger_; }
Trigger<float> *TemplateCover::get_tilt_trigger() const { return this->tilt_trigger_; }
void TemplateCover::set_tilt_lambda(optional<float> (*tilt_f)()) { this->tilt_f_ = tilt_f; }
void TemplateCover::set_has_stop(bool has_stop) { this->has_stop_ = has_stop; }
void TemplateCover::set_has_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; }
void TemplateCover::set_has_position(bool has_position) { this->has_position_ = has_position; }

View File

@@ -2,6 +2,7 @@
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/core/template_lambda.h"
#include "esphome/components/cover/cover.h"
namespace esphome {
@@ -17,7 +18,14 @@ class TemplateCover : public cover::Cover, public Component {
public:
TemplateCover();
void set_state_lambda(optional<float> (*f)());
template<typename F> void set_state_lambda(F &&f) {
this->state_f_.set(std::forward<F>(f));
this->enable_loop();
}
template<typename F> void set_tilt_lambda(F &&f) {
this->tilt_f_.set(std::forward<F>(f));
this->enable_loop();
}
Trigger<> *get_open_trigger() const;
Trigger<> *get_close_trigger() const;
Trigger<> *get_stop_trigger() const;
@@ -26,7 +34,6 @@ class TemplateCover : public cover::Cover, public Component {
Trigger<float> *get_tilt_trigger() const;
void set_optimistic(bool optimistic);
void set_assumed_state(bool assumed_state);
void set_tilt_lambda(optional<float> (*tilt_f)());
void set_has_stop(bool has_stop);
void set_has_position(bool has_position);
void set_has_tilt(bool has_tilt);
@@ -45,8 +52,8 @@ class TemplateCover : public cover::Cover, public Component {
void stop_prev_trigger_();
TemplateCoverRestoreMode restore_mode_{COVER_RESTORE};
optional<optional<float> (*)()> state_f_;
optional<optional<float> (*)()> tilt_f_;
TemplateLambda<float> state_f_;
TemplateLambda<float> tilt_f_;
bool assumed_state_{false};
bool optimistic_{false};
Trigger<> *open_trigger_;

View File

@@ -40,14 +40,13 @@ void TemplateDate::update() {
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
if (!val.has_value())
return;
this->year_ = val->year;
this->month_ = val->month;
this->day_ = val->day_of_month;
this->publish_state();
auto val = this->f_();
if (val.has_value()) {
this->year_ = val->year;
this->month_ = val->month;
this->day_ = val->day_of_month;
this->publish_state();
}
}
void TemplateDate::control(const datetime::DateCall &call) {

View File

@@ -9,13 +9,14 @@
#include "esphome/core/component.h"
#include "esphome/core/preferences.h"
#include "esphome/core/time.h"
#include "esphome/core/template_lambda.h"
namespace esphome {
namespace template_ {
class TemplateDate : public datetime::DateEntity, public PollingComponent {
public:
void set_template(optional<ESPTime> (*f)()) { this->f_ = f; }
template<typename F> void set_template(F &&f) { this->f_.set(std::forward<F>(f)); }
void setup() override;
void update() override;
@@ -35,7 +36,7 @@ class TemplateDate : public datetime::DateEntity, public PollingComponent {
ESPTime initial_value_{};
bool restore_value_{false};
Trigger<ESPTime> *set_trigger_ = new Trigger<ESPTime>();
optional<optional<ESPTime> (*)()> f_;
TemplateLambda<ESPTime> f_;
ESPPreferenceObject pref_;
};

View File

@@ -43,17 +43,16 @@ void TemplateDateTime::update() {
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
if (!val.has_value())
return;
this->year_ = val->year;
this->month_ = val->month;
this->day_ = val->day_of_month;
this->hour_ = val->hour;
this->minute_ = val->minute;
this->second_ = val->second;
this->publish_state();
auto val = this->f_();
if (val.has_value()) {
this->year_ = val->year;
this->month_ = val->month;
this->day_ = val->day_of_month;
this->hour_ = val->hour;
this->minute_ = val->minute;
this->second_ = val->second;
this->publish_state();
}
}
void TemplateDateTime::control(const datetime::DateTimeCall &call) {

View File

@@ -9,13 +9,14 @@
#include "esphome/core/component.h"
#include "esphome/core/preferences.h"
#include "esphome/core/time.h"
#include "esphome/core/template_lambda.h"
namespace esphome {
namespace template_ {
class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponent {
public:
void set_template(optional<ESPTime> (*f)()) { this->f_ = f; }
template<typename F> void set_template(F &&f) { this->f_.set(std::forward<F>(f)); }
void setup() override;
void update() override;
@@ -35,7 +36,7 @@ class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponen
ESPTime initial_value_{};
bool restore_value_{false};
Trigger<ESPTime> *set_trigger_ = new Trigger<ESPTime>();
optional<optional<ESPTime> (*)()> f_;
TemplateLambda<ESPTime> f_;
ESPPreferenceObject pref_;
};

View File

@@ -40,14 +40,13 @@ void TemplateTime::update() {
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
if (!val.has_value())
return;
this->hour_ = val->hour;
this->minute_ = val->minute;
this->second_ = val->second;
this->publish_state();
auto val = this->f_();
if (val.has_value()) {
this->hour_ = val->hour;
this->minute_ = val->minute;
this->second_ = val->second;
this->publish_state();
}
}
void TemplateTime::control(const datetime::TimeCall &call) {

View File

@@ -9,13 +9,14 @@
#include "esphome/core/component.h"
#include "esphome/core/preferences.h"
#include "esphome/core/time.h"
#include "esphome/core/template_lambda.h"
namespace esphome {
namespace template_ {
class TemplateTime : public datetime::TimeEntity, public PollingComponent {
public:
void set_template(optional<ESPTime> (*f)()) { this->f_ = f; }
template<typename F> void set_template(F &&f) { this->f_.set(std::forward<F>(f)); }
void setup() override;
void update() override;
@@ -35,7 +36,7 @@ class TemplateTime : public datetime::TimeEntity, public PollingComponent {
ESPTime initial_value_{};
bool restore_value_{false};
Trigger<ESPTime> *set_trigger_ = new Trigger<ESPTime>();
optional<optional<ESPTime> (*)()> f_;
TemplateLambda<ESPTime> f_;
ESPPreferenceObject pref_;
};

View File

@@ -12,13 +12,10 @@ TemplateLock::TemplateLock()
: lock_trigger_(new Trigger<>()), unlock_trigger_(new Trigger<>()), open_trigger_(new Trigger<>()) {}
void TemplateLock::loop() {
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
if (!val.has_value())
return;
this->publish_state(*val);
auto val = this->f_();
if (val.has_value()) {
this->publish_state(*val);
}
}
void TemplateLock::control(const lock::LockCall &call) {
if (this->prev_trigger_ != nullptr) {
@@ -45,7 +42,6 @@ void TemplateLock::open_latch() {
this->open_trigger_->trigger();
}
void TemplateLock::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
void TemplateLock::set_state_lambda(optional<lock::LockState> (*f)()) { this->f_ = f; }
float TemplateLock::get_setup_priority() const { return setup_priority::HARDWARE; }
Trigger<> *TemplateLock::get_lock_trigger() const { return this->lock_trigger_; }
Trigger<> *TemplateLock::get_unlock_trigger() const { return this->unlock_trigger_; }

View File

@@ -2,6 +2,7 @@
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/core/template_lambda.h"
#include "esphome/components/lock/lock.h"
namespace esphome {
@@ -13,7 +14,10 @@ class TemplateLock : public lock::Lock, public Component {
void dump_config() override;
void set_state_lambda(optional<lock::LockState> (*f)());
template<typename F> void set_state_lambda(F &&f) {
this->f_.set(std::forward<F>(f));
this->enable_loop();
}
Trigger<> *get_lock_trigger() const;
Trigger<> *get_unlock_trigger() const;
Trigger<> *get_open_trigger() const;
@@ -26,7 +30,7 @@ class TemplateLock : public lock::Lock, public Component {
void control(const lock::LockCall &call) override;
void open_latch() override;
optional<optional<lock::LockState> (*)()> f_;
TemplateLambda<lock::LockState> f_;
bool optimistic_{false};
Trigger<> *lock_trigger_;
Trigger<> *unlock_trigger_;

View File

@@ -30,11 +30,10 @@ void TemplateNumber::update() {
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
if (!val.has_value())
return;
this->publish_state(*val);
auto val = this->f_();
if (val.has_value()) {
this->publish_state(*val);
}
}
void TemplateNumber::control(float value) {

View File

@@ -4,13 +4,14 @@
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/preferences.h"
#include "esphome/core/template_lambda.h"
namespace esphome {
namespace template_ {
class TemplateNumber : public number::Number, public PollingComponent {
public:
void set_template(optional<float> (*f)()) { this->f_ = f; }
template<typename F> void set_template(F &&f) { this->f_.set(std::forward<F>(f)); }
void setup() override;
void update() override;
@@ -28,7 +29,7 @@ class TemplateNumber : public number::Number, public PollingComponent {
float initial_value_{NAN};
bool restore_value_{false};
Trigger<float> *set_trigger_ = new Trigger<float>();
optional<optional<float> (*)()> f_;
TemplateLambda<float> f_;
ESPPreferenceObject pref_;
};

View File

@@ -16,12 +16,12 @@ void TemplateSelect::setup() {
size_t restored_index;
if (this->pref_.load(&restored_index) && this->has_index(restored_index)) {
index = restored_index;
ESP_LOGD(TAG, "State from restore: %s", this->at(index).value().c_str());
ESP_LOGD(TAG, "State from restore: %s", this->option_at(index));
} else {
ESP_LOGD(TAG, "State from initial (could not load or invalid stored index): %s", this->at(index).value().c_str());
ESP_LOGD(TAG, "State from initial (could not load or invalid stored index): %s", this->option_at(index));
}
} else {
ESP_LOGD(TAG, "State from initial: %s", this->at(index).value().c_str());
ESP_LOGD(TAG, "State from initial: %s", this->option_at(index));
}
this->publish_state(this->at(index).value());
@@ -31,16 +31,14 @@ void TemplateSelect::update() {
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
if (!val.has_value())
return;
if (!this->has_option(*val)) {
ESP_LOGE(TAG, "Lambda returned an invalid option: %s", (*val).c_str());
return;
auto val = this->f_();
if (val.has_value()) {
if (!this->has_option(*val)) {
ESP_LOGE(TAG, "Lambda returned an invalid option: %s", (*val).c_str());
return;
}
this->publish_state(*val);
}
this->publish_state(*val);
}
void TemplateSelect::control(const std::string &value) {
@@ -64,8 +62,7 @@ void TemplateSelect::dump_config() {
" Optimistic: %s\n"
" Initial Option: %s\n"
" Restore Value: %s",
YESNO(this->optimistic_), this->at(this->initial_option_index_).value().c_str(),
YESNO(this->restore_value_));
YESNO(this->optimistic_), this->option_at(this->initial_option_index_), YESNO(this->restore_value_));
}
} // namespace template_

View File

@@ -4,13 +4,14 @@
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/preferences.h"
#include "esphome/core/template_lambda.h"
namespace esphome {
namespace template_ {
class TemplateSelect : public select::Select, public PollingComponent {
public:
void set_template(optional<std::string> (*f)()) { this->f_ = f; }
template<typename F> void set_template(F &&f) { this->f_.set(std::forward<F>(f)); }
void setup() override;
void update() override;
@@ -28,7 +29,7 @@ class TemplateSelect : public select::Select, public PollingComponent {
size_t initial_option_index_{0};
bool restore_value_ = false;
Trigger<std::string> *set_trigger_ = new Trigger<std::string>();
optional<optional<std::string> (*)()> f_;
TemplateLambda<std::string> f_;
ESPPreferenceObject pref_;
};

View File

@@ -11,13 +11,14 @@ void TemplateSensor::update() {
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
auto val = this->f_();
if (val.has_value()) {
this->publish_state(*val);
}
}
float TemplateSensor::get_setup_priority() const { return setup_priority::HARDWARE; }
void TemplateSensor::set_template(optional<float> (*f)()) { this->f_ = f; }
void TemplateSensor::dump_config() {
LOG_SENSOR("", "Template Sensor", this);
LOG_UPDATE_INTERVAL(this);

View File

@@ -1,6 +1,7 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/template_lambda.h"
#include "esphome/components/sensor/sensor.h"
namespace esphome {
@@ -8,7 +9,7 @@ namespace template_ {
class TemplateSensor : public sensor::Sensor, public PollingComponent {
public:
void set_template(optional<float> (*f)());
template<typename F> void set_template(F &&f) { this->f_.set(std::forward<F>(f)); }
void update() override;
@@ -17,7 +18,7 @@ class TemplateSensor : public sensor::Sensor, public PollingComponent {
float get_setup_priority() const override;
protected:
optional<optional<float> (*)()> f_;
TemplateLambda<float> f_;
};
} // namespace template_

View File

@@ -9,13 +9,10 @@ static const char *const TAG = "template.switch";
TemplateSwitch::TemplateSwitch() : turn_on_trigger_(new Trigger<>()), turn_off_trigger_(new Trigger<>()) {}
void TemplateSwitch::loop() {
if (!this->f_.has_value())
return;
auto s = (*this->f_)();
if (!s.has_value())
return;
this->publish_state(*s);
auto s = this->f_();
if (s.has_value()) {
this->publish_state(*s);
}
}
void TemplateSwitch::write_state(bool state) {
if (this->prev_trigger_ != nullptr) {
@@ -35,11 +32,13 @@ void TemplateSwitch::write_state(bool state) {
}
void TemplateSwitch::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
bool TemplateSwitch::assumed_state() { return this->assumed_state_; }
void TemplateSwitch::set_state_lambda(optional<bool> (*f)()) { this->f_ = f; }
float TemplateSwitch::get_setup_priority() const { return setup_priority::HARDWARE - 2.0f; }
Trigger<> *TemplateSwitch::get_turn_on_trigger() const { return this->turn_on_trigger_; }
Trigger<> *TemplateSwitch::get_turn_off_trigger() const { return this->turn_off_trigger_; }
void TemplateSwitch::setup() {
if (!this->f_.has_value())
this->disable_loop();
optional<bool> initial_state = this->get_initial_state_with_restore_mode();
if (initial_state.has_value()) {

View File

@@ -2,6 +2,7 @@
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/core/template_lambda.h"
#include "esphome/components/switch/switch.h"
namespace esphome {
@@ -14,7 +15,10 @@ class TemplateSwitch : public switch_::Switch, public Component {
void setup() override;
void dump_config() override;
void set_state_lambda(optional<bool> (*f)());
template<typename F> void set_state_lambda(F &&f) {
this->f_.set(std::forward<F>(f));
this->enable_loop();
}
Trigger<> *get_turn_on_trigger() const;
Trigger<> *get_turn_off_trigger() const;
void set_optimistic(bool optimistic);
@@ -28,7 +32,7 @@ class TemplateSwitch : public switch_::Switch, public Component {
void write_state(bool state) override;
optional<optional<bool> (*)()> f_;
TemplateLambda<bool> f_;
bool optimistic_{false};
bool assumed_state_{false};
Trigger<> *turn_on_trigger_;

View File

@@ -7,10 +7,8 @@ namespace template_ {
static const char *const TAG = "template.text";
void TemplateText::setup() {
if (!(this->f_ == nullptr)) {
if (this->f_.has_value())
return;
}
if (this->f_.has_value())
return;
std::string value = this->initial_value_;
if (!this->pref_) {
ESP_LOGD(TAG, "State from initial: %s", value.c_str());
@@ -26,17 +24,13 @@ void TemplateText::setup() {
}
void TemplateText::update() {
if (this->f_ == nullptr)
return;
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
if (!val.has_value())
return;
this->publish_state(*val);
auto val = this->f_();
if (val.has_value()) {
this->publish_state(*val);
}
}
void TemplateText::control(const std::string &value) {

View File

@@ -4,6 +4,7 @@
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/preferences.h"
#include "esphome/core/template_lambda.h"
namespace esphome {
namespace template_ {
@@ -61,7 +62,7 @@ template<uint8_t SZ> class TextSaver : public TemplateTextSaverBase {
class TemplateText : public text::Text, public PollingComponent {
public:
void set_template(optional<std::string> (*f)()) { this->f_ = f; }
template<typename F> void set_template(F &&f) { this->f_.set(std::forward<F>(f)); }
void setup() override;
void update() override;
@@ -78,7 +79,7 @@ class TemplateText : public text::Text, public PollingComponent {
bool optimistic_ = false;
std::string initial_value_;
Trigger<std::string> *set_trigger_ = new Trigger<std::string>();
optional<optional<std::string> (*)()> f_{nullptr};
TemplateLambda<std::string> f_{};
TemplateTextSaverBase *pref_ = nullptr;
};

View File

@@ -10,13 +10,14 @@ void TemplateTextSensor::update() {
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
auto val = this->f_();
if (val.has_value()) {
this->publish_state(*val);
}
}
float TemplateTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; }
void TemplateTextSensor::set_template(optional<std::string> (*f)()) { this->f_ = f; }
void TemplateTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Template Sensor", this); }
} // namespace template_

View File

@@ -2,6 +2,7 @@
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/core/template_lambda.h"
#include "esphome/components/text_sensor/text_sensor.h"
namespace esphome {
@@ -9,7 +10,7 @@ namespace template_ {
class TemplateTextSensor : public text_sensor::TextSensor, public PollingComponent {
public:
void set_template(optional<std::string> (*f)());
template<typename F> void set_template(F &&f) { this->f_.set(std::forward<F>(f)); }
void update() override;
@@ -18,7 +19,7 @@ class TemplateTextSensor : public text_sensor::TextSensor, public PollingCompone
void dump_config() override;
protected:
optional<optional<std::string> (*)()> f_{};
TemplateLambda<std::string> f_{};
};
} // namespace template_

View File

@@ -33,19 +33,19 @@ void TemplateValve::setup() {
break;
}
}
if (!this->state_f_.has_value())
this->disable_loop();
}
void TemplateValve::loop() {
bool changed = false;
if (this->state_f_.has_value()) {
auto s = (*this->state_f_)();
if (s.has_value()) {
auto pos = clamp(*s, 0.0f, 1.0f);
if (pos != this->position) {
this->position = pos;
changed = true;
}
auto s = this->state_f_();
if (s.has_value()) {
auto pos = clamp(*s, 0.0f, 1.0f);
if (pos != this->position) {
this->position = pos;
changed = true;
}
}
@@ -55,7 +55,6 @@ void TemplateValve::loop() {
void TemplateValve::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
void TemplateValve::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; }
void TemplateValve::set_state_lambda(optional<float> (*f)()) { this->state_f_ = f; }
float TemplateValve::get_setup_priority() const { return setup_priority::HARDWARE; }
Trigger<> *TemplateValve::get_open_trigger() const { return this->open_trigger_; }

View File

@@ -2,6 +2,7 @@
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/core/template_lambda.h"
#include "esphome/components/valve/valve.h"
namespace esphome {
@@ -17,7 +18,10 @@ class TemplateValve : public valve::Valve, public Component {
public:
TemplateValve();
void set_state_lambda(optional<float> (*f)());
template<typename F> void set_state_lambda(F &&f) {
this->state_f_.set(std::forward<F>(f));
this->enable_loop();
}
Trigger<> *get_open_trigger() const;
Trigger<> *get_close_trigger() const;
Trigger<> *get_stop_trigger() const;
@@ -42,7 +46,7 @@ class TemplateValve : public valve::Valve, public Component {
void stop_prev_trigger_();
TemplateValveRestoreMode restore_mode_{VALVE_NO_RESTORE};
optional<optional<float> (*)()> state_f_;
TemplateLambda<float> state_f_;
bool assumed_state_{false};
bool optimistic_{false};
Trigger<> *open_trigger_;

View File

@@ -84,11 +84,6 @@ def text_schema(
return _TEXT_SCHEMA.extend(schema)
# Remove before 2025.11.0
TEXT_SCHEMA = text_schema()
TEXT_SCHEMA.add_extra(cv.deprecated_schema_constant("text"))
async def setup_text_core_(
var,
config,

View File

@@ -193,11 +193,6 @@ def text_sensor_schema(
return _TEXT_SENSOR_SCHEMA.extend(schema)
# Remove before 2025.11.0
TEXT_SENSOR_SCHEMA = text_sensor_schema()
TEXT_SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("text_sensor"))
async def build_filters(config):
return await cg.build_registry_list(FILTER_REGISTRY, config)

View File

@@ -10,7 +10,6 @@ void TuyaSelect::setup() {
this->parent_->register_listener(this->select_id_, [this](const TuyaDatapoint &datapoint) {
uint8_t enum_value = datapoint.value_enum;
ESP_LOGV(TAG, "MCU reported select %u value %u", this->select_id_, enum_value);
auto options = this->traits.get_options();
auto mappings = this->mappings_;
auto it = std::find(mappings.cbegin(), mappings.cend(), enum_value);
if (it == mappings.end()) {
@@ -49,9 +48,9 @@ void TuyaSelect::dump_config() {
" Data type: %s\n"
" Options are:",
this->select_id_, this->is_int_ ? "int" : "enum");
auto options = this->traits.get_options();
const auto &options = this->traits.get_options();
for (size_t i = 0; i < this->mappings_.size(); i++) {
ESP_LOGCONFIG(TAG, " %i: %s", this->mappings_.at(i), options.at(i).c_str());
ESP_LOGCONFIG(TAG, " %i: %s", this->mappings_.at(i), options.at(i));
}
}

View File

@@ -84,11 +84,6 @@ def update_schema(
return _UPDATE_SCHEMA.extend(schema)
# Remove before 2025.11.0
UPDATE_SCHEMA = update_schema()
UPDATE_SCHEMA.add_extra(cv.deprecated_schema_constant("update"))
async def setup_update_core_(var, config):
await setup_entity(var, config, "update")

View File

@@ -129,11 +129,6 @@ def valve_schema(
return _VALVE_SCHEMA.extend(schema)
# Remove before 2025.11.0
VALVE_SCHEMA = valve_schema()
VALVE_SCHEMA.add_extra(cv.deprecated_schema_constant("valve"))
async def _setup_valve_core(var, config):
await setup_entity(var, config, "valve")

View File

@@ -319,6 +319,9 @@ def iter_ids(config, path=None):
yield from iter_ids(item, path + [i])
elif isinstance(config, dict):
for key, value in config.items():
if len(path) == 0 and key == CONF_SUBSTITUTIONS:
# Ignore IDs in substitution definitions.
continue
if isinstance(key, core.ID):
yield key, path
yield from iter_ids(value, path + [key])

View File

@@ -2195,26 +2195,3 @@ def rename_key(old_key, new_key):
return config
return validator
# Remove before 2025.11.0
def deprecated_schema_constant(entity_type: str):
def validator(config):
type: str = "unknown"
if (id := config.get(CONF_ID)) is not None and isinstance(id, core.ID):
type = str(id.type).split("::", maxsplit=1)[0]
_LOGGER.warning(
"Using `%s.%s_SCHEMA` is deprecated and will be removed in ESPHome 2025.11.0. "
"Please use `%s.%s_schema(...)` instead. "
"If you are seeing this, report an issue to the external_component author and ask them to update it. "
"https://developers.esphome.io/blog/2025/05/14/_schema-deprecations/. "
"Component using this schema: %s",
entity_type,
entity_type.upper(),
entity_type,
entity_type,
type,
)
return config
return validator

View File

@@ -304,6 +304,11 @@ template<typename T> class FixedVector {
return data_[size_ - 1];
}
/// Access first element (no bounds checking - matches std::vector behavior)
/// Caller must ensure vector is not empty (size() > 0)
T &front() { return data_[0]; }
const T &front() const { return data_[0]; }
/// Access last element (no bounds checking - matches std::vector behavior)
/// Caller must ensure vector is not empty (size() > 0)
T &back() { return data_[size_ - 1]; }
@@ -317,6 +322,11 @@ template<typename T> class FixedVector {
T &operator[](size_t i) { return data_[i]; }
const T &operator[](size_t i) const { return data_[i]; }
/// Access element with bounds checking (matches std::vector behavior)
/// Note: No exception thrown on out of bounds - caller must ensure index is valid
T &at(size_t i) { return data_[i]; }
const T &at(size_t i) const { return data_[i]; }
// Iterator support for range-based for loops
T *begin() { return data_; }
T *end() { return data_ + size_; }

View File

@@ -0,0 +1,120 @@
#pragma once
#include <functional>
#include <concepts>
#include "esphome/core/optional.h"
namespace esphome {
/** Helper class for template platforms that stores either a stateless lambda (function pointer)
* or a stateful lambda (std::function pointer).
*
* This provides backward compatibility with PR #11555 while maintaining the optimization:
* - Stateless lambdas (no capture) → function pointer (4 bytes on ESP32)
* - Stateful lambdas (with capture) → pointer to std::function (4 bytes on ESP32)
* Total size: enum (1 byte) + union (4 bytes) + padding = 8 bytes (same as PR #11555)
*
* Both lambda types must return optional<T> (as YAML codegen does) to support the pattern:
* return {}; // Don't publish a value
* return 42.0; // Publish this value
*
* operator() returns optional<T>, returning nullopt when no lambda is set (type == NONE).
* This eliminates redundant "is lambda set" checks by reusing optional's discriminator.
*
* @tparam T The return type (e.g., float for TemplateLambda<optional<float>>)
* @tparam Args Optional arguments for the lambda
*/
template<typename T, typename... Args> class TemplateLambda {
public:
TemplateLambda() : type_(NONE) {}
// For stateless lambdas: use function pointer
template<typename F>
requires std::invocable<F, Args...> && std::convertible_to < F, optional<T>(*)
(Args...) > void set(F f) {
this->reset_();
this->type_ = STATELESS_LAMBDA;
this->stateless_f_ = f; // Implicit conversion to function pointer
}
// For stateful lambdas: use std::function pointer
template<typename F>
requires std::invocable<F, Args...> &&
(!std::convertible_to<F, optional<T> (*)(Args...)>) &&std::convertible_to<std::invoke_result_t<F, Args...>,
optional<T>> void set(F &&f) {
this->reset_();
this->type_ = LAMBDA;
this->f_ = new std::function<optional<T>(Args...)>(std::forward<F>(f));
}
~TemplateLambda() { this->reset_(); }
// Copy constructor
TemplateLambda(const TemplateLambda &) = delete;
TemplateLambda &operator=(const TemplateLambda &) = delete;
// Move constructor
TemplateLambda(TemplateLambda &&other) noexcept : type_(other.type_) {
if (type_ == LAMBDA) {
this->f_ = other.f_;
other.f_ = nullptr;
} else if (type_ == STATELESS_LAMBDA) {
this->stateless_f_ = other.stateless_f_;
}
other.type_ = NONE;
}
TemplateLambda &operator=(TemplateLambda &&other) noexcept {
if (this != &other) {
this->reset_();
this->type_ = other.type_;
if (type_ == LAMBDA) {
this->f_ = other.f_;
other.f_ = nullptr;
} else if (type_ == STATELESS_LAMBDA) {
this->stateless_f_ = other.stateless_f_;
}
other.type_ = NONE;
}
return *this;
}
bool has_value() const { return this->type_ != NONE; }
// Returns optional<T>, returning nullopt if no lambda is set
optional<T> operator()(Args... args) {
switch (this->type_) {
case STATELESS_LAMBDA:
return this->stateless_f_(args...); // Direct function pointer call
case LAMBDA:
return (*this->f_)(args...); // std::function call via pointer
case NONE:
default:
return nullopt; // No lambda set
}
}
optional<T> call(Args... args) { return (*this)(args...); }
protected:
void reset_() {
if (this->type_ == LAMBDA) {
delete this->f_;
this->f_ = nullptr;
}
this->type_ = NONE;
}
enum : uint8_t {
NONE,
STATELESS_LAMBDA,
LAMBDA,
} type_;
union {
optional<T> (*stateless_f_)(Args...); // Function pointer (4 bytes on ESP32)
std::function<optional<T>(Args...)> *f_; // Pointer to std::function (4 bytes on ESP32)
};
};
} // namespace esphome

View File

@@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
esptool==5.1.0
click==8.1.7
esphome-dashboard==20251013.0
aioesphomeapi==42.4.0
aioesphomeapi==42.5.0
zeroconf==0.148.0
puremagic==1.30
ruamel.yaml==0.18.16 # dashboard_import

View File

@@ -1,6 +1,6 @@
pylint==4.0.2
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
ruff==0.14.1 # also change in .pre-commit-config.yaml when updating
ruff==0.14.2 # also change in .pre-commit-config.yaml when updating
pyupgrade==3.21.0 # also change in .pre-commit-config.yaml when updating
pre-commit

View File

@@ -1162,7 +1162,11 @@ class SInt64Type(TypeInfo):
def _generate_array_dump_content(
ti, field_name: str, name: str, is_bool: bool = False
ti,
field_name: str,
name: str,
is_bool: bool = False,
is_const_char_ptr: bool = False,
) -> str:
"""Generate dump content for array types (repeated or fixed array).
@@ -1170,7 +1174,10 @@ def _generate_array_dump_content(
"""
o = f"for (const auto {'' if is_bool else '&'}it : {field_name}) {{\n"
# Check if underlying type can use dump_field
if ti.can_use_dump_field():
if is_const_char_ptr:
# Special case for const char* - use it directly
o += f' dump_field(out, "{name}", it, 4);\n'
elif ti.can_use_dump_field():
# For types that have dump_field overloads, use them with extra indent
# std::vector<bool> iterators return proxy objects, need explicit cast
value_expr = "static_cast<bool>(it)" if is_bool else ti.dump_field_value("it")
@@ -1533,11 +1540,16 @@ class RepeatedTypeInfo(TypeInfo):
def encode_content(self) -> str:
if self._use_pointer:
# For pointer fields, just dereference (pointer should never be null in our use case)
o = f"for (const auto &it : *this->{self.field_name}) {{\n"
if isinstance(self._ti, EnumType):
o += f" buffer.{self._ti.encode_func}({self.number}, static_cast<uint32_t>(it), true);\n"
# Special handling for const char* elements (when container_no_template contains "const char")
if "const char" in self._container_no_template:
o = f"for (const char *it : *this->{self.field_name}) {{\n"
o += f" buffer.{self._ti.encode_func}({self.number}, it, strlen(it), true);\n"
else:
o += f" buffer.{self._ti.encode_func}({self.number}, it, true);\n"
o = f"for (const auto &it : *this->{self.field_name}) {{\n"
if isinstance(self._ti, EnumType):
o += f" buffer.{self._ti.encode_func}({self.number}, static_cast<uint32_t>(it), true);\n"
else:
o += f" buffer.{self._ti.encode_func}({self.number}, it, true);\n"
o += "}"
return o
o = f"for (auto {'' if self._ti_is_bool else '&'}it : this->{self.field_name}) {{\n"
@@ -1550,10 +1562,18 @@ class RepeatedTypeInfo(TypeInfo):
@property
def dump_content(self) -> str:
# Check if this is const char* elements
is_const_char_ptr = (
self._use_pointer and "const char" in self._container_no_template
)
if self._use_pointer:
# For pointer fields, dereference and use the existing helper
return _generate_array_dump_content(
self._ti, f"*this->{self.field_name}", self.name, is_bool=False
self._ti,
f"*this->{self.field_name}",
self.name,
is_bool=False,
is_const_char_ptr=is_const_char_ptr,
)
return _generate_array_dump_content(
self._ti, f"this->{self.field_name}", self.name, is_bool=self._ti_is_bool
@@ -1588,9 +1608,14 @@ class RepeatedTypeInfo(TypeInfo):
o += f" size.add_precalculated_size({size_expr} * {bytes_per_element});\n"
else:
# Other types need the actual value
auto_ref = "" if self._ti_is_bool else "&"
o += f" for (const auto {auto_ref}it : {container_ref}) {{\n"
o += f" {self._ti.get_size_calculation('it', True)}\n"
# Special handling for const char* elements
if self._use_pointer and "const char" in self._container_no_template:
o += f" for (const char *it : {container_ref}) {{\n"
o += " size.add_length_force(1, strlen(it));\n"
else:
auto_ref = "" if self._ti_is_bool else "&"
o += f" for (const auto {auto_ref}it : {container_ref}) {{\n"
o += f" {self._ti.get_size_calculation('it', True)}\n"
o += " }\n"
o += "}"
@@ -2542,6 +2567,12 @@ static void dump_field(std::string &out, const char *field_name, StringRef value
out.append("\\n");
}
static void dump_field(std::string &out, const char *field_name, const char *value, int indent = 2) {
append_field_prefix(out, field_name, indent);
out.append("'").append(value).append("'");
out.append("\\n");
}
template<typename T>
static void dump_field(std::string &out, const char *field_name, T value, int indent = 2) {
append_field_prefix(out, field_name, indent);

View File

@@ -300,7 +300,7 @@ def fix_remote_receiver():
remote_receiver_schema["CONFIG_SCHEMA"] = {
"type": "schema",
"schema": {
"extends": ["binary_sensor.BINARY_SENSOR_SCHEMA", "core.COMPONENT_SCHEMA"],
"extends": ["binary_sensor._BINARY_SENSOR_SCHEMA", "core.COMPONENT_SCHEMA"],
"config_vars": output["remote_base"].pop("binary"),
},
}

View File

@@ -90,16 +90,18 @@ def get_component_from_path(file_path: str) -> str | None:
"""Extract component name from a file path.
Args:
file_path: Path to a file (e.g., "esphome/components/wifi/wifi.cpp")
file_path: Path to a file (e.g., "esphome/components/wifi/wifi.cpp"
or "tests/components/uart/test.esp32-idf.yaml")
Returns:
Component name if path is in components directory, None otherwise
Component name if path is in components or tests directory, None otherwise
"""
if not file_path.startswith(ESPHOME_COMPONENTS_PATH):
return None
parts = file_path.split("/")
if len(parts) >= 3:
return parts[2]
if file_path.startswith(ESPHOME_COMPONENTS_PATH) or file_path.startswith(
ESPHOME_TESTS_COMPONENTS_PATH
):
parts = file_path.split("/")
if len(parts) >= 3 and parts[2]:
return parts[2]
return None

View File

@@ -52,6 +52,19 @@ number:
widget: spinbox_id
id: lvgl_spinbox_number
name: LVGL Spinbox Number
- platform: template
id: test_brightness
name: "Test Brightness"
min_value: 0
max_value: 255
step: 1
optimistic: true
# Test lambda in automation accessing x parameter directly
# This is a real-world pattern from user configs
on_value:
- lambda: !lambda |-
// Direct use of x parameter in automation
ESP_LOGD("test", "Brightness: %.0f", x);
light:
- platform: lvgl
@@ -110,3 +123,21 @@ text:
platform: lvgl
widget: hello_label
mode: text
text_sensor:
- platform: template
id: test_text_sensor
name: "Test Text Sensor"
# Test nested lambdas in LVGL actions can access automation parameters
on_value:
- lvgl.label.update:
id: hello_label
text: !lambda return x.c_str();
- lvgl.label.update:
id: hello_label
text: !lambda |-
// Test complex lambda with conditionals accessing x parameter
if (x == "*") {
return "WILDCARD";
}
return x.c_str();

View File

@@ -257,7 +257,30 @@ lvgl:
text: "Hello shiny day"
text_color: 0xFFFFFF
align: bottom_mid
- label:
id: setup_lambda_label
# Test lambda in widget property during setup (LvContext)
# Should NOT receive lv_component parameter
text: !lambda |-
char buf[32];
snprintf(buf, sizeof(buf), "Setup: %d", 42);
return std::string(buf);
align: top_mid
text_font: space16
- label:
id: chip_info_label
# Test complex setup lambda (real-world pattern)
# Should NOT receive lv_component parameter
text: !lambda |-
// Test conditional compilation and string formatting
char buf[64];
#ifdef USE_ESP_IDF
snprintf(buf, sizeof(buf), "IDF: v%d.%d", ESP_IDF_VERSION_MAJOR, ESP_IDF_VERSION_MINOR);
#else
snprintf(buf, sizeof(buf), "Arduino");
#endif
return std::string(buf);
align: top_left
- obj:
align: center
arc_opa: COVER

View File

@@ -2,6 +2,7 @@ remote_transmitter:
- id: xmitr
pin: ${pin}
carrier_duty_percent: 50%
non_blocking: true
clock_resolution: ${clock_resolution}
rmt_symbols: ${rmt_symbols}

View File

@@ -9,6 +9,27 @@ esphome:
id: template_sens
state: !lambda "return 42.0;"
# Test C++ API: set_template() with stateless lambda (no captures)
- lambda: |-
id(template_sens).set_template([]() -> esphome::optional<float> {
return 123.0f;
});
# Test C++ API: set_template() with stateful lambda (with captures)
# This is the regression test for issue #11555
- lambda: |-
float captured_value = 456.0f;
id(template_sens).set_template([captured_value]() -> esphome::optional<float> {
return captured_value;
});
# Test C++ API: set_template() with more complex capture
- lambda: |-
auto sensor_id = id(template_sens);
id(template_number).set_template([sensor_id]() -> esphome::optional<float> {
return sensor_id->state * 2.0f;
});
- datetime.date.set:
id: test_date
date:
@@ -215,6 +236,7 @@ cover:
number:
- platform: template
id: template_number
name: "Template number"
optimistic: true
min_value: 0

View File

@@ -1065,3 +1065,39 @@ def test_parse_list_components_output(output: str, expected: list[str]) -> None:
"""Test parse_list_components_output function."""
result = helpers.parse_list_components_output(output)
assert result == expected
@pytest.mark.parametrize(
("file_path", "expected_component"),
[
# Component files
("esphome/components/wifi/wifi.cpp", "wifi"),
("esphome/components/uart/uart.h", "uart"),
("esphome/components/api/api_server.cpp", "api"),
("esphome/components/sensor/sensor.cpp", "sensor"),
# Test files
("tests/components/uart/test.esp32-idf.yaml", "uart"),
("tests/components/wifi/test.esp8266-ard.yaml", "wifi"),
("tests/components/sensor/test.esp32-idf.yaml", "sensor"),
("tests/components/api/test_api.cpp", "api"),
("tests/components/uart/common.h", "uart"),
# Non-component files
("esphome/core/component.cpp", None),
("esphome/core/helpers.h", None),
("tests/integration/test_api.py", None),
("tests/unit_tests/test_helpers.py", None),
("README.md", None),
("script/helpers.py", None),
# Edge cases
("esphome/components/", None), # No component name
("tests/components/", None), # No component name
("esphome/components", None), # No trailing slash
("tests/components", None), # No trailing slash
],
)
def test_get_component_from_path(
file_path: str, expected_component: str | None
) -> None:
"""Test extraction of component names from file paths."""
result = helpers.get_component_from_path(file_path)
assert result == expected_component

View File

@@ -261,6 +261,17 @@ def test_device_duplicate_id(
assert "ID duplicate_device redefined!" in captured.out
def test_substitution_with_id(
yaml_file: Callable[[str], str], capsys: pytest.CaptureFixture[str]
) -> None:
"""Test that a ids coming from substitutions do not cause false positive ID redefinition."""
load_config_from_fixture(
yaml_file, "id_collision_with_substitution.yaml", FIXTURES_DIR
)
captured = capsys.readouterr()
assert "ID some_switch_id redefined!" not in captured.out
def test_add_platform_defines_priority() -> None:
"""Test that _add_platform_defines runs after globals.

View File

@@ -0,0 +1,12 @@
esphome:
name: test
host:
substitutions:
support_switches:
- platform: gpio
id: some_switch_id
pin: 12
switch: $support_switches