1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-01 07:31:51 +00:00

Compare commits

...

20 Commits

Author SHA1 Message Date
Clyde Stubbs
0b4d445794 [sdl] Fix keymappings (#11635) 2025-11-01 17:45:42 +11:00
Clyde Stubbs
4d1d37a911 [lvgl] Fix event for binary sensor (#11636) 2025-11-01 17:37:07 +11:00
Clyde Stubbs
8df5a3a630 [lvgl] Trigger improvements and additions (#11628) 2025-11-01 17:27:28 +11:00
J. Nick Koston
5a5894eaa3 [ruff] Remove deprecated UP038 rule from ignore list (#11646) 2025-11-01 17:05:26 +11:00
Clyde Stubbs
d9d2d2f6b9 [automations] Update error message (#11640) 2025-11-01 15:17:23 +11:00
Clyde Stubbs
30f2a4395f [image] Catch and report svg load errors (#11619) 2025-11-01 11:08:28 +11:00
dependabot[bot]
292abd1187 Bump ruff from 0.14.2 to 0.14.3 (#11633)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-10-31 19:46:50 +00:00
Javier Peletier
6d0527ff2a [substitutions] fix jinja parsing strings that look like sets as sets (#11611) 2025-10-31 14:04:55 -05:00
dependabot[bot]
fd64585f99 Bump github/codeql-action from 4.31.0 to 4.31.2 (#11626)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-30 16:50:06 -05:00
Markus
077cce9848 [core] .local addresses are only resolvable if mDNS is enabled (#11508) 2025-10-30 10:08:08 -05:00
J. Nick Koston
bd87e56bc7 [e131] Replace std::set with std::vector to reduce flash usage (#11598) 2025-10-30 15:14:03 +13:00
J. Nick Koston
58235049e3 [template] Eliminate optional wrapper to save 4 bytes RAM per instance (#11610) 2025-10-30 15:06:21 +13:00
J. Nick Koston
29ed3c20af [gpio] Skip set_use_interrupt call when using default value (#11612) 2025-10-30 14:28:38 +13:00
J. Nick Koston
08aae39ea4 [ci] Consolidate component splitting into determine-jobs (#11614) 2025-10-30 14:27:28 +13:00
J. Nick Koston
03fd114371 [ci] Restore parallel execution for clang-tidy split mode (#11613) 2025-10-30 14:26:37 +13:00
Stuart Parmenter
918650f15a [lvgl] memset canvas buffer to prevent display of random garbage (#11582)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2025-10-29 21:06:45 +00:00
Stuart Parmenter
287f65cbaf [lvgl] fix typo from previous refactor (#11596) 2025-10-30 07:27:31 +11: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
72 changed files with 842 additions and 520 deletions

View File

@@ -180,6 +180,7 @@ jobs:
memory_impact: ${{ steps.determine.outputs.memory-impact }}
cpp-unit-tests-run-all: ${{ steps.determine.outputs.cpp-unit-tests-run-all }}
cpp-unit-tests-components: ${{ steps.determine.outputs.cpp-unit-tests-components }}
component-test-batches: ${{ steps.determine.outputs.component-test-batches }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -214,6 +215,7 @@ jobs:
echo "memory-impact=$(echo "$output" | jq -c '.memory_impact')" >> $GITHUB_OUTPUT
echo "cpp-unit-tests-run-all=$(echo "$output" | jq -r '.cpp_unit_tests_run_all')" >> $GITHUB_OUTPUT
echo "cpp-unit-tests-components=$(echo "$output" | jq -c '.cpp_unit_tests_components')" >> $GITHUB_OUTPUT
echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT
integration-tests:
name: Run integration tests
@@ -458,7 +460,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
strategy:
fail-fast: false
max-parallel: 1
max-parallel: 2
matrix:
include:
- id: clang-tidy
@@ -536,59 +538,18 @@ jobs:
run: script/ci-suggest-changes
if: always()
test-build-components-splitter:
name: Split components for intelligent grouping (40 weighted per batch)
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0
outputs:
matrix: ${{ steps.split.outputs.components }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Split components intelligently based on bus configurations
id: split
run: |
. venv/bin/activate
# Use intelligent splitter that groups components with same bus configs
components='${{ needs.determine-jobs.outputs.changed-components-with-tests }}'
# Only isolate directly changed components when targeting dev branch
# For beta/release branches, group everything for faster CI
if [[ "${{ github.base_ref }}" == beta* ]] || [[ "${{ github.base_ref }}" == release* ]]; then
directly_changed='[]'
echo "Target branch: ${{ github.base_ref }} - grouping all components"
else
directly_changed='${{ needs.determine-jobs.outputs.directly-changed-components-with-tests }}'
echo "Target branch: ${{ github.base_ref }} - isolating directly changed components"
fi
echo "Splitting components intelligently..."
output=$(python3 script/split_components_for_ci.py --components "$components" --directly-changed "$directly_changed" --batch-size 40 --output github)
echo "$output" >> $GITHUB_OUTPUT
test-build-components-split:
name: Test components batch (${{ matrix.components }})
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
- test-build-components-splitter
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0
strategy:
fail-fast: false
max-parallel: ${{ (startsWith(github.base_ref, 'beta') || startsWith(github.base_ref, 'release')) && 8 || 4 }}
matrix:
components: ${{ fromJson(needs.test-build-components-splitter.outputs.matrix) }}
components: ${{ fromJson(needs.determine-jobs.outputs.component-test-batches) }}
steps:
- name: Show disk space
run: |
@@ -980,7 +941,6 @@ jobs:
- clang-tidy-nosplit
- clang-tidy-split
- determine-jobs
- test-build-components-splitter
- test-build-components-split
- pre-commit-ci-lite
- memory-impact-target-branch

View File

@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
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@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
with:
category: "/language:${{matrix.language}}"

View File

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

View File

@@ -207,14 +207,14 @@ def choose_upload_log_host(
if has_mqtt_logging():
resolved.append("MQTT")
if has_api() and has_non_ip_address():
if has_api() and has_non_ip_address() and has_resolvable_address():
resolved.extend(_resolve_with_cache(CORE.address, purpose))
elif purpose == Purpose.UPLOADING:
if has_ota() and has_mqtt_ip_lookup():
resolved.append("MQTTIP")
if has_ota() and has_non_ip_address():
if has_ota() and has_non_ip_address() and has_resolvable_address():
resolved.extend(_resolve_with_cache(CORE.address, purpose))
else:
resolved.append(device)
@@ -318,7 +318,17 @@ def has_resolvable_address() -> bool:
"""Check if CORE.address is resolvable (via mDNS, DNS, or is an IP address)."""
# Any address (IP, mDNS hostname, or regular DNS hostname) is resolvable
# The resolve_ip_address() function in helpers.py handles all types via AsyncResolver
return CORE.address is not None
if CORE.address is None:
return False
if has_ip_address():
return True
if has_mdns():
return True
# .local mDNS hostnames are only resolvable if mDNS is enabled
return not CORE.address.endswith(".local")
def mqtt_get_ip(config: ConfigType, username: str, password: str, client_id: str):

View File

@@ -182,7 +182,7 @@ def validate_automation(extra_schema=None, extra_validators=None, single=False):
value = cv.Schema([extra_validators])(value)
if single:
if len(value) != 1:
raise cv.Invalid("Cannot have more than 1 automation for templates")
raise cv.Invalid("This trigger allows only a single automation")
return value[0]
return value

View File

@@ -3,6 +3,8 @@
#include "e131_addressable_light_effect.h"
#include "esphome/core/log.h"
#include <algorithm>
namespace esphome {
namespace e131 {
@@ -76,14 +78,14 @@ void E131Component::loop() {
}
void E131Component::add_effect(E131AddressableLightEffect *light_effect) {
if (light_effects_.count(light_effect)) {
if (std::find(light_effects_.begin(), light_effects_.end(), light_effect) != light_effects_.end()) {
return;
}
ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(),
light_effect->get_last_universe());
light_effects_.insert(light_effect);
light_effects_.push_back(light_effect);
for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) {
join_(universe);
@@ -91,14 +93,17 @@ void E131Component::add_effect(E131AddressableLightEffect *light_effect) {
}
void E131Component::remove_effect(E131AddressableLightEffect *light_effect) {
if (!light_effects_.count(light_effect)) {
auto it = std::find(light_effects_.begin(), light_effects_.end(), light_effect);
if (it == light_effects_.end()) {
return;
}
ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(),
light_effect->get_last_universe());
light_effects_.erase(light_effect);
// Swap with last element and pop for O(1) removal (order doesn't matter)
*it = light_effects_.back();
light_effects_.pop_back();
for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) {
leave_(universe);

View File

@@ -7,7 +7,6 @@
#include <cinttypes>
#include <map>
#include <memory>
#include <set>
#include <vector>
namespace esphome {
@@ -47,9 +46,8 @@ class E131Component : public esphome::Component {
E131ListenMethod listen_method_{E131_MULTICAST};
std::unique_ptr<socket::Socket> socket_;
std::set<E131AddressableLightEffect *> light_effects_;
std::vector<E131AddressableLightEffect *> light_effects_;
std::map<int, int> universe_consumers_;
std::map<int, E131Packet> universe_packets_;
};
} // namespace e131

View File

@@ -94,6 +94,8 @@ async def to_code(config):
)
use_interrupt = False
cg.add(var.set_use_interrupt(use_interrupt))
if use_interrupt:
cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE]))
else:
# Only generate call when disabling interrupts (default is true)
cg.add(var.set_use_interrupt(use_interrupt))

View File

@@ -671,18 +671,33 @@ async def write_image(config, all_frames=False):
resize = config.get(CONF_RESIZE)
if is_svg_file(path):
# Local import so use of non-SVG files needn't require cairosvg installed
from pyexpat import ExpatError
from xml.etree.ElementTree import ParseError
from cairosvg import svg2png
from cairosvg.helpers import PointError
if not resize:
resize = (None, None)
with open(path, "rb") as file:
image = svg2png(
file_obj=file,
output_width=resize[0],
output_height=resize[1],
)
image = Image.open(io.BytesIO(image))
width, height = image.size
try:
with open(path, "rb") as file:
image = svg2png(
file_obj=file,
output_width=resize[0],
output_height=resize[1],
)
image = Image.open(io.BytesIO(image))
width, height = image.size
except (
ValueError,
ParseError,
IndexError,
ExpatError,
AttributeError,
TypeError,
PointError,
) as e:
raise core.EsphomeError(f"Could not load SVG image {path}: {e}") from e
else:
image = Image.open(path)
width, height = image.size

View File

@@ -125,7 +125,7 @@ lv_img_dsc_t *Image::get_lv_img_dsc() {
case IMAGE_TYPE_RGB:
#if LV_COLOR_DEPTH == 32
switch (this->transparent_) {
switch (this->transparency_) {
case TRANSPARENCY_ALPHA_CHANNEL:
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA;
break;
@@ -156,7 +156,8 @@ lv_img_dsc_t *Image::get_lv_img_dsc() {
break;
}
#else
this->dsc_.header.cf = this->transparent_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGB565A8 : LV_IMG_CF_RGB565;
this->dsc_.header.cf =
this->transparency_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGB565A8 : LV_IMG_CF_RGB565;
#endif
break;
}

View File

@@ -58,7 +58,7 @@ from .types import (
FontEngine,
IdleTrigger,
ObjUpdateAction,
PauseTrigger,
PlainTrigger,
lv_font_t,
lv_group_t,
lv_style_t,
@@ -151,6 +151,13 @@ for w_type in WIDGET_TYPES.values():
create_modify_schema(w_type),
)(update_to_code)
SIMPLE_TRIGGERS = (
df.CONF_ON_PAUSE,
df.CONF_ON_RESUME,
df.CONF_ON_DRAW_START,
df.CONF_ON_DRAW_END,
)
def as_macro(macro, value):
if value is None:
@@ -244,9 +251,9 @@ def final_validation(configs):
for w in refreshed_widgets:
path = global_config.get_path_for_id(w)
widget_conf = global_config.get_config_for_path(path[:-1])
if not any(isinstance(v, Lambda) for v in widget_conf.values()):
if not any(isinstance(v, (Lambda, dict)) for v in widget_conf.values()):
raise cv.Invalid(
f"Widget '{w}' does not have any templated properties to refresh",
f"Widget '{w}' does not have any dynamic properties to refresh",
)
@@ -366,16 +373,16 @@ async def to_code(configs):
conf[CONF_TRIGGER_ID], lv_component, templ
)
await build_automation(idle_trigger, [], conf)
for conf in config.get(df.CONF_ON_PAUSE, ()):
pause_trigger = cg.new_Pvariable(
conf[CONF_TRIGGER_ID], lv_component, True
)
await build_automation(pause_trigger, [], conf)
for conf in config.get(df.CONF_ON_RESUME, ()):
resume_trigger = cg.new_Pvariable(
conf[CONF_TRIGGER_ID], lv_component, False
)
await build_automation(resume_trigger, [], conf)
for trigger_name in SIMPLE_TRIGGERS:
if conf := config.get(trigger_name):
trigger_var = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
await build_automation(trigger_var, [], conf)
cg.add(
getattr(
lv_component,
f"set_{trigger_name.removeprefix('on_')}_trigger",
)(trigger_var)
)
await add_on_boot_triggers(config.get(CONF_ON_BOOT, ()))
# This must be done after all widgets are created
@@ -443,16 +450,15 @@ LVGL_SCHEMA = cv.All(
),
}
),
cv.Optional(df.CONF_ON_PAUSE): validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PauseTrigger),
}
),
cv.Optional(df.CONF_ON_RESUME): validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PauseTrigger),
}
),
**{
cv.Optional(x): validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PlainTrigger),
},
single=True,
)
for x in SIMPLE_TRIGGERS
},
cv.Exclusive(df.CONF_WIDGETS, CONF_PAGES): cv.ensure_list(
WIDGET_SCHEMA
),

View File

@@ -400,7 +400,8 @@ async def obj_refresh_to_code(config, action_id, template_arg, args):
# must pass all widget-specific options here, even if not templated, but only do so if at least one is
# templated. First filter out common style properties.
config = {k: v for k, v in widget.config.items() if k not in ALL_STYLES}
if any(isinstance(v, Lambda) for v in config.values()):
# Check if v is a Lambda or a dict, implying it is dynamic
if any(isinstance(v, (Lambda, dict)) for v in config.values()):
await widget.type.to_code(widget, config)
if (
widget.type.w_type.value_property is not None

View File

@@ -31,7 +31,7 @@ async def to_code(config):
lvgl_static.add_event_cb(
widget.obj,
await pressed_ctx.get_lambda(),
LV_EVENT.PRESSING,
LV_EVENT.PRESSED,
LV_EVENT.RELEASED,
)
)

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)
@@ -465,6 +483,8 @@ CONF_MSGBOXES = "msgboxes"
CONF_OBJ = "obj"
CONF_ONE_CHECKED = "one_checked"
CONF_ONE_LINE = "one_line"
CONF_ON_DRAW_START = "on_draw_start"
CONF_ON_DRAW_END = "on_draw_end"
CONF_ON_PAUSE = "on_pause"
CONF_ON_RESUME = "on_resume"
CONF_ON_SELECT = "on_select"

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

@@ -82,6 +82,18 @@ static void rounder_cb(lv_disp_drv_t *disp_drv, lv_area_t *area) {
area->y2 = (area->y2 + draw_rounding) / draw_rounding * draw_rounding - 1;
}
void LvglComponent::monitor_cb(lv_disp_drv_t *disp_drv, uint32_t time, uint32_t px) {
ESP_LOGVV(TAG, "Draw end: %" PRIu32 " pixels in %" PRIu32 " ms", px, time);
auto *comp = static_cast<LvglComponent *>(disp_drv->user_data);
comp->draw_end_();
}
void LvglComponent::render_start_cb(lv_disp_drv_t *disp_drv) {
ESP_LOGVV(TAG, "Draw start");
auto *comp = static_cast<LvglComponent *>(disp_drv->user_data);
comp->draw_start_();
}
lv_event_code_t lv_api_event; // NOLINT
lv_event_code_t lv_update_event; // NOLINT
void LvglComponent::dump_config() {
@@ -101,7 +113,10 @@ void LvglComponent::set_paused(bool paused, bool show_snow) {
lv_disp_trig_activity(this->disp_); // resets the inactivity time
lv_obj_invalidate(lv_scr_act());
}
this->pause_callbacks_.call(paused);
if (paused && this->pause_callback_ != nullptr)
this->pause_callback_->trigger();
if (!paused && this->resume_callback_ != nullptr)
this->resume_callback_->trigger();
}
void LvglComponent::esphome_lvgl_init() {
@@ -225,13 +240,6 @@ IdleTrigger::IdleTrigger(LvglComponent *parent, TemplatableValue<uint32_t> timeo
});
}
PauseTrigger::PauseTrigger(LvglComponent *parent, TemplatableValue<bool> paused) : paused_(std::move(paused)) {
parent->add_on_pause_callback([this](bool pausing) {
if (this->paused_.value() == pausing)
this->trigger();
});
}
#ifdef USE_LVGL_TOUCHSCREEN
LVTouchListener::LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time, LvglComponent *parent) {
this->set_parent(parent);
@@ -474,6 +482,12 @@ void LvglComponent::setup() {
return;
}
}
if (this->draw_start_callback_ != nullptr) {
this->disp_drv_.render_start_cb = render_start_cb;
}
if (this->draw_end_callback_ != nullptr) {
this->disp_drv_.monitor_cb = monitor_cb;
}
#if LV_USE_LOG
lv_log_register_print_cb([](const char *buf) {
auto next = strchr(buf, ')');
@@ -502,8 +516,9 @@ void LvglComponent::loop() {
if (this->paused_) {
if (this->show_snow_)
this->write_random_();
} else {
lv_timer_handler_run_in_period(5);
}
lv_timer_handler_run_in_period(5);
}
#ifdef USE_LVGL_ANIMIMG

View File

@@ -171,7 +171,9 @@ class LvglComponent : public PollingComponent {
void add_on_idle_callback(std::function<void(uint32_t)> &&callback) {
this->idle_callbacks_.add(std::move(callback));
}
void add_on_pause_callback(std::function<void(bool)> &&callback) { this->pause_callbacks_.add(std::move(callback)); }
static void monitor_cb(lv_disp_drv_t *disp_drv, uint32_t time, uint32_t px);
static void render_start_cb(lv_disp_drv_t *disp_drv);
void dump_config() override;
bool is_idle(uint32_t idle_ms) { return lv_disp_get_inactive_time(this->disp_) > idle_ms; }
lv_disp_t *get_disp() { return this->disp_; }
@@ -213,12 +215,20 @@ class LvglComponent : public PollingComponent {
size_t draw_rounding{2};
display::DisplayRotation rotation{display::DISPLAY_ROTATION_0_DEGREES};
void set_pause_trigger(Trigger<> *trigger) { this->pause_callback_ = trigger; }
void set_resume_trigger(Trigger<> *trigger) { this->resume_callback_ = trigger; }
void set_draw_start_trigger(Trigger<> *trigger) { this->draw_start_callback_ = trigger; }
void set_draw_end_trigger(Trigger<> *trigger) { this->draw_end_callback_ = trigger; }
protected:
// these functions are never called unless the callbacks are non-null since the
// LVGL callbacks that call them are not set unless the start/end callbacks are non-null
void draw_start_() const { this->draw_start_callback_->trigger(); }
void draw_end_() const { this->draw_end_callback_->trigger(); }
void write_random_();
void draw_buffer_(const lv_area_t *area, lv_color_t *ptr);
void flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p);
std::vector<display::Display *> displays_{};
size_t buffer_frac_{1};
bool full_refresh_{};
@@ -235,7 +245,10 @@ class LvglComponent : public PollingComponent {
std::map<lv_group_t *, lv_obj_t *> focus_marks_{};
CallbackManager<void(uint32_t)> idle_callbacks_{};
CallbackManager<void(bool)> pause_callbacks_{};
Trigger<> *pause_callback_{};
Trigger<> *resume_callback_{};
Trigger<> *draw_start_callback_{};
Trigger<> *draw_end_callback_{};
lv_color_t *rotate_buf_{};
};
@@ -248,14 +261,6 @@ class IdleTrigger : public Trigger<> {
bool is_idle_{};
};
class PauseTrigger : public Trigger<> {
public:
explicit PauseTrigger(LvglComponent *parent, TemplatableValue<bool> paused);
protected:
TemplatableValue<bool> paused_;
};
template<typename... Ts> class LvglAction : public Action<Ts...>, public Parented<LvglComponent> {
public:
explicit LvglAction(std::function<void(LvglComponent *)> &&lamb) : action_(std::move(lamb)) {}

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

@@ -3,6 +3,7 @@ import sys
from esphome import automation, codegen as cg
from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_TEXT, CONF_VALUE
from esphome.cpp_generator import MockObj, MockObjClass
from esphome.cpp_types import esphome_ns
from .defines import lvgl_ns
from .lvcode import lv_expr
@@ -42,8 +43,11 @@ lv_event_code_t = cg.global_ns.enum("lv_event_code_t")
lv_indev_type_t = cg.global_ns.enum("lv_indev_type_t")
lv_key_t = cg.global_ns.enum("lv_key_t")
FontEngine = lvgl_ns.class_("FontEngine")
PlainTrigger = esphome_ns.class_("Trigger<>", automation.Trigger.template())
DrawEndTrigger = esphome_ns.class_(
"Trigger<uint32_t, uint32_t>", automation.Trigger.template(cg.uint32, cg.uint32)
)
IdleTrigger = lvgl_ns.class_("IdleTrigger", automation.Trigger.template())
PauseTrigger = lvgl_ns.class_("PauseTrigger", automation.Trigger.template())
ObjUpdateAction = lvgl_ns.class_("ObjUpdateAction", automation.Action)
LvglCondition = lvgl_ns.class_("LvglCondition", automation.Condition)
LvglAction = lvgl_ns.class_("LvglAction", automation.Action)

View File

@@ -33,7 +33,7 @@ from ..lv_validation import (
pixels,
size,
)
from ..lvcode import LocalVariable, lv, lv_assign
from ..lvcode import LocalVariable, lv, lv_assign, lv_expr
from ..schemas import STYLE_PROPS, STYLE_REMAP, TEXT_SCHEMA, point_schema
from ..types import LvType, ObjUpdateAction, WidgetType
from . import Widget, get_widgets
@@ -70,15 +70,18 @@ class CanvasType(WidgetType):
width = config[CONF_WIDTH]
height = config[CONF_HEIGHT]
use_alpha = "_ALPHA" if config[CONF_TRANSPARENT] else ""
lv.canvas_set_buffer(
w.obj,
lv.custom_mem_alloc(
literal(f"LV_CANVAS_BUF_SIZE_TRUE_COLOR{use_alpha}({width}, {height})")
),
width,
height,
literal(f"LV_IMG_CF_TRUE_COLOR{use_alpha}"),
buf_size = literal(
f"LV_CANVAS_BUF_SIZE_TRUE_COLOR{use_alpha}({width}, {height})"
)
with LocalVariable("buf", cg.void, lv_expr.custom_mem_alloc(buf_size)) as buf:
cg.add(cg.RawExpression(f"memset({buf}, 0, {buf_size});"))
lv.canvas_set_buffer(
w.obj,
buf,
width,
height,
literal(f"LV_IMG_CF_TRUE_COLOR{use_alpha}"),
)
canvas_spec = CanvasType()

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

@@ -12,241 +12,256 @@ CODEOWNERS = ["@bdm310"]
STATE_ARG = "state"
SDL_KEYMAP = {
"SDLK_UNKNOWN": 0,
"SDLK_FIRST": 0,
"SDLK_BACKSPACE": 8,
"SDLK_TAB": 9,
"SDLK_CLEAR": 12,
"SDLK_RETURN": 13,
"SDLK_PAUSE": 19,
"SDLK_ESCAPE": 27,
"SDLK_SPACE": 32,
"SDLK_EXCLAIM": 33,
"SDLK_QUOTEDBL": 34,
"SDLK_HASH": 35,
"SDLK_DOLLAR": 36,
"SDLK_AMPERSAND": 38,
"SDLK_QUOTE": 39,
"SDLK_LEFTPAREN": 40,
"SDLK_RIGHTPAREN": 41,
"SDLK_ASTERISK": 42,
"SDLK_PLUS": 43,
"SDLK_COMMA": 44,
"SDLK_MINUS": 45,
"SDLK_PERIOD": 46,
"SDLK_SLASH": 47,
"SDLK_0": 48,
"SDLK_1": 49,
"SDLK_2": 50,
"SDLK_3": 51,
"SDLK_4": 52,
"SDLK_5": 53,
"SDLK_6": 54,
"SDLK_7": 55,
"SDLK_8": 56,
"SDLK_9": 57,
"SDLK_COLON": 58,
"SDLK_SEMICOLON": 59,
"SDLK_LESS": 60,
"SDLK_EQUALS": 61,
"SDLK_GREATER": 62,
"SDLK_QUESTION": 63,
"SDLK_AT": 64,
"SDLK_LEFTBRACKET": 91,
"SDLK_BACKSLASH": 92,
"SDLK_RIGHTBRACKET": 93,
"SDLK_CARET": 94,
"SDLK_UNDERSCORE": 95,
"SDLK_BACKQUOTE": 96,
"SDLK_a": 97,
"SDLK_b": 98,
"SDLK_c": 99,
"SDLK_d": 100,
"SDLK_e": 101,
"SDLK_f": 102,
"SDLK_g": 103,
"SDLK_h": 104,
"SDLK_i": 105,
"SDLK_j": 106,
"SDLK_k": 107,
"SDLK_l": 108,
"SDLK_m": 109,
"SDLK_n": 110,
"SDLK_o": 111,
"SDLK_p": 112,
"SDLK_q": 113,
"SDLK_r": 114,
"SDLK_s": 115,
"SDLK_t": 116,
"SDLK_u": 117,
"SDLK_v": 118,
"SDLK_w": 119,
"SDLK_x": 120,
"SDLK_y": 121,
"SDLK_z": 122,
"SDLK_DELETE": 127,
"SDLK_WORLD_0": 160,
"SDLK_WORLD_1": 161,
"SDLK_WORLD_2": 162,
"SDLK_WORLD_3": 163,
"SDLK_WORLD_4": 164,
"SDLK_WORLD_5": 165,
"SDLK_WORLD_6": 166,
"SDLK_WORLD_7": 167,
"SDLK_WORLD_8": 168,
"SDLK_WORLD_9": 169,
"SDLK_WORLD_10": 170,
"SDLK_WORLD_11": 171,
"SDLK_WORLD_12": 172,
"SDLK_WORLD_13": 173,
"SDLK_WORLD_14": 174,
"SDLK_WORLD_15": 175,
"SDLK_WORLD_16": 176,
"SDLK_WORLD_17": 177,
"SDLK_WORLD_18": 178,
"SDLK_WORLD_19": 179,
"SDLK_WORLD_20": 180,
"SDLK_WORLD_21": 181,
"SDLK_WORLD_22": 182,
"SDLK_WORLD_23": 183,
"SDLK_WORLD_24": 184,
"SDLK_WORLD_25": 185,
"SDLK_WORLD_26": 186,
"SDLK_WORLD_27": 187,
"SDLK_WORLD_28": 188,
"SDLK_WORLD_29": 189,
"SDLK_WORLD_30": 190,
"SDLK_WORLD_31": 191,
"SDLK_WORLD_32": 192,
"SDLK_WORLD_33": 193,
"SDLK_WORLD_34": 194,
"SDLK_WORLD_35": 195,
"SDLK_WORLD_36": 196,
"SDLK_WORLD_37": 197,
"SDLK_WORLD_38": 198,
"SDLK_WORLD_39": 199,
"SDLK_WORLD_40": 200,
"SDLK_WORLD_41": 201,
"SDLK_WORLD_42": 202,
"SDLK_WORLD_43": 203,
"SDLK_WORLD_44": 204,
"SDLK_WORLD_45": 205,
"SDLK_WORLD_46": 206,
"SDLK_WORLD_47": 207,
"SDLK_WORLD_48": 208,
"SDLK_WORLD_49": 209,
"SDLK_WORLD_50": 210,
"SDLK_WORLD_51": 211,
"SDLK_WORLD_52": 212,
"SDLK_WORLD_53": 213,
"SDLK_WORLD_54": 214,
"SDLK_WORLD_55": 215,
"SDLK_WORLD_56": 216,
"SDLK_WORLD_57": 217,
"SDLK_WORLD_58": 218,
"SDLK_WORLD_59": 219,
"SDLK_WORLD_60": 220,
"SDLK_WORLD_61": 221,
"SDLK_WORLD_62": 222,
"SDLK_WORLD_63": 223,
"SDLK_WORLD_64": 224,
"SDLK_WORLD_65": 225,
"SDLK_WORLD_66": 226,
"SDLK_WORLD_67": 227,
"SDLK_WORLD_68": 228,
"SDLK_WORLD_69": 229,
"SDLK_WORLD_70": 230,
"SDLK_WORLD_71": 231,
"SDLK_WORLD_72": 232,
"SDLK_WORLD_73": 233,
"SDLK_WORLD_74": 234,
"SDLK_WORLD_75": 235,
"SDLK_WORLD_76": 236,
"SDLK_WORLD_77": 237,
"SDLK_WORLD_78": 238,
"SDLK_WORLD_79": 239,
"SDLK_WORLD_80": 240,
"SDLK_WORLD_81": 241,
"SDLK_WORLD_82": 242,
"SDLK_WORLD_83": 243,
"SDLK_WORLD_84": 244,
"SDLK_WORLD_85": 245,
"SDLK_WORLD_86": 246,
"SDLK_WORLD_87": 247,
"SDLK_WORLD_88": 248,
"SDLK_WORLD_89": 249,
"SDLK_WORLD_90": 250,
"SDLK_WORLD_91": 251,
"SDLK_WORLD_92": 252,
"SDLK_WORLD_93": 253,
"SDLK_WORLD_94": 254,
"SDLK_WORLD_95": 255,
"SDLK_KP0": 256,
"SDLK_KP1": 257,
"SDLK_KP2": 258,
"SDLK_KP3": 259,
"SDLK_KP4": 260,
"SDLK_KP5": 261,
"SDLK_KP6": 262,
"SDLK_KP7": 263,
"SDLK_KP8": 264,
"SDLK_KP9": 265,
"SDLK_KP_PERIOD": 266,
"SDLK_KP_DIVIDE": 267,
"SDLK_KP_MULTIPLY": 268,
"SDLK_KP_MINUS": 269,
"SDLK_KP_PLUS": 270,
"SDLK_KP_ENTER": 271,
"SDLK_KP_EQUALS": 272,
"SDLK_UP": 273,
"SDLK_DOWN": 274,
"SDLK_RIGHT": 275,
"SDLK_LEFT": 276,
"SDLK_INSERT": 277,
"SDLK_HOME": 278,
"SDLK_END": 279,
"SDLK_PAGEUP": 280,
"SDLK_PAGEDOWN": 281,
"SDLK_F1": 282,
"SDLK_F2": 283,
"SDLK_F3": 284,
"SDLK_F4": 285,
"SDLK_F5": 286,
"SDLK_F6": 287,
"SDLK_F7": 288,
"SDLK_F8": 289,
"SDLK_F9": 290,
"SDLK_F10": 291,
"SDLK_F11": 292,
"SDLK_F12": 293,
"SDLK_F13": 294,
"SDLK_F14": 295,
"SDLK_F15": 296,
"SDLK_NUMLOCK": 300,
"SDLK_CAPSLOCK": 301,
"SDLK_SCROLLOCK": 302,
"SDLK_RSHIFT": 303,
"SDLK_LSHIFT": 304,
"SDLK_RCTRL": 305,
"SDLK_LCTRL": 306,
"SDLK_RALT": 307,
"SDLK_LALT": 308,
"SDLK_RMETA": 309,
"SDLK_LMETA": 310,
"SDLK_LSUPER": 311,
"SDLK_RSUPER": 312,
"SDLK_MODE": 313,
"SDLK_COMPOSE": 314,
"SDLK_HELP": 315,
"SDLK_PRINT": 316,
"SDLK_SYSREQ": 317,
"SDLK_BREAK": 318,
"SDLK_MENU": 319,
"SDLK_POWER": 320,
"SDLK_EURO": 321,
"SDLK_UNDO": 322,
}
SDL_KeyCode = cg.global_ns.enum("SDL_KeyCode")
SDL_KEYS = (
"SDLK_UNKNOWN",
"SDLK_RETURN",
"SDLK_ESCAPE",
"SDLK_BACKSPACE",
"SDLK_TAB",
"SDLK_SPACE",
"SDLK_EXCLAIM",
"SDLK_QUOTEDBL",
"SDLK_HASH",
"SDLK_PERCENT",
"SDLK_DOLLAR",
"SDLK_AMPERSAND",
"SDLK_QUOTE",
"SDLK_LEFTPAREN",
"SDLK_RIGHTPAREN",
"SDLK_ASTERISK",
"SDLK_PLUS",
"SDLK_COMMA",
"SDLK_MINUS",
"SDLK_PERIOD",
"SDLK_SLASH",
"SDLK_0",
"SDLK_1",
"SDLK_2",
"SDLK_3",
"SDLK_4",
"SDLK_5",
"SDLK_6",
"SDLK_7",
"SDLK_8",
"SDLK_9",
"SDLK_COLON",
"SDLK_SEMICOLON",
"SDLK_LESS",
"SDLK_EQUALS",
"SDLK_GREATER",
"SDLK_QUESTION",
"SDLK_AT",
"SDLK_LEFTBRACKET",
"SDLK_BACKSLASH",
"SDLK_RIGHTBRACKET",
"SDLK_CARET",
"SDLK_UNDERSCORE",
"SDLK_BACKQUOTE",
"SDLK_a",
"SDLK_b",
"SDLK_c",
"SDLK_d",
"SDLK_e",
"SDLK_f",
"SDLK_g",
"SDLK_h",
"SDLK_i",
"SDLK_j",
"SDLK_k",
"SDLK_l",
"SDLK_m",
"SDLK_n",
"SDLK_o",
"SDLK_p",
"SDLK_q",
"SDLK_r",
"SDLK_s",
"SDLK_t",
"SDLK_u",
"SDLK_v",
"SDLK_w",
"SDLK_x",
"SDLK_y",
"SDLK_z",
"SDLK_CAPSLOCK",
"SDLK_F1",
"SDLK_F2",
"SDLK_F3",
"SDLK_F4",
"SDLK_F5",
"SDLK_F6",
"SDLK_F7",
"SDLK_F8",
"SDLK_F9",
"SDLK_F10",
"SDLK_F11",
"SDLK_F12",
"SDLK_PRINTSCREEN",
"SDLK_SCROLLLOCK",
"SDLK_PAUSE",
"SDLK_INSERT",
"SDLK_HOME",
"SDLK_PAGEUP",
"SDLK_DELETE",
"SDLK_END",
"SDLK_PAGEDOWN",
"SDLK_RIGHT",
"SDLK_LEFT",
"SDLK_DOWN",
"SDLK_UP",
"SDLK_NUMLOCKCLEAR",
"SDLK_KP_DIVIDE",
"SDLK_KP_MULTIPLY",
"SDLK_KP_MINUS",
"SDLK_KP_PLUS",
"SDLK_KP_ENTER",
"SDLK_KP_1",
"SDLK_KP_2",
"SDLK_KP_3",
"SDLK_KP_4",
"SDLK_KP_5",
"SDLK_KP_6",
"SDLK_KP_7",
"SDLK_KP_8",
"SDLK_KP_9",
"SDLK_KP_0",
"SDLK_KP_PERIOD",
"SDLK_APPLICATION",
"SDLK_POWER",
"SDLK_KP_EQUALS",
"SDLK_F13",
"SDLK_F14",
"SDLK_F15",
"SDLK_F16",
"SDLK_F17",
"SDLK_F18",
"SDLK_F19",
"SDLK_F20",
"SDLK_F21",
"SDLK_F22",
"SDLK_F23",
"SDLK_F24",
"SDLK_EXECUTE",
"SDLK_HELP",
"SDLK_MENU",
"SDLK_SELECT",
"SDLK_STOP",
"SDLK_AGAIN",
"SDLK_UNDO",
"SDLK_CUT",
"SDLK_COPY",
"SDLK_PASTE",
"SDLK_FIND",
"SDLK_MUTE",
"SDLK_VOLUMEUP",
"SDLK_VOLUMEDOWN",
"SDLK_KP_COMMA",
"SDLK_KP_EQUALSAS400",
"SDLK_ALTERASE",
"SDLK_SYSREQ",
"SDLK_CANCEL",
"SDLK_CLEAR",
"SDLK_PRIOR",
"SDLK_RETURN2",
"SDLK_SEPARATOR",
"SDLK_OUT",
"SDLK_OPER",
"SDLK_CLEARAGAIN",
"SDLK_CRSEL",
"SDLK_EXSEL",
"SDLK_KP_00",
"SDLK_KP_000",
"SDLK_THOUSANDSSEPARATOR",
"SDLK_DECIMALSEPARATOR",
"SDLK_CURRENCYUNIT",
"SDLK_CURRENCYSUBUNIT",
"SDLK_KP_LEFTPAREN",
"SDLK_KP_RIGHTPAREN",
"SDLK_KP_LEFTBRACE",
"SDLK_KP_RIGHTBRACE",
"SDLK_KP_TAB",
"SDLK_KP_BACKSPACE",
"SDLK_KP_A",
"SDLK_KP_B",
"SDLK_KP_C",
"SDLK_KP_D",
"SDLK_KP_E",
"SDLK_KP_F",
"SDLK_KP_XOR",
"SDLK_KP_POWER",
"SDLK_KP_PERCENT",
"SDLK_KP_LESS",
"SDLK_KP_GREATER",
"SDLK_KP_AMPERSAND",
"SDLK_KP_DBLAMPERSAND",
"SDLK_KP_VERTICALBAR",
"SDLK_KP_DBLVERTICALBAR",
"SDLK_KP_COLON",
"SDLK_KP_HASH",
"SDLK_KP_SPACE",
"SDLK_KP_AT",
"SDLK_KP_EXCLAM",
"SDLK_KP_MEMSTORE",
"SDLK_KP_MEMRECALL",
"SDLK_KP_MEMCLEAR",
"SDLK_KP_MEMADD",
"SDLK_KP_MEMSUBTRACT",
"SDLK_KP_MEMMULTIPLY",
"SDLK_KP_MEMDIVIDE",
"SDLK_KP_PLUSMINUS",
"SDLK_KP_CLEAR",
"SDLK_KP_CLEARENTRY",
"SDLK_KP_BINARY",
"SDLK_KP_OCTAL",
"SDLK_KP_DECIMAL",
"SDLK_KP_HEXADECIMAL",
"SDLK_LCTRL",
"SDLK_LSHIFT",
"SDLK_LALT",
"SDLK_LGUI",
"SDLK_RCTRL",
"SDLK_RSHIFT",
"SDLK_RALT",
"SDLK_RGUI",
"SDLK_MODE",
"SDLK_AUDIONEXT",
"SDLK_AUDIOPREV",
"SDLK_AUDIOSTOP",
"SDLK_AUDIOPLAY",
"SDLK_AUDIOMUTE",
"SDLK_MEDIASELECT",
"SDLK_WWW",
"SDLK_MAIL",
"SDLK_CALCULATOR",
"SDLK_COMPUTER",
"SDLK_AC_SEARCH",
"SDLK_AC_HOME",
"SDLK_AC_BACK",
"SDLK_AC_FORWARD",
"SDLK_AC_STOP",
"SDLK_AC_REFRESH",
"SDLK_AC_BOOKMARKS",
"SDLK_BRIGHTNESSDOWN",
"SDLK_BRIGHTNESSUP",
"SDLK_DISPLAYSWITCH",
"SDLK_KBDILLUMTOGGLE",
"SDLK_KBDILLUMDOWN",
"SDLK_KBDILLUMUP",
"SDLK_EJECT",
"SDLK_SLEEP",
"SDLK_APP1",
"SDLK_APP2",
"SDLK_AUDIOREWIND",
"SDLK_AUDIOFASTFORWARD",
"SDLK_SOFTLEFT",
"SDLK_SOFTRIGHT",
"SDLK_CALL",
"SDLK_ENDCALL",
)
SDL_KEYMAP = {key: getattr(SDL_KeyCode, key) for key in SDL_KEYS}
CONFIG_SCHEMA = (
binary_sensor.binary_sensor_schema(BinarySensor)

View File

@@ -138,6 +138,7 @@ def _concat_nodes_override(values: Iterator[Any]) -> Any:
values = chain(head, values)
raw = "".join([str(v) for v in values])
result = None
try:
# Attempt to parse the concatenated string into a Python literal.
# This allows expressions like "1 + 2" to be evaluated to the integer 3.
@@ -145,11 +146,16 @@ def _concat_nodes_override(values: Iterator[Any]) -> Any:
# fall back to returning the raw string. This is consistent with
# Home Assistant's behavior when evaluating templates
result = literal_eval(raw)
except (ValueError, SyntaxError, MemoryError, TypeError):
pass
else:
if isinstance(result, set):
# Sets are not supported, return raw string
return raw
if not isinstance(result, str):
return result
except (ValueError, SyntaxError, MemoryError, TypeError):
pass
return raw

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,7 @@ 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)); }
void setup() override;
void loop() override;
@@ -17,7 +18,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,8 @@ 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)); }
template<typename F> void set_tilt_lambda(F &&f) { this->tilt_f_.set(std::forward<F>(f)); }
Trigger<> *get_open_trigger() const;
Trigger<> *get_close_trigger() const;
Trigger<> *get_stop_trigger() const;
@@ -26,7 +28,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 +46,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

@@ -11,14 +11,16 @@ static const char *const TAG = "template.lock";
TemplateLock::TemplateLock()
: lock_trigger_(new Trigger<>()), unlock_trigger_(new Trigger<>()), open_trigger_(new Trigger<>()) {}
void TemplateLock::loop() {
void TemplateLock::setup() {
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
if (!val.has_value())
return;
this->disable_loop();
}
this->publish_state(*val);
void TemplateLock::loop() {
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 +47,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 {
@@ -11,9 +12,10 @@ class TemplateLock : public lock::Lock, public Component {
public:
TemplateLock();
void setup() override;
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)); }
Trigger<> *get_lock_trigger() const;
Trigger<> *get_unlock_trigger() const;
Trigger<> *get_open_trigger() const;
@@ -26,7 +28,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

@@ -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) {

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,7 @@ 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)); }
Trigger<> *get_turn_on_trigger() const;
Trigger<> *get_turn_off_trigger() const;
void set_optimistic(bool optimistic);
@@ -28,7 +29,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,7 @@ 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)); }
Trigger<> *get_open_trigger() const;
Trigger<> *get_close_trigger() const;
Trigger<> *get_stop_trigger() const;
@@ -42,7 +43,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

@@ -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

@@ -0,0 +1,51 @@
#pragma once
#include "esphome/core/optional.h"
namespace esphome {
/** Lightweight wrapper for template platform lambdas (stateless function pointers only).
*
* This optimizes template platforms by storing only a function pointer (4 bytes on ESP32)
* instead of std::function (16-32 bytes).
*
* IMPORTANT: This only supports stateless lambdas (no captures). The set_template() method
* is an internal API used by YAML codegen, not intended for external use.
*
* Lambdas must return optional<T> 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 (nullptr check).
*
* @tparam T The return type (e.g., float for sensor values)
* @tparam Args Optional arguments for the lambda
*/
template<typename T, typename... Args> class TemplateLambda {
public:
TemplateLambda() : f_(nullptr) {}
/** Set the lambda function pointer.
* INTERNAL API: Only for use by YAML codegen.
* Only stateless lambdas (no captures) are supported.
*/
void set(optional<T> (*f)(Args...)) { this->f_ = f; }
/** Check if a lambda is set */
bool has_value() const { return this->f_ != nullptr; }
/** Call the lambda, returning nullopt if no lambda is set */
optional<T> operator()(Args &&...args) {
if (this->f_ == nullptr)
return nullopt;
return this->f_(std::forward<Args>(args)...);
}
/** Alias for operator() for compatibility */
optional<T> call(Args &&...args) { return (*this)(std::forward<Args>(args)...); }
protected:
optional<T> (*f_)(Args...); // Function pointer (4 bytes on ESP32)
};
} // namespace esphome

View File

@@ -133,7 +133,6 @@ ignore = [
"PLW1641", # Object does not implement `__hash__` method
"PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable
"PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target
"UP038", # https://github.com/astral-sh/ruff/issues/7871 https://github.com/astral-sh/ruff/pull/16681
]
[tool.ruff.lint.isort]

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.2 # also change in .pre-commit-config.yaml when updating
ruff==0.14.3 # 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

@@ -43,12 +43,14 @@ from enum import StrEnum
from functools import cache
import json
import os
from pathlib import Path
import subprocess
import sys
from typing import Any
from helpers import (
CPP_FILE_EXTENSIONS,
ESPHOME_TESTS_COMPONENTS_PATH,
PYTHON_FILE_EXTENSIONS,
changed_files,
core_changed,
@@ -65,12 +67,17 @@ from helpers import (
parse_test_filename,
root_path,
)
from split_components_for_ci import create_intelligent_batches
# Threshold for splitting clang-tidy jobs
# For small PRs (< 65 files), use nosplit for faster CI
# For large PRs (>= 65 files), use split for better parallelization
CLANG_TIDY_SPLIT_THRESHOLD = 65
# Component test batch size (weighted)
# Isolated components count as 10x, groupable components count as 1x
COMPONENT_TEST_BATCH_SIZE = 40
class Platform(StrEnum):
"""Platform identifiers for memory impact analysis."""
@@ -686,6 +693,22 @@ def main() -> None:
# Determine which C++ unit tests to run
cpp_run_all, cpp_components = determine_cpp_unit_tests(args.branch)
# Split components into batches for CI testing
# This intelligently groups components with similar bus configurations
component_test_batches: list[str]
if changed_components_with_tests:
tests_dir = Path(root_path) / ESPHOME_TESTS_COMPONENTS_PATH
batches, _ = create_intelligent_batches(
components=changed_components_with_tests,
tests_dir=tests_dir,
batch_size=COMPONENT_TEST_BATCH_SIZE,
directly_changed=directly_changed_with_tests,
)
# Convert batches to space-separated strings for CI matrix
component_test_batches = [" ".join(batch) for batch in batches]
else:
component_test_batches = []
output: dict[str, Any] = {
"integration_tests": run_integration,
"clang_tidy": run_clang_tidy,
@@ -703,6 +726,7 @@ def main() -> None:
"memory_impact": memory_impact,
"cpp_unit_tests_run_all": cpp_run_all,
"cpp_unit_tests_components": cpp_components,
"component_test_batches": component_test_batches,
}
# Output as JSON

View File

@@ -62,6 +62,10 @@ def create_intelligent_batches(
) -> tuple[list[list[str]], dict[tuple[str, str], list[str]]]:
"""Create batches optimized for component grouping.
IMPORTANT: This function is called from both split_components_for_ci.py (standalone script)
and determine-jobs.py (integrated into job determination). Be careful when refactoring
to ensure changes work in both contexts.
Args:
components: List of component names to batch
tests_dir: Path to tests/components directory

View File

@@ -18,7 +18,8 @@ def test_gpio_binary_sensor_basic_setup(
assert "new gpio::GPIOBinarySensor();" in main_cpp
assert "App.register_binary_sensor" in main_cpp
assert "bs_gpio->set_use_interrupt(true);" in main_cpp
# set_use_interrupt(true) should NOT be generated (uses C++ default)
assert "bs_gpio->set_use_interrupt(true);" not in main_cpp
assert "bs_gpio->set_interrupt_type(gpio::INTERRUPT_ANY_EDGE);" in main_cpp
@@ -51,8 +52,8 @@ def test_gpio_binary_sensor_esp8266_other_pins_use_interrupt(
"tests/component_tests/gpio/test_gpio_binary_sensor_esp8266.yaml"
)
# GPIO5 should still use interrupts
assert "bs_gpio5->set_use_interrupt(true);" in main_cpp
# GPIO5 should still use interrupts (default, so no setter call)
assert "bs_gpio5->set_use_interrupt(true);" not in main_cpp
assert "bs_gpio5->set_interrupt_type(gpio::INTERRUPT_ANY_EDGE);" in main_cpp

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

@@ -68,5 +68,13 @@ lvgl:
enter_button: pushbutton
group: general
initial_focus: lv_roller
on_draw_start:
- logger.log: draw started
on_draw_end:
- logger.log: draw ended
- lvgl.pause:
- component.update: tft_display
- delay: 60s
- lvgl.resume:
<<: !include common.yaml

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

@@ -14,10 +14,10 @@ display:
binary_sensor:
- platform: sdl
id: key_up
key: SDLK_a
key: SDLK_UP
- platform: sdl
id: key_down
key: SDLK_d
key: SDLK_DOWN
- platform: sdl
id: key_enter
key: SDLK_s
key: SDLK_RETURN

View File

@@ -9,6 +9,13 @@ esphome:
id: template_sens
state: !lambda "return 42.0;"
# Test C++ API: set_template() with stateless lambda (no captures)
# NOTE: set_template() is not intended to be a public API, but we test it to ensure it doesn't break.
- lambda: |-
id(template_sens).set_template([]() -> esphome::optional<float> {
return 123.0f;
});
- datetime.date.set:
id: test_date
date:
@@ -215,6 +222,7 @@ cover:
number:
- platform: template
id: template_number
name: "Template number"
optimistic: true
min_value: 0

View File

@@ -32,6 +32,7 @@ switch:
name: "Test Switch"
id: test_switch
optimistic: true
lambda: return false;
interval:
- interval: 0.5s

View File

@@ -152,6 +152,14 @@ def test_main_all_tests_should_run(
assert output["memory_impact"]["should_run"] == "false"
assert output["cpp_unit_tests_run_all"] is False
assert output["cpp_unit_tests_components"] == ["wifi", "api", "sensor"]
# component_test_batches should be present and be a list of space-separated strings
assert "component_test_batches" in output
assert isinstance(output["component_test_batches"], list)
# Each batch should be a space-separated string of component names
for batch in output["component_test_batches"]:
assert isinstance(batch, str)
# Should contain at least one component (no empty batches)
assert len(batch) > 0
def test_main_no_tests_should_run(
@@ -209,6 +217,9 @@ def test_main_no_tests_should_run(
assert output["memory_impact"]["should_run"] == "false"
assert output["cpp_unit_tests_run_all"] is False
assert output["cpp_unit_tests_components"] == []
# component_test_batches should be empty list
assert "component_test_batches" in output
assert output["component_test_batches"] == []
def test_main_with_branch_argument(

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

View File

@@ -33,3 +33,4 @@ test_list:
{{{ "x", "79"}, { "y", "82"}}}
- '{{{"AA"}}}'
- '"HELLO"'
- '{ 79, 82 }'

View File

@@ -34,3 +34,4 @@ test_list:
{{{ "x", "${ position.x }"}, { "y", "${ position.y }"}}}
- ${ '{{{"AA"}}}' }
- ${ '"HELLO"' }
- '{ ${position.x}, ${position.y} }'

View File

@@ -744,7 +744,7 @@ def test_choose_upload_log_host_ota_local_all_options() -> None:
check_default=None,
purpose=Purpose.UPLOADING,
)
assert result == ["MQTTIP", "test.local"]
assert result == ["MQTTIP"]
@pytest.mark.usefixtures("mock_serial_ports")
@@ -794,7 +794,7 @@ def test_choose_upload_log_host_ota_local_all_options_logging() -> None:
check_default=None,
purpose=Purpose.LOGGING,
)
assert result == ["MQTTIP", "MQTT", "test.local"]
assert result == ["MQTTIP", "MQTT"]
@pytest.mark.usefixtures("mock_no_mqtt_logging")
@@ -1564,7 +1564,7 @@ def test_has_resolvable_address() -> None:
setup_core(
config={CONF_MDNS: {CONF_DISABLED: True}}, address="esphome-device.local"
)
assert has_resolvable_address() is True
assert has_resolvable_address() is False
# Test with mDNS disabled and regular DNS hostname (resolvable)
setup_core(config={CONF_MDNS: {CONF_DISABLED: True}}, address="device.example.com")