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

Compare commits

...

23 Commits

Author SHA1 Message Date
tomaszduda23
55af818629 [nrf52] fix compilation warning (#11656) 2025-11-01 11:18:38 -05:00
J. Nick Koston
c662697ca7 [json] Fix component test compilation errors (#11647) 2025-11-01 11:18:10 -05:00
J. Nick Koston
e28c152298 [cpp_generator] Align isinstance() with codebase style (tuple vs PEP 604) (#11645) 2025-11-01 20:48:58 +11:00
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
75 changed files with 847 additions and 523 deletions

View File

@@ -180,6 +180,7 @@ jobs:
memory_impact: ${{ steps.determine.outputs.memory-impact }} memory_impact: ${{ steps.determine.outputs.memory-impact }}
cpp-unit-tests-run-all: ${{ steps.determine.outputs.cpp-unit-tests-run-all }} cpp-unit-tests-run-all: ${{ steps.determine.outputs.cpp-unit-tests-run-all }}
cpp-unit-tests-components: ${{ steps.determine.outputs.cpp-unit-tests-components }} cpp-unit-tests-components: ${{ steps.determine.outputs.cpp-unit-tests-components }}
component-test-batches: ${{ steps.determine.outputs.component-test-batches }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -214,6 +215,7 @@ jobs:
echo "memory-impact=$(echo "$output" | jq -c '.memory_impact')" >> $GITHUB_OUTPUT 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-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 "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: integration-tests:
name: Run integration tests name: Run integration tests
@@ -458,7 +460,7 @@ jobs:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
strategy: strategy:
fail-fast: false fail-fast: false
max-parallel: 1 max-parallel: 2
matrix: matrix:
include: include:
- id: clang-tidy - id: clang-tidy
@@ -536,59 +538,18 @@ jobs:
run: script/ci-suggest-changes run: script/ci-suggest-changes
if: always() 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: test-build-components-split:
name: Test components batch (${{ matrix.components }}) name: Test components batch (${{ matrix.components }})
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: needs:
- common - common
- determine-jobs - determine-jobs
- test-build-components-splitter
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0 if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0
strategy: strategy:
fail-fast: false fail-fast: false
max-parallel: ${{ (startsWith(github.base_ref, 'beta') || startsWith(github.base_ref, 'release')) && 8 || 4 }} max-parallel: ${{ (startsWith(github.base_ref, 'beta') || startsWith(github.base_ref, 'release')) && 8 || 4 }}
matrix: matrix:
components: ${{ fromJson(needs.test-build-components-splitter.outputs.matrix) }} components: ${{ fromJson(needs.determine-jobs.outputs.component-test-batches) }}
steps: steps:
- name: Show disk space - name: Show disk space
run: | run: |
@@ -980,7 +941,6 @@ jobs:
- clang-tidy-nosplit - clang-tidy-nosplit
- clang-tidy-split - clang-tidy-split
- determine-jobs - determine-jobs
- test-build-components-splitter
- test-build-components-split - test-build-components-split
- pre-commit-ci-lite - pre-commit-ci-lite
- memory-impact-target-branch - memory-impact-target-branch

View File

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

View File

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

View File

@@ -207,14 +207,14 @@ def choose_upload_log_host(
if has_mqtt_logging(): if has_mqtt_logging():
resolved.append("MQTT") 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)) resolved.extend(_resolve_with_cache(CORE.address, purpose))
elif purpose == Purpose.UPLOADING: elif purpose == Purpose.UPLOADING:
if has_ota() and has_mqtt_ip_lookup(): if has_ota() and has_mqtt_ip_lookup():
resolved.append("MQTTIP") 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)) resolved.extend(_resolve_with_cache(CORE.address, purpose))
else: else:
resolved.append(device) 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).""" """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 # Any address (IP, mDNS hostname, or regular DNS hostname) is resolvable
# The resolve_ip_address() function in helpers.py handles all types via AsyncResolver # 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): 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) value = cv.Schema([extra_validators])(value)
if single: if single:
if len(value) != 1: 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[0]
return value return value

View File

@@ -3,6 +3,8 @@
#include "e131_addressable_light_effect.h" #include "e131_addressable_light_effect.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <algorithm>
namespace esphome { namespace esphome {
namespace e131 { namespace e131 {
@@ -76,14 +78,14 @@ void E131Component::loop() {
} }
void E131Component::add_effect(E131AddressableLightEffect *light_effect) { 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; return;
} }
ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(), ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(),
light_effect->get_last_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) { for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) {
join_(universe); join_(universe);
@@ -91,14 +93,17 @@ void E131Component::add_effect(E131AddressableLightEffect *light_effect) {
} }
void E131Component::remove_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; return;
} }
ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(), ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(),
light_effect->get_last_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) { for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) {
leave_(universe); leave_(universe);

View File

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

View File

@@ -94,6 +94,8 @@ async def to_code(config):
) )
use_interrupt = False use_interrupt = False
cg.add(var.set_use_interrupt(use_interrupt))
if use_interrupt: if use_interrupt:
cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE])) 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) resize = config.get(CONF_RESIZE)
if is_svg_file(path): if is_svg_file(path):
# Local import so use of non-SVG files needn't require cairosvg installed # 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 import svg2png
from cairosvg.helpers import PointError
if not resize: if not resize:
resize = (None, None) resize = (None, None)
with open(path, "rb") as file: try:
image = svg2png( with open(path, "rb") as file:
file_obj=file, image = svg2png(
output_width=resize[0], file_obj=file,
output_height=resize[1], output_width=resize[0],
) output_height=resize[1],
image = Image.open(io.BytesIO(image)) )
width, height = image.size 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: else:
image = Image.open(path) image = Image.open(path)
width, height = image.size width, height = image.size

View File

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

View File

@@ -58,7 +58,7 @@ from .types import (
FontEngine, FontEngine,
IdleTrigger, IdleTrigger,
ObjUpdateAction, ObjUpdateAction,
PauseTrigger, PlainTrigger,
lv_font_t, lv_font_t,
lv_group_t, lv_group_t,
lv_style_t, lv_style_t,
@@ -151,6 +151,13 @@ for w_type in WIDGET_TYPES.values():
create_modify_schema(w_type), create_modify_schema(w_type),
)(update_to_code) )(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): def as_macro(macro, value):
if value is None: if value is None:
@@ -244,9 +251,9 @@ def final_validation(configs):
for w in refreshed_widgets: for w in refreshed_widgets:
path = global_config.get_path_for_id(w) path = global_config.get_path_for_id(w)
widget_conf = global_config.get_config_for_path(path[:-1]) 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( 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 conf[CONF_TRIGGER_ID], lv_component, templ
) )
await build_automation(idle_trigger, [], conf) await build_automation(idle_trigger, [], conf)
for conf in config.get(df.CONF_ON_PAUSE, ()): for trigger_name in SIMPLE_TRIGGERS:
pause_trigger = cg.new_Pvariable( if conf := config.get(trigger_name):
conf[CONF_TRIGGER_ID], lv_component, True trigger_var = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
) await build_automation(trigger_var, [], conf)
await build_automation(pause_trigger, [], conf) cg.add(
for conf in config.get(df.CONF_ON_RESUME, ()): getattr(
resume_trigger = cg.new_Pvariable( lv_component,
conf[CONF_TRIGGER_ID], lv_component, False f"set_{trigger_name.removeprefix('on_')}_trigger",
) )(trigger_var)
await build_automation(resume_trigger, [], conf) )
await add_on_boot_triggers(config.get(CONF_ON_BOOT, ())) await add_on_boot_triggers(config.get(CONF_ON_BOOT, ()))
# This must be done after all widgets are created # 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.Optional(x): validate_automation(
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PauseTrigger), {
} cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PlainTrigger),
), },
cv.Optional(df.CONF_ON_RESUME): validate_automation( single=True,
{ )
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PauseTrigger), for x in SIMPLE_TRIGGERS
} },
),
cv.Exclusive(df.CONF_WIDGETS, CONF_PAGES): cv.ensure_list( cv.Exclusive(df.CONF_WIDGETS, CONF_PAGES): cv.ensure_list(
WIDGET_SCHEMA 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 # 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. # templated. First filter out common style properties.
config = {k: v for k, v in widget.config.items() if k not in ALL_STYLES} 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) await widget.type.to_code(widget, config)
if ( if (
widget.type.w_type.value_property is not None 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( lvgl_static.add_event_cb(
widget.obj, widget.obj,
await pressed_ctx.get_lambda(), await pressed_ctx.get_lambda(),
LV_EVENT.PRESSING, LV_EVENT.PRESSED,
LV_EVENT.RELEASED, 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 import logging
from typing import TYPE_CHECKING, Any
from esphome import codegen as cg, config_validation as cv from esphome import codegen as cg, config_validation as cv
from esphome.const import CONF_ITEMS 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_generator import LambdaExpression, MockObj
from esphome.cpp_types import uint32 from esphome.cpp_types import uint32
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.types import Expression, SafeExpType
from .helpers import requires_component from .helpers import requires_component
@@ -42,7 +44,13 @@ def static_cast(type, value):
def call_lambda(lamb: LambdaExpression): def call_lambda(lamb: LambdaExpression):
expr = lamb.content.strip() expr = lamb.content.strip()
if expr.startswith("return") and expr.endswith(";"): 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}()" return f"{lamb}()"
@@ -65,10 +73,20 @@ class LValidator:
return cv.returning_lambda(value) return cv.returning_lambda(value)
return self.validator(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: if value is None:
return None return None
if isinstance(value, Lambda): 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( return cg.RawExpression(
call_lambda( call_lambda(
await cg.process_lambda(value, args, return_type=self.rtype) await cg.process_lambda(value, args, return_type=self.rtype)
@@ -465,6 +483,8 @@ CONF_MSGBOXES = "msgboxes"
CONF_OBJ = "obj" CONF_OBJ = "obj"
CONF_ONE_CHECKED = "one_checked" CONF_ONE_CHECKED = "one_checked"
CONF_ONE_LINE = "one_line" 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_PAUSE = "on_pause"
CONF_ON_RESUME = "on_resume" CONF_ON_RESUME = "on_resume"
CONF_ON_SELECT = "on_select" CONF_ON_SELECT = "on_select"

View File

@@ -1,3 +1,5 @@
from typing import TYPE_CHECKING, Any
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import image from esphome.components import image
from esphome.components.color import CONF_HEX, ColorStruct, from_rgbw 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.cpp_types import ESPTime, int32, uint32
from esphome.helpers import cpp_string_escape from esphome.helpers import cpp_string_escape
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.types import Expression, SafeExpType
from . import types as ty from . import types as ty
from .defines import ( from .defines import (
@@ -388,11 +391,23 @@ class TextValidator(LValidator):
return value return value
return super().__call__(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 isinstance(value, dict):
if format_str := value.get(CONF_FORMAT): if format_str := value.get(CONF_FORMAT):
args = [str(x) for x in value[CONF_ARGS]] str_args = [str(x) for x in value[CONF_ARGS]]
arg_expr = cg.RawExpression(",".join(args)) arg_expr = cg.RawExpression(",".join(str_args))
format_str = cpp_string_escape(format_str) format_str = cpp_string_escape(format_str)
return literal(f"str_sprintf({format_str}, {arg_expr}).c_str()") return literal(f"str_sprintf({format_str}, {arg_expr}).c_str()")
if time_format := value.get(CONF_TIME_FORMAT): if time_format := value.get(CONF_TIME_FORMAT):

View File

@@ -164,6 +164,9 @@ class LambdaContext(CodeContext):
code_text.append(text) code_text.append(text)
return code_text return code_text
def get_automation_parameters(self) -> list[tuple[SafeExpType, str]]:
return self.parameters
async def __aenter__(self): async def __aenter__(self):
await super().__aenter__() await super().__aenter__()
add_line_marks(self.where) add_line_marks(self.where)
@@ -178,9 +181,8 @@ class LvContext(LambdaContext):
added_lambda_count = 0 added_lambda_count = 0
def __init__(self, args=None): def __init__(self):
self.args = args or LVGL_COMP_ARG super().__init__(parameters=LVGL_COMP_ARG)
super().__init__(parameters=self.args)
async def __aexit__(self, exc_type, exc_val, exc_tb): async def __aexit__(self, exc_type, exc_val, exc_tb):
await super().__aexit__(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) cg.add(expression)
return 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): def __call__(self, *args):
return self.add(*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; 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_api_event; // NOLINT
lv_event_code_t lv_update_event; // NOLINT lv_event_code_t lv_update_event; // NOLINT
void LvglComponent::dump_config() { 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_disp_trig_activity(this->disp_); // resets the inactivity time
lv_obj_invalidate(lv_scr_act()); 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() { 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 #ifdef USE_LVGL_TOUCHSCREEN
LVTouchListener::LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time, LvglComponent *parent) { LVTouchListener::LVTouchListener(uint16_t long_press_time, uint16_t long_press_repeat_time, LvglComponent *parent) {
this->set_parent(parent); this->set_parent(parent);
@@ -474,6 +482,12 @@ void LvglComponent::setup() {
return; 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 #if LV_USE_LOG
lv_log_register_print_cb([](const char *buf) { lv_log_register_print_cb([](const char *buf) {
auto next = strchr(buf, ')'); auto next = strchr(buf, ')');
@@ -502,8 +516,9 @@ void LvglComponent::loop() {
if (this->paused_) { if (this->paused_) {
if (this->show_snow_) if (this->show_snow_)
this->write_random_(); this->write_random_();
} else {
lv_timer_handler_run_in_period(5);
} }
lv_timer_handler_run_in_period(5);
} }
#ifdef USE_LVGL_ANIMIMG #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) { void add_on_idle_callback(std::function<void(uint32_t)> &&callback) {
this->idle_callbacks_.add(std::move(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; void dump_config() override;
bool is_idle(uint32_t idle_ms) { return lv_disp_get_inactive_time(this->disp_) > idle_ms; } 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_; } lv_disp_t *get_disp() { return this->disp_; }
@@ -213,12 +215,20 @@ class LvglComponent : public PollingComponent {
size_t draw_rounding{2}; size_t draw_rounding{2};
display::DisplayRotation rotation{display::DISPLAY_ROTATION_0_DEGREES}; 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: 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 write_random_();
void draw_buffer_(const lv_area_t *area, lv_color_t *ptr); 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); void flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p);
std::vector<display::Display *> displays_{}; std::vector<display::Display *> displays_{};
size_t buffer_frac_{1}; size_t buffer_frac_{1};
bool full_refresh_{}; bool full_refresh_{};
@@ -235,7 +245,10 @@ class LvglComponent : public PollingComponent {
std::map<lv_group_t *, lv_obj_t *> focus_marks_{}; std::map<lv_group_t *, lv_obj_t *> focus_marks_{};
CallbackManager<void(uint32_t)> idle_callbacks_{}; 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_{}; lv_color_t *rotate_buf_{};
}; };
@@ -248,14 +261,6 @@ class IdleTrigger : public Trigger<> {
bool is_idle_{}; 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> { template<typename... Ts> class LvglAction : public Action<Ts...>, public Parented<LvglComponent> {
public: public:
explicit LvglAction(std::function<void(LvglComponent *)> &&lamb) : action_(std::move(lamb)) {} 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 ( from ..lvcode import (
API_EVENT, API_EVENT,
EVENT_ARG, EVENT_ARG,
LVGL_COMP_ARG,
UPDATE_EVENT, UPDATE_EVENT,
LambdaContext, LambdaContext,
LvContext, LvContext,
@@ -30,7 +29,7 @@ async def to_code(config):
await wait_for_widgets() await wait_for_widgets()
async with LambdaContext(EVENT_ARG) as lamb: async with LambdaContext(EVENT_ARG) as lamb:
lv_add(sensor.publish_state(widget.get_value())) lv_add(sensor.publish_state(widget.get_value()))
async with LvContext(LVGL_COMP_ARG): async with LvContext():
lv_add( lv_add(
lvgl_static.add_event_cb( lvgl_static.add_event_cb(
widget.obj, widget.obj,

View File

@@ -3,6 +3,7 @@ import sys
from esphome import automation, codegen as cg from esphome import automation, codegen as cg
from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_TEXT, CONF_VALUE from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_TEXT, CONF_VALUE
from esphome.cpp_generator import MockObj, MockObjClass from esphome.cpp_generator import MockObj, MockObjClass
from esphome.cpp_types import esphome_ns
from .defines import lvgl_ns from .defines import lvgl_ns
from .lvcode import lv_expr 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_indev_type_t = cg.global_ns.enum("lv_indev_type_t")
lv_key_t = cg.global_ns.enum("lv_key_t") lv_key_t = cg.global_ns.enum("lv_key_t")
FontEngine = lvgl_ns.class_("FontEngine") 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()) IdleTrigger = lvgl_ns.class_("IdleTrigger", automation.Trigger.template())
PauseTrigger = lvgl_ns.class_("PauseTrigger", automation.Trigger.template())
ObjUpdateAction = lvgl_ns.class_("ObjUpdateAction", automation.Action) ObjUpdateAction = lvgl_ns.class_("ObjUpdateAction", automation.Action)
LvglCondition = lvgl_ns.class_("LvglCondition", automation.Condition) LvglCondition = lvgl_ns.class_("LvglCondition", automation.Condition)
LvglAction = lvgl_ns.class_("LvglAction", automation.Action) LvglAction = lvgl_ns.class_("LvglAction", automation.Action)

View File

@@ -33,7 +33,7 @@ from ..lv_validation import (
pixels, pixels,
size, 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 ..schemas import STYLE_PROPS, STYLE_REMAP, TEXT_SCHEMA, point_schema
from ..types import LvType, ObjUpdateAction, WidgetType from ..types import LvType, ObjUpdateAction, WidgetType
from . import Widget, get_widgets from . import Widget, get_widgets
@@ -70,15 +70,18 @@ class CanvasType(WidgetType):
width = config[CONF_WIDTH] width = config[CONF_WIDTH]
height = config[CONF_HEIGHT] height = config[CONF_HEIGHT]
use_alpha = "_ALPHA" if config[CONF_TRANSPARENT] else "" use_alpha = "_ALPHA" if config[CONF_TRANSPARENT] else ""
lv.canvas_set_buffer( buf_size = literal(
w.obj, f"LV_CANVAS_BUF_SIZE_TRUE_COLOR{use_alpha}({width}, {height})"
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}"),
) )
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() canvas_spec = CanvasType()

View File

@@ -1,3 +1,5 @@
import logging
from esphome import automation, pins from esphome import automation, pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import esp32, esp32_rmt, remote_base from esphome.components import esp32, esp32_rmt, remote_base
@@ -18,9 +20,12 @@ from esphome.const import (
) )
from esphome.core import CORE from esphome.core import CORE
_LOGGER = logging.getLogger(__name__)
AUTO_LOAD = ["remote_base"] AUTO_LOAD = ["remote_base"]
CONF_EOT_LEVEL = "eot_level" CONF_EOT_LEVEL = "eot_level"
CONF_NON_BLOCKING = "non_blocking"
CONF_ON_TRANSMIT = "on_transmit" CONF_ON_TRANSMIT = "on_transmit"
CONF_ON_COMPLETE = "on_complete" CONF_ON_COMPLETE = "on_complete"
CONF_TRANSMITTER_ID = remote_base.CONF_TRANSMITTER_ID CONF_TRANSMITTER_ID = remote_base.CONF_TRANSMITTER_ID
@@ -65,11 +70,25 @@ CONFIG_SCHEMA = cv.Schema(
esp32_c6=48, esp32_c6=48,
esp32_h2=48, esp32_h2=48,
): cv.All(cv.only_on_esp32, cv.int_range(min=2)), ): 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_TRANSMIT): automation.validate_automation(single=True),
cv.Optional(CONF_ON_COMPLETE): automation.validate_automation(single=True), cv.Optional(CONF_ON_COMPLETE): automation.validate_automation(single=True),
} }
).extend(cv.COMPONENT_SCHEMA) ).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( DIGITAL_WRITE_ACTION_SCHEMA = cv.maybe_simple_value(
{ {
cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(RemoteTransmitterComponent), cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(RemoteTransmitterComponent),
@@ -95,6 +114,7 @@ async def to_code(config):
if CORE.is_esp32: if CORE.is_esp32:
var = cg.new_Pvariable(config[CONF_ID], pin) var = cg.new_Pvariable(config[CONF_ID], pin)
cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS])) 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: if CONF_CLOCK_RESOLUTION in config:
cg.add(var.set_clock_resolution(config[CONF_CLOCK_RESOLUTION])) cg.add(var.set_clock_resolution(config[CONF_CLOCK_RESOLUTION]))
if CONF_USE_DMA in config: if CONF_USE_DMA in config:

View File

@@ -54,6 +54,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase,
#if defined(USE_ESP32) #if defined(USE_ESP32)
void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; } 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_eot_level(bool eot_level) { this->eot_level_ = eot_level; }
void set_non_blocking(bool non_blocking) { this->non_blocking_ = non_blocking; }
#endif #endif
Trigger<> *get_transmit_trigger() const { return this->transmit_trigger_; }; Trigger<> *get_transmit_trigger() const { return this->transmit_trigger_; };
@@ -74,6 +75,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase,
#ifdef USE_ESP32 #ifdef USE_ESP32
void configure_rmt_(); void configure_rmt_();
void wait_for_rmt_();
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
RemoteTransmitterComponentStore store_{}; RemoteTransmitterComponentStore store_{};
@@ -90,6 +92,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase,
esp_err_t error_code_{ESP_OK}; esp_err_t error_code_{ESP_OK};
std::string error_string_{""}; std::string error_string_{""};
bool inverted_{false}; bool inverted_{false};
bool non_blocking_{false};
#endif #endif
uint8_t carrier_duty_percent_; 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) #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
uint64_t total_duration = 0;
if (this->is_failed()) { if (this->is_failed()) {
return; 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()) { if (this->current_carrier_frequency_ != this->temp_.get_carrier_frequency()) {
this->current_carrier_frequency_ = this->temp_.get_carrier_frequency(); this->current_carrier_frequency_ = this->temp_.get_carrier_frequency();
this->configure_rmt_(); 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 // encode any delay at the start of the buffer to simplify the encoder callback
// this will be skipped the first time around // 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)); send_wait = this->from_microseconds_(static_cast<uint32_t>(send_wait));
while (send_wait > 0) { while (send_wait > 0) {
int32_t duration = std::min(send_wait, uint32_t(RMT_SYMBOL_DURATION_MAX)); 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) { if (!level) {
value = -value; value = -value;
} }
total_duration += value * send_times;
value = this->from_microseconds_(static_cast<uint32_t>(value)); value = this->from_microseconds_(static_cast<uint32_t>(value));
while (value > 0) { while (value > 0) {
int32_t duration = std::min(value, int32_t(RMT_SYMBOL_DURATION_MAX)); 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 { } else {
this->status_clear_warning(); 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 #else
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/template_lambda.h"
#include "esphome/components/binary_sensor/binary_sensor.h" #include "esphome/components/binary_sensor/binary_sensor.h"
namespace esphome { namespace esphome {
@@ -8,7 +9,7 @@ namespace template_ {
class TemplateBinarySensor : public Component, public binary_sensor::BinarySensor { class TemplateBinarySensor : public Component, public binary_sensor::BinarySensor {
public: 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 setup() override;
void loop() 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; } float get_setup_priority() const override { return setup_priority::HARDWARE; }
protected: protected:
optional<optional<bool> (*)()> f_; TemplateLambda<bool> f_;
}; };
} // namespace template_ } // namespace template_

View File

@@ -33,28 +33,27 @@ void TemplateCover::setup() {
break; break;
} }
} }
if (!this->state_f_.has_value() && !this->tilt_f_.has_value())
this->disable_loop();
} }
void TemplateCover::loop() { void TemplateCover::loop() {
bool changed = false; bool changed = false;
if (this->state_f_.has_value()) { auto s = this->state_f_();
auto s = (*this->state_f_)(); if (s.has_value()) {
if (s.has_value()) { auto pos = clamp(*s, 0.0f, 1.0f);
auto pos = clamp(*s, 0.0f, 1.0f); if (pos != this->position) {
if (pos != this->position) { this->position = pos;
this->position = pos; changed = true;
changed = true;
}
} }
} }
if (this->tilt_f_.has_value()) {
auto s = (*this->tilt_f_)(); auto tilt = this->tilt_f_();
if (s.has_value()) { if (tilt.has_value()) {
auto tilt = clamp(*s, 0.0f, 1.0f); auto tilt_val = clamp(*tilt, 0.0f, 1.0f);
if (tilt != this->tilt) { if (tilt_val != this->tilt) {
this->tilt = tilt; this->tilt = tilt_val;
changed = true; changed = true;
}
} }
} }
@@ -63,7 +62,6 @@ void TemplateCover::loop() {
} }
void TemplateCover::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } 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_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; } float TemplateCover::get_setup_priority() const { return setup_priority::HARDWARE; }
Trigger<> *TemplateCover::get_open_trigger() const { return this->open_trigger_; } Trigger<> *TemplateCover::get_open_trigger() const { return this->open_trigger_; }
Trigger<> *TemplateCover::get_close_trigger() const { return this->close_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_position_trigger() const { return this->position_trigger_; }
Trigger<float> *TemplateCover::get_tilt_trigger() const { return this->tilt_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_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_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; }
void TemplateCover::set_has_position(bool has_position) { this->has_position_ = has_position; } 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/component.h"
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "esphome/core/template_lambda.h"
#include "esphome/components/cover/cover.h" #include "esphome/components/cover/cover.h"
namespace esphome { namespace esphome {
@@ -17,7 +18,8 @@ class TemplateCover : public cover::Cover, public Component {
public: public:
TemplateCover(); 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_open_trigger() const;
Trigger<> *get_close_trigger() const; Trigger<> *get_close_trigger() const;
Trigger<> *get_stop_trigger() const; Trigger<> *get_stop_trigger() const;
@@ -26,7 +28,6 @@ class TemplateCover : public cover::Cover, public Component {
Trigger<float> *get_tilt_trigger() const; Trigger<float> *get_tilt_trigger() const;
void set_optimistic(bool optimistic); void set_optimistic(bool optimistic);
void set_assumed_state(bool assumed_state); 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_stop(bool has_stop);
void set_has_position(bool has_position); void set_has_position(bool has_position);
void set_has_tilt(bool has_tilt); void set_has_tilt(bool has_tilt);
@@ -45,8 +46,8 @@ class TemplateCover : public cover::Cover, public Component {
void stop_prev_trigger_(); void stop_prev_trigger_();
TemplateCoverRestoreMode restore_mode_{COVER_RESTORE}; TemplateCoverRestoreMode restore_mode_{COVER_RESTORE};
optional<optional<float> (*)()> state_f_; TemplateLambda<float> state_f_;
optional<optional<float> (*)()> tilt_f_; TemplateLambda<float> tilt_f_;
bool assumed_state_{false}; bool assumed_state_{false};
bool optimistic_{false}; bool optimistic_{false};
Trigger<> *open_trigger_; Trigger<> *open_trigger_;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,14 +11,16 @@ static const char *const TAG = "template.lock";
TemplateLock::TemplateLock() TemplateLock::TemplateLock()
: lock_trigger_(new Trigger<>()), unlock_trigger_(new Trigger<>()), open_trigger_(new Trigger<>()) {} : lock_trigger_(new Trigger<>()), unlock_trigger_(new Trigger<>()), open_trigger_(new Trigger<>()) {}
void TemplateLock::loop() { void TemplateLock::setup() {
if (!this->f_.has_value()) if (!this->f_.has_value())
return; this->disable_loop();
auto val = (*this->f_)(); }
if (!val.has_value())
return;
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) { void TemplateLock::control(const lock::LockCall &call) {
if (this->prev_trigger_ != nullptr) { if (this->prev_trigger_ != nullptr) {
@@ -45,7 +47,6 @@ void TemplateLock::open_latch() {
this->open_trigger_->trigger(); this->open_trigger_->trigger();
} }
void TemplateLock::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } 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; } float TemplateLock::get_setup_priority() const { return setup_priority::HARDWARE; }
Trigger<> *TemplateLock::get_lock_trigger() const { return this->lock_trigger_; } Trigger<> *TemplateLock::get_lock_trigger() const { return this->lock_trigger_; }
Trigger<> *TemplateLock::get_unlock_trigger() const { return this->unlock_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/component.h"
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "esphome/core/template_lambda.h"
#include "esphome/components/lock/lock.h" #include "esphome/components/lock/lock.h"
namespace esphome { namespace esphome {
@@ -11,9 +12,10 @@ class TemplateLock : public lock::Lock, public Component {
public: public:
TemplateLock(); TemplateLock();
void setup() override;
void dump_config() 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_lock_trigger() const;
Trigger<> *get_unlock_trigger() const; Trigger<> *get_unlock_trigger() const;
Trigger<> *get_open_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 control(const lock::LockCall &call) override;
void open_latch() override; void open_latch() override;
optional<optional<lock::LockState> (*)()> f_; TemplateLambda<lock::LockState> f_;
bool optimistic_{false}; bool optimistic_{false};
Trigger<> *lock_trigger_; Trigger<> *lock_trigger_;
Trigger<> *unlock_trigger_; Trigger<> *unlock_trigger_;

View File

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

View File

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

View File

@@ -31,16 +31,14 @@ void TemplateSelect::update() {
if (!this->f_.has_value()) if (!this->f_.has_value())
return; return;
auto val = (*this->f_)(); auto val = this->f_();
if (!val.has_value()) if (val.has_value()) {
return; if (!this->has_option(*val)) {
ESP_LOGE(TAG, "Lambda returned an invalid option: %s", (*val).c_str());
if (!this->has_option(*val)) { return;
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) { void TemplateSelect::control(const std::string &value) {

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/template_lambda.h"
#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensor/sensor.h"
namespace esphome { namespace esphome {
@@ -8,7 +9,7 @@ namespace template_ {
class TemplateSensor : public sensor::Sensor, public PollingComponent { class TemplateSensor : public sensor::Sensor, public PollingComponent {
public: 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; void update() override;
@@ -17,7 +18,7 @@ class TemplateSensor : public sensor::Sensor, public PollingComponent {
float get_setup_priority() const override; float get_setup_priority() const override;
protected: protected:
optional<optional<float> (*)()> f_; TemplateLambda<float> f_;
}; };
} // namespace template_ } // 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<>()) {} TemplateSwitch::TemplateSwitch() : turn_on_trigger_(new Trigger<>()), turn_off_trigger_(new Trigger<>()) {}
void TemplateSwitch::loop() { void TemplateSwitch::loop() {
if (!this->f_.has_value()) auto s = this->f_();
return; if (s.has_value()) {
auto s = (*this->f_)(); this->publish_state(*s);
if (!s.has_value()) }
return;
this->publish_state(*s);
} }
void TemplateSwitch::write_state(bool state) { void TemplateSwitch::write_state(bool state) {
if (this->prev_trigger_ != nullptr) { if (this->prev_trigger_ != nullptr) {
@@ -35,11 +32,13 @@ void TemplateSwitch::write_state(bool state) {
} }
void TemplateSwitch::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void TemplateSwitch::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
bool TemplateSwitch::assumed_state() { return this->assumed_state_; } 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; } 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_on_trigger() const { return this->turn_on_trigger_; }
Trigger<> *TemplateSwitch::get_turn_off_trigger() const { return this->turn_off_trigger_; } Trigger<> *TemplateSwitch::get_turn_off_trigger() const { return this->turn_off_trigger_; }
void TemplateSwitch::setup() { void TemplateSwitch::setup() {
if (!this->f_.has_value())
this->disable_loop();
optional<bool> initial_state = this->get_initial_state_with_restore_mode(); optional<bool> initial_state = this->get_initial_state_with_restore_mode();
if (initial_state.has_value()) { if (initial_state.has_value()) {

View File

@@ -2,6 +2,7 @@
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "esphome/core/template_lambda.h"
#include "esphome/components/switch/switch.h" #include "esphome/components/switch/switch.h"
namespace esphome { namespace esphome {
@@ -14,7 +15,7 @@ class TemplateSwitch : public switch_::Switch, public Component {
void setup() override; void setup() override;
void dump_config() 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_on_trigger() const;
Trigger<> *get_turn_off_trigger() const; Trigger<> *get_turn_off_trigger() const;
void set_optimistic(bool optimistic); void set_optimistic(bool optimistic);
@@ -28,7 +29,7 @@ class TemplateSwitch : public switch_::Switch, public Component {
void write_state(bool state) override; void write_state(bool state) override;
optional<optional<bool> (*)()> f_; TemplateLambda<bool> f_;
bool optimistic_{false}; bool optimistic_{false};
bool assumed_state_{false}; bool assumed_state_{false};
Trigger<> *turn_on_trigger_; Trigger<> *turn_on_trigger_;

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "esphome/core/template_lambda.h"
#include "esphome/components/text_sensor/text_sensor.h" #include "esphome/components/text_sensor/text_sensor.h"
namespace esphome { namespace esphome {
@@ -9,7 +10,7 @@ namespace template_ {
class TemplateTextSensor : public text_sensor::TextSensor, public PollingComponent { class TemplateTextSensor : public text_sensor::TextSensor, public PollingComponent {
public: 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; void update() override;
@@ -18,7 +19,7 @@ class TemplateTextSensor : public text_sensor::TextSensor, public PollingCompone
void dump_config() override; void dump_config() override;
protected: protected:
optional<optional<std::string> (*)()> f_{}; TemplateLambda<std::string> f_{};
}; };
} // namespace template_ } // namespace template_

View File

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

View File

@@ -2,6 +2,7 @@
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "esphome/core/template_lambda.h"
#include "esphome/components/valve/valve.h" #include "esphome/components/valve/valve.h"
namespace esphome { namespace esphome {
@@ -17,7 +18,7 @@ class TemplateValve : public valve::Valve, public Component {
public: public:
TemplateValve(); 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_open_trigger() const;
Trigger<> *get_close_trigger() const; Trigger<> *get_close_trigger() const;
Trigger<> *get_stop_trigger() const; Trigger<> *get_stop_trigger() const;
@@ -42,7 +43,7 @@ class TemplateValve : public valve::Valve, public Component {
void stop_prev_trigger_(); void stop_prev_trigger_();
TemplateValveRestoreMode restore_mode_{VALVE_NO_RESTORE}; TemplateValveRestoreMode restore_mode_{VALVE_NO_RESTORE};
optional<optional<float> (*)()> state_f_; TemplateLambda<float> state_f_;
bool assumed_state_{false}; bool assumed_state_{false};
bool optimistic_{false}; bool optimistic_{false};
Trigger<> *open_trigger_; Trigger<> *open_trigger_;

View File

@@ -3,7 +3,7 @@
#include <zephyr/kernel.h> #include <zephyr/kernel.h>
#include <zephyr/drivers/watchdog.h> #include <zephyr/drivers/watchdog.h>
#include <zephyr/sys/reboot.h> #include <zephyr/sys/reboot.h>
#include <zephyr/random/rand32.h> #include <zephyr/random/random.h>
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"

View File

@@ -319,6 +319,9 @@ def iter_ids(config, path=None):
yield from iter_ids(item, path + [i]) yield from iter_ids(item, path + [i])
elif isinstance(config, dict): elif isinstance(config, dict):
for key, value in config.items(): 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): if isinstance(key, core.ID):
yield key, path yield key, path
yield from iter_ids(value, path + [key]) 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

@@ -350,7 +350,7 @@ def safe_exp(obj: SafeExpType) -> Expression:
return IntLiteral(int(obj.total_seconds)) return IntLiteral(int(obj.total_seconds))
if isinstance(obj, TimePeriodMinutes): if isinstance(obj, TimePeriodMinutes):
return IntLiteral(int(obj.total_minutes)) return IntLiteral(int(obj.total_minutes))
if isinstance(obj, tuple | list): if isinstance(obj, (tuple, list)):
return ArrayInitializer(*[safe_exp(o) for o in obj]) return ArrayInitializer(*[safe_exp(o) for o in obj])
if obj is bool: if obj is bool:
return bool_ return bool_

View File

@@ -133,7 +133,6 @@ ignore = [
"PLW1641", # Object does not implement `__hash__` method "PLW1641", # Object does not implement `__hash__` method
"PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable "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 "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] [tool.ruff.lint.isort]

View File

@@ -1,6 +1,6 @@
pylint==4.0.2 pylint==4.0.2
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating 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 pyupgrade==3.21.0 # also change in .pre-commit-config.yaml when updating
pre-commit pre-commit

View File

@@ -43,12 +43,14 @@ from enum import StrEnum
from functools import cache from functools import cache
import json import json
import os import os
from pathlib import Path
import subprocess import subprocess
import sys import sys
from typing import Any from typing import Any
from helpers import ( from helpers import (
CPP_FILE_EXTENSIONS, CPP_FILE_EXTENSIONS,
ESPHOME_TESTS_COMPONENTS_PATH,
PYTHON_FILE_EXTENSIONS, PYTHON_FILE_EXTENSIONS,
changed_files, changed_files,
core_changed, core_changed,
@@ -65,12 +67,17 @@ from helpers import (
parse_test_filename, parse_test_filename,
root_path, root_path,
) )
from split_components_for_ci import create_intelligent_batches
# Threshold for splitting clang-tidy jobs # Threshold for splitting clang-tidy jobs
# For small PRs (< 65 files), use nosplit for faster CI # For small PRs (< 65 files), use nosplit for faster CI
# For large PRs (>= 65 files), use split for better parallelization # For large PRs (>= 65 files), use split for better parallelization
CLANG_TIDY_SPLIT_THRESHOLD = 65 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): class Platform(StrEnum):
"""Platform identifiers for memory impact analysis.""" """Platform identifiers for memory impact analysis."""
@@ -686,6 +693,22 @@ def main() -> None:
# Determine which C++ unit tests to run # Determine which C++ unit tests to run
cpp_run_all, cpp_components = determine_cpp_unit_tests(args.branch) 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] = { output: dict[str, Any] = {
"integration_tests": run_integration, "integration_tests": run_integration,
"clang_tidy": run_clang_tidy, "clang_tidy": run_clang_tidy,
@@ -703,6 +726,7 @@ def main() -> None:
"memory_impact": memory_impact, "memory_impact": memory_impact,
"cpp_unit_tests_run_all": cpp_run_all, "cpp_unit_tests_run_all": cpp_run_all,
"cpp_unit_tests_components": cpp_components, "cpp_unit_tests_components": cpp_components,
"component_test_batches": component_test_batches,
} }
# Output as JSON # Output as JSON

View File

@@ -62,6 +62,10 @@ def create_intelligent_batches(
) -> tuple[list[list[str]], dict[tuple[str, str], list[str]]]: ) -> tuple[list[list[str]], dict[tuple[str, str], list[str]]]:
"""Create batches optimized for component grouping. """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: Args:
components: List of component names to batch components: List of component names to batch
tests_dir: Path to tests/components directory 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 "new gpio::GPIOBinarySensor();" in main_cpp
assert "App.register_binary_sensor" 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 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" "tests/component_tests/gpio/test_gpio_binary_sensor_esp8266.yaml"
) )
# GPIO5 should still use interrupts # GPIO5 should still use interrupts (default, so no setter call)
assert "bs_gpio5->set_use_interrupt(true);" in main_cpp assert "bs_gpio5->set_use_interrupt(true);" not in main_cpp
assert "bs_gpio5->set_interrupt_type(gpio::INTERRUPT_ANY_EDGE);" in main_cpp assert "bs_gpio5->set_interrupt_type(gpio::INTERRUPT_ANY_EDGE);" in main_cpp

View File

@@ -14,12 +14,14 @@ interval:
// Test parse_json // Test parse_json
bool parse_ok = esphome::json::parse_json(json_str, [](JsonObject root) { bool parse_ok = esphome::json::parse_json(json_str, [](JsonObject root) {
if (root.containsKey("sensor") && root.containsKey("value")) { if (root["sensor"].is<const char*>() && root["value"].is<float>()) {
const char* sensor = root["sensor"]; const char* sensor = root["sensor"];
float value = root["value"]; float value = root["value"];
ESP_LOGD("test", "Parsed: sensor=%s, value=%.1f", sensor, value); ESP_LOGD("test", "Parsed: sensor=%s, value=%.1f", sensor, value);
return true;
} else { } else {
ESP_LOGD("test", "Parsed JSON missing required keys"); ESP_LOGD("test", "Parsed JSON missing required keys");
return false;
} }
}); });
ESP_LOGD("test", "Parse result (JSON syntax only): %s", parse_ok ? "success" : "failed"); ESP_LOGD("test", "Parse result (JSON syntax only): %s", parse_ok ? "success" : "failed");

View File

@@ -52,6 +52,19 @@ number:
widget: spinbox_id widget: spinbox_id
id: lvgl_spinbox_number id: lvgl_spinbox_number
name: 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: light:
- platform: lvgl - platform: lvgl
@@ -110,3 +123,21 @@ text:
platform: lvgl platform: lvgl
widget: hello_label widget: hello_label
mode: text 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: "Hello shiny day"
text_color: 0xFFFFFF text_color: 0xFFFFFF
align: bottom_mid 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 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: - obj:
align: center align: center
arc_opa: COVER arc_opa: COVER

View File

@@ -68,5 +68,13 @@ lvgl:
enter_button: pushbutton enter_button: pushbutton
group: general group: general
initial_focus: lv_roller 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 <<: !include common.yaml

View File

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

View File

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

View File

@@ -9,6 +9,13 @@ esphome:
id: template_sens id: template_sens
state: !lambda "return 42.0;" 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: - datetime.date.set:
id: test_date id: test_date
date: date:
@@ -215,6 +222,7 @@ cover:
number: number:
- platform: template - platform: template
id: template_number
name: "Template number" name: "Template number"
optimistic: true optimistic: true
min_value: 0 min_value: 0

View File

@@ -32,6 +32,7 @@ switch:
name: "Test Switch" name: "Test Switch"
id: test_switch id: test_switch
optimistic: true optimistic: true
lambda: return false;
interval: interval:
- interval: 0.5s - 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["memory_impact"]["should_run"] == "false"
assert output["cpp_unit_tests_run_all"] is False assert output["cpp_unit_tests_run_all"] is False
assert output["cpp_unit_tests_components"] == ["wifi", "api", "sensor"] 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( 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["memory_impact"]["should_run"] == "false"
assert output["cpp_unit_tests_run_all"] is False assert output["cpp_unit_tests_run_all"] is False
assert output["cpp_unit_tests_components"] == [] 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( 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 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: def test_add_platform_defines_priority() -> None:
"""Test that _add_platform_defines runs after globals. """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"}}} {{{ "x", "79"}, { "y", "82"}}}
- '{{{"AA"}}}' - '{{{"AA"}}}'
- '"HELLO"' - '"HELLO"'
- '{ 79, 82 }'

View File

@@ -34,3 +34,4 @@ test_list:
{{{ "x", "${ position.x }"}, { "y", "${ position.y }"}}} {{{ "x", "${ position.x }"}, { "y", "${ position.y }"}}}
- ${ '{{{"AA"}}}' } - ${ '{{{"AA"}}}' }
- ${ '"HELLO"' } - ${ '"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, check_default=None,
purpose=Purpose.UPLOADING, purpose=Purpose.UPLOADING,
) )
assert result == ["MQTTIP", "test.local"] assert result == ["MQTTIP"]
@pytest.mark.usefixtures("mock_serial_ports") @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, check_default=None,
purpose=Purpose.LOGGING, purpose=Purpose.LOGGING,
) )
assert result == ["MQTTIP", "MQTT", "test.local"] assert result == ["MQTTIP", "MQTT"]
@pytest.mark.usefixtures("mock_no_mqtt_logging") @pytest.mark.usefixtures("mock_no_mqtt_logging")
@@ -1564,7 +1564,7 @@ def test_has_resolvable_address() -> None:
setup_core( setup_core(
config={CONF_MDNS: {CONF_DISABLED: True}}, address="esphome-device.local" 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) # Test with mDNS disabled and regular DNS hostname (resolvable)
setup_core(config={CONF_MDNS: {CONF_DISABLED: True}}, address="device.example.com") setup_core(config={CONF_MDNS: {CONF_DISABLED: True}}, address="device.example.com")