Compare commits

..

11 Commits

Author SHA1 Message Date
J. Nick Koston
9913d7d9e9 Merge branch 'determine_jobs_comp_split' into determine_jobs_comp_split_test 2025-10-29 19:01:26 -05:00
J. Nick Koston
119dc8b530 flatten 2025-10-29 19:01:12 -05:00
J. Nick Koston
3c8f4c1d5d Merge branch 'determine_jobs_comp_split' into determine_jobs_comp_split_test 2025-10-29 18:58:30 -05:00
J. Nick Koston
4892638768 flatten 2025-10-29 18:58:07 -05:00
J. Nick Koston
306448c02c Merge branch 'determine_jobs_comp_split' into determine_jobs_comp_split_test 2025-10-29 18:54:31 -05:00
J. Nick Koston
eb73a23ab1 flatten 2025-10-29 18:53:50 -05:00
J. Nick Koston
22cb0fbb70 flatten 2025-10-29 18:53:35 -05:00
J. Nick Koston
04d22becd0 Merge branch 'determine_jobs_comp_split' into determine_jobs_comp_split_test 2025-10-29 18:48:14 -05:00
J. Nick Koston
36a7951f7a fix 2025-10-29 18:48:00 -05:00
J. Nick Koston
81382db3d0 DNM: ci testing 2025-10-29 18:45:29 -05:00
J. Nick Koston
e0b02ccfc5 [ci] Consolidate component splitting into determine-jobs 2025-10-29 18:43:49 -05:00
48 changed files with 194 additions and 311 deletions

View File

@@ -460,7 +460,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
strategy:
fail-fast: false
max-parallel: 2
max-parallel: 1
matrix:
include:
- id: clang-tidy

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,5 @@
// random comment
#include "api_connection.h"
#ifdef USE_API
#ifdef USE_API_NOISE

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,16 +11,14 @@ static const char *const TAG = "template.lock";
TemplateLock::TemplateLock()
: lock_trigger_(new Trigger<>()), unlock_trigger_(new Trigger<>()), open_trigger_(new Trigger<>()) {}
void TemplateLock::setup() {
if (!this->f_.has_value())
this->disable_loop();
}
void TemplateLock::loop() {
auto val = this->f_();
if (val.has_value()) {
this->publish_state(*val);
}
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
if (!val.has_value())
return;
this->publish_state(*val);
}
void TemplateLock::control(const lock::LockCall &call) {
if (this->prev_trigger_ != nullptr) {
@@ -47,6 +45,7 @@ void TemplateLock::open_latch() {
this->open_trigger_->trigger();
}
void TemplateLock::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
void TemplateLock::set_state_lambda(optional<lock::LockState> (*f)()) { this->f_ = f; }
float TemplateLock::get_setup_priority() const { return setup_priority::HARDWARE; }
Trigger<> *TemplateLock::get_lock_trigger() const { return this->lock_trigger_; }
Trigger<> *TemplateLock::get_unlock_trigger() const { return this->unlock_trigger_; }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -435,10 +435,9 @@ void WebServer::on_sensor_update(sensor::Sensor *obj, float state) {
}
void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (sensor::Sensor *obj : App.get_sensors()) {
if (!match.id_equals_entity(obj))
if (!match.id_equals(obj->get_object_id()))
continue;
// Note: request->method() is always HTTP_GET here (canHandle ensures this)
if (match.method_empty()) {
if (request->method() == HTTP_GET && match.method_empty()) {
auto detail = get_request_detail(request);
std::string data = this->sensor_json(obj, obj->state, detail);
request->send(200, "application/json", data.c_str());
@@ -478,10 +477,9 @@ void WebServer::on_text_sensor_update(text_sensor::TextSensor *obj, const std::s
}
void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (text_sensor::TextSensor *obj : App.get_text_sensors()) {
if (!match.id_equals_entity(obj))
if (!match.id_equals(obj->get_object_id()))
continue;
// Note: request->method() is always HTTP_GET here (canHandle ensures this)
if (match.method_empty()) {
if (request->method() == HTTP_GET && match.method_empty()) {
auto detail = get_request_detail(request);
std::string data = this->text_sensor_json(obj, obj->state, detail);
request->send(200, "application/json", data.c_str());
@@ -518,7 +516,7 @@ void WebServer::on_switch_update(switch_::Switch *obj, bool state) {
}
void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (switch_::Switch *obj : App.get_switches()) {
if (!match.id_equals_entity(obj))
if (!match.id_equals(obj->get_object_id()))
continue;
if (request->method() == HTTP_GET && match.method_empty()) {
@@ -587,7 +585,7 @@ std::string WebServer::switch_json(switch_::Switch *obj, bool value, JsonDetail
#ifdef USE_BUTTON
void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (button::Button *obj : App.get_buttons()) {
if (!match.id_equals_entity(obj))
if (!match.id_equals(obj->get_object_id()))
continue;
if (request->method() == HTTP_GET && match.method_empty()) {
auto detail = get_request_detail(request);
@@ -629,10 +627,9 @@ void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj) {
}
void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (binary_sensor::BinarySensor *obj : App.get_binary_sensors()) {
if (!match.id_equals_entity(obj))
if (!match.id_equals(obj->get_object_id()))
continue;
// Note: request->method() is always HTTP_GET here (canHandle ensures this)
if (match.method_empty()) {
if (request->method() == HTTP_GET && match.method_empty()) {
auto detail = get_request_detail(request);
std::string data = this->binary_sensor_json(obj, obj->state, detail);
request->send(200, "application/json", data.c_str());
@@ -668,7 +665,7 @@ void WebServer::on_fan_update(fan::Fan *obj) {
}
void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (fan::Fan *obj : App.get_fans()) {
if (!match.id_equals_entity(obj))
if (!match.id_equals(obj->get_object_id()))
continue;
if (request->method() == HTTP_GET && match.method_empty()) {
@@ -742,7 +739,7 @@ void WebServer::on_light_update(light::LightState *obj) {
}
void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (light::LightState *obj : App.get_lights()) {
if (!match.id_equals_entity(obj))
if (!match.id_equals(obj->get_object_id()))
continue;
if (request->method() == HTTP_GET && match.method_empty()) {
@@ -815,7 +812,7 @@ void WebServer::on_cover_update(cover::Cover *obj) {
}
void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (cover::Cover *obj : App.get_covers()) {
if (!match.id_equals_entity(obj))
if (!match.id_equals(obj->get_object_id()))
continue;
if (request->method() == HTTP_GET && match.method_empty()) {
@@ -900,7 +897,7 @@ void WebServer::on_number_update(number::Number *obj, float state) {
}
void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (auto *obj : App.get_numbers()) {
if (!match.id_equals_entity(obj))
if (!match.id_equals(obj->get_object_id()))
continue;
if (request->method() == HTTP_GET && match.method_empty()) {
@@ -965,7 +962,7 @@ void WebServer::on_date_update(datetime::DateEntity *obj) {
}
void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (auto *obj : App.get_dates()) {
if (!match.id_equals_entity(obj))
if (!match.id_equals(obj->get_object_id()))
continue;
if (request->method() == HTTP_GET && match.method_empty()) {
auto detail = get_request_detail(request);
@@ -1020,7 +1017,7 @@ void WebServer::on_time_update(datetime::TimeEntity *obj) {
}
void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (auto *obj : App.get_times()) {
if (!match.id_equals_entity(obj))
if (!match.id_equals(obj->get_object_id()))
continue;
if (request->method() == HTTP_GET && match.method_empty()) {
auto detail = get_request_detail(request);
@@ -1074,7 +1071,7 @@ void WebServer::on_datetime_update(datetime::DateTimeEntity *obj) {
}
void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (auto *obj : App.get_datetimes()) {
if (!match.id_equals_entity(obj))
if (!match.id_equals(obj->get_object_id()))
continue;
if (request->method() == HTTP_GET && match.method_empty()) {
auto detail = get_request_detail(request);
@@ -1129,7 +1126,7 @@ void WebServer::on_text_update(text::Text *obj, const std::string &state) {
}
void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (auto *obj : App.get_texts()) {
if (!match.id_equals_entity(obj))
if (!match.id_equals(obj->get_object_id()))
continue;
if (request->method() == HTTP_GET && match.method_empty()) {
@@ -1183,7 +1180,7 @@ void WebServer::on_select_update(select::Select *obj, const std::string &state,
}
void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (auto *obj : App.get_selects()) {
if (!match.id_equals_entity(obj))
if (!match.id_equals(obj->get_object_id()))
continue;
if (request->method() == HTTP_GET && match.method_empty()) {
@@ -1239,7 +1236,7 @@ void WebServer::on_climate_update(climate::Climate *obj) {
}
void WebServer::handle_climate_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (auto *obj : App.get_climates()) {
if (!match.id_equals_entity(obj))
if (!match.id_equals(obj->get_object_id()))
continue;
if (request->method() == HTTP_GET && match.method_empty()) {
@@ -1380,7 +1377,7 @@ void WebServer::on_lock_update(lock::Lock *obj) {
}
void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (lock::Lock *obj : App.get_locks()) {
if (!match.id_equals_entity(obj))
if (!match.id_equals(obj->get_object_id()))
continue;
if (request->method() == HTTP_GET && match.method_empty()) {
@@ -1451,7 +1448,7 @@ void WebServer::on_valve_update(valve::Valve *obj) {
}
void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (valve::Valve *obj : App.get_valves()) {
if (!match.id_equals_entity(obj))
if (!match.id_equals(obj->get_object_id()))
continue;
if (request->method() == HTTP_GET && match.method_empty()) {
@@ -1532,7 +1529,7 @@ void WebServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlP
}
void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (alarm_control_panel::AlarmControlPanel *obj : App.get_alarm_control_panels()) {
if (!match.id_equals_entity(obj))
if (!match.id_equals(obj->get_object_id()))
continue;
if (request->method() == HTTP_GET && match.method_empty()) {
@@ -1611,11 +1608,10 @@ void WebServer::on_event(event::Event *obj, const std::string &event_type) {
void WebServer::handle_event_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (event::Event *obj : App.get_events()) {
if (!match.id_equals_entity(obj))
if (!match.id_equals(obj->get_object_id()))
continue;
// Note: request->method() is always HTTP_GET here (canHandle ensures this)
if (match.method_empty()) {
if (request->method() == HTTP_GET && match.method_empty()) {
auto detail = get_request_detail(request);
std::string data = this->event_json(obj, "", detail);
request->send(200, "application/json", data.c_str());
@@ -1677,7 +1673,7 @@ void WebServer::on_update(update::UpdateEntity *obj) {
}
void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlMatch &match) {
for (update::UpdateEntity *obj : App.get_updates()) {
if (!match.id_equals_entity(obj))
if (!match.id_equals(obj->get_object_id()))
continue;
if (request->method() == HTTP_GET && match.method_empty()) {

View File

@@ -48,15 +48,8 @@ struct UrlMatch {
return domain && domain_len == strlen(str) && memcmp(domain, str, domain_len) == 0;
}
bool id_equals_entity(EntityBase *entity) const {
// Zero-copy comparison using StringRef
StringRef static_ref = entity->get_object_id_ref_for_api_();
if (!static_ref.empty()) {
return id && id_len == static_ref.size() && memcmp(id, static_ref.c_str(), id_len) == 0;
}
// Fallback to allocation (rare)
const auto &obj_id = entity->get_object_id();
return id && id_len == obj_id.length() && memcmp(id, obj_id.c_str(), id_len) == 0;
bool id_equals(const std::string &str) const {
return id && id_len == str.length() && memcmp(id, str.c_str(), id_len) == 0;
}
bool method_equals(const char *str) const {

View File

@@ -17,10 +17,6 @@ namespace api {
class APIConnection;
} // namespace api
namespace web_server {
struct UrlMatch;
} // namespace web_server
enum EntityCategory : uint8_t {
ENTITY_CATEGORY_NONE = 0,
ENTITY_CATEGORY_CONFIG = 1,
@@ -120,7 +116,6 @@ class EntityBase {
protected:
friend class api::APIConnection;
friend struct web_server::UrlMatch;
// Get object_id as StringRef when it's static (for API usage)
// Returns empty StringRef if object_id is dynamic (needs allocation)

View File

@@ -1,51 +0,0 @@
#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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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