Compare commits

..

10 Commits

Author SHA1 Message Date
J. Nick Koston
c18a0f538f preen 2025-10-25 15:05:13 -07:00
J. Nick Koston
7e31149584 readable 2025-10-25 15:02:56 -07:00
J. Nick Koston
2c6b9d3826 no race window 2025-10-25 14:56:59 -07:00
J. Nick Koston
527039211e fix off by one 2025-10-25 14:53:48 -07:00
J. Nick Koston
1ea17607f3 fix race. 2025-10-25 14:44:36 -07:00
J. Nick Koston
6cfca87ca7 safer 2025-10-25 14:39:28 -07:00
J. Nick Koston
8bd640875f touch ups 2025-10-25 14:20:57 -07:00
J. Nick Koston
1e17ed8c1e narrow scope 2025-10-25 13:51:29 -07:00
J. Nick Koston
d3b4b11302 narrow scope 2025-10-25 13:50:16 -07:00
J. Nick Koston
c5ff19d3ab [usb_host] Fix atomic memory ordering in transfer slot allocation 2025-10-25 13:43:53 -07:00
60 changed files with 615 additions and 991 deletions

View File

@@ -16,12 +16,7 @@ from esphome.const import (
CONF_UPDATE_INTERVAL,
)
from esphome.core import ID
from esphome.cpp_generator import (
LambdaExpression,
MockObj,
MockObjClass,
TemplateArgsType,
)
from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.types import ConfigType
from esphome.util import Registry
@@ -105,36 +100,6 @@ LambdaCondition = cg.esphome_ns.class_("LambdaCondition", Condition)
ForCondition = cg.esphome_ns.class_("ForCondition", Condition, cg.Component)
def new_lambda_pvariable(
id_obj: ID,
lambda_expr: LambdaExpression,
stateless_class: MockObjClass,
template_arg: cg.TemplateArguments | None = None,
) -> MockObj:
"""Create Pvariable for lambda, using stateless class if applicable.
Combines ID selection and Pvariable creation in one call. For stateless
lambdas (empty capture), uses function pointer instead of std::function.
Args:
id_obj: The ID object (action_id, condition_id, or filter_id)
lambda_expr: The lambda expression object
stateless_class: The stateless class to use for stateless lambdas
template_arg: Optional template arguments (for actions/conditions)
Returns:
The created Pvariable
"""
# For stateless lambdas, use function pointer instead of std::function
if lambda_expr.capture == "":
id_obj = id_obj.copy()
id_obj.type = stateless_class
if template_arg is not None:
return cg.new_Pvariable(id_obj, template_arg, lambda_expr)
return cg.new_Pvariable(id_obj, lambda_expr)
def validate_automation(extra_schema=None, extra_validators=None, single=False):
if extra_schema is None:
extra_schema = {}

View File

@@ -40,13 +40,13 @@ class ESP32InternalGPIOPin : public InternalGPIOPin {
// - 3 bytes for members below
// - 1 byte padding for alignment
// - 4 bytes for vtable pointer
uint8_t pin_; // GPIO pin number (0-255, actual max ~54 on ESP32)
gpio::Flags flags_{}; // GPIO flags (1 byte)
uint8_t pin_; // GPIO pin number (0-255, actual max ~54 on ESP32)
gpio::Flags flags_; // GPIO flags (1 byte)
struct PinFlags {
uint8_t inverted : 1; // Invert pin logic (1 bit)
uint8_t drive_strength : 2; // Drive strength 0-3 (2 bits)
uint8_t reserved : 5; // Reserved for future use (5 bits)
} pin_flags_{}; // Total: 1 byte
} pin_flags_; // Total: 1 byte
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
static bool isr_service_installed;
};

View File

@@ -223,10 +223,7 @@ async def esp32_pin_to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
num = config[CONF_NUMBER]
cg.add(var.set_pin(getattr(gpio_num_t, f"GPIO_NUM_{num}")))
# Only set if true to avoid bloating setup() function
# (inverted bit in pin_flags_ bitfield is zero-initialized to false)
if config[CONF_INVERTED]:
cg.add(var.set_inverted(True))
cg.add(var.set_inverted(config[CONF_INVERTED]))
if CONF_DRIVE_STRENGTH in config:
cg.add(var.set_drive_strength(config[CONF_DRIVE_STRENGTH]))
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))

View File

@@ -29,8 +29,8 @@ class ESP8266GPIOPin : public InternalGPIOPin {
void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
uint8_t pin_;
bool inverted_{};
gpio::Flags flags_{};
bool inverted_;
gpio::Flags flags_;
};
} // namespace esp8266

View File

@@ -165,10 +165,7 @@ async def esp8266_pin_to_code(config):
num = config[CONF_NUMBER]
mode = config[CONF_MODE]
cg.add(var.set_pin(num))
# Only set if true to avoid bloating setup() function
# (inverted bit in pin_flags_ bitfield is zero-initialized to false)
if config[CONF_INVERTED]:
cg.add(var.set_inverted(True))
cg.add(var.set_inverted(config[CONF_INVERTED]))
cg.add(var.set_flags(pins.gpio_flags_expr(mode)))
if num < 16:
initial_state: PinInitialState = CORE.data[KEY_ESP8266][KEY_PIN_INITIAL_STATES][

View File

@@ -28,8 +28,8 @@ class HostGPIOPin : public InternalGPIOPin {
void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
uint8_t pin_;
bool inverted_{};
gpio::Flags flags_{};
bool inverted_;
gpio::Flags flags_;
};
} // namespace host

View File

@@ -57,9 +57,6 @@ async def host_pin_to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
num = config[CONF_NUMBER]
cg.add(var.set_pin(num))
# Only set if true to avoid bloating setup() function
# (inverted bit in pin_flags_ bitfield is zero-initialized to false)
if config[CONF_INVERTED]:
cg.add(var.set_inverted(True))
cg.add(var.set_inverted(config[CONF_INVERTED]))
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
return var

View File

@@ -199,9 +199,6 @@ async def component_pin_to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
num = config[CONF_NUMBER]
cg.add(var.set_pin(num))
# Only set if true to avoid bloating setup() function
# (inverted bit in pin_flags_ bitfield is zero-initialized to false)
if config[CONF_INVERTED]:
cg.add(var.set_inverted(True))
cg.add(var.set_inverted(config[CONF_INVERTED]))
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
return var

View File

@@ -27,8 +27,8 @@ class ArduinoInternalGPIOPin : public InternalGPIOPin {
void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
uint8_t pin_;
bool inverted_{};
gpio::Flags flags_{};
bool inverted_;
gpio::Flags flags_;
};
} // namespace libretiny

View File

@@ -74,9 +74,6 @@ async def nrf52_pin_to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
num = config[CONF_NUMBER]
cg.add(var.set_pin(num))
# Only set if true to avoid bloating setup() function
# (inverted bit in pin_flags_ bitfield is zero-initialized to false)
if config[CONF_INVERTED]:
cg.add(var.set_inverted(True))
cg.add(var.set_inverted(config[CONF_INVERTED]))
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
return var

View File

@@ -29,8 +29,8 @@ class RP2040GPIOPin : public InternalGPIOPin {
void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
uint8_t pin_;
bool inverted_{};
gpio::Flags flags_{};
bool inverted_;
gpio::Flags flags_;
};
} // namespace rp2040

View File

@@ -94,9 +94,6 @@ async def rp2040_pin_to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
num = config[CONF_NUMBER]
cg.add(var.set_pin(num))
# Only set if true to avoid bloating setup() function
# (inverted bit in pin_flags_ bitfield is zero-initialized to false)
if config[CONF_INVERTED]:
cg.add(var.set_inverted(True))
cg.add(var.set_inverted(config[CONF_INVERTED]))
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
return var

View File

@@ -10,9 +10,6 @@ from .. import template_ns
TemplateBinarySensor = template_ns.class_(
"TemplateBinarySensor", binary_sensor.BinarySensor, cg.Component
)
StatelessTemplateBinarySensor = template_ns.class_(
"StatelessTemplateBinarySensor", binary_sensor.BinarySensor, cg.Component
)
CONFIG_SCHEMA = (
binary_sensor.binary_sensor_schema(TemplateBinarySensor)
@@ -29,22 +26,15 @@ CONFIG_SCHEMA = (
async def to_code(config):
# Check if we have a lambda first - determines which class to instantiate
var = await binary_sensor.new_binary_sensor(config)
await cg.register_component(var, config)
if lamb := config.get(CONF_LAMBDA):
# Use new_lambda_pvariable to create either TemplateBinarySensor or StatelessTemplateBinarySensor
template_ = await cg.process_lambda(
lamb, [], return_type=cg.optional.template(bool)
)
var = automation.new_lambda_pvariable(
config[CONF_ID], template_, StatelessTemplateBinarySensor
)
# Manually register as binary sensor since we didn't use new_binary_sensor
await binary_sensor.register_binary_sensor(var, config)
await cg.register_component(var, config)
elif condition := config.get(CONF_CONDITION):
# For conditions, create stateful version and set template
var = await binary_sensor.new_binary_sensor(config)
await cg.register_component(var, config)
cg.add(var.set_template(template_))
if condition := config.get(CONF_CONDITION):
condition = await automation.build_condition(
condition, cg.TemplateArguments(), []
)
@@ -52,10 +42,6 @@ async def to_code(config):
f"return {condition.check()};", [], return_type=cg.optional.template(bool)
)
cg.add(var.set_template(template_))
else:
# No lambda or condition - just create the base template sensor
var = await binary_sensor.new_binary_sensor(config)
await cg.register_component(var, config)
@automation.register_action(

View File

@@ -6,13 +6,18 @@ namespace template_ {
static const char *const TAG = "template.binary_sensor";
// Template instantiations
template<typename F> void TemplateBinarySensorBase<F>::dump_config() {
LOG_BINARY_SENSOR("", "Template Binary Sensor", this);
}
void TemplateBinarySensor::setup() { this->loop(); }
template class TemplateBinarySensorBase<std::function<optional<bool>()>>;
template class TemplateBinarySensorBase<optional<bool> (*)()>;
void TemplateBinarySensor::loop() {
if (this->f_ == nullptr)
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_
} // namespace esphome

View File

@@ -6,41 +6,18 @@
namespace esphome {
namespace template_ {
template<typename F> class TemplateBinarySensorBase : public Component, public binary_sensor::BinarySensor {
class TemplateBinarySensor : public Component, public binary_sensor::BinarySensor {
public:
void setup() override { this->loop(); }
void loop() override {
if (this->f_ == nullptr)
return;
auto s = this->f_();
if (s.has_value()) {
this->publish_state(*s);
}
}
void set_template(std::function<optional<bool>()> &&f) { this->f_ = f; }
void setup() override;
void loop() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::HARDWARE; }
protected:
F f_;
};
class TemplateBinarySensor : public TemplateBinarySensorBase<std::function<optional<bool>()>> {
public:
TemplateBinarySensor() { this->f_ = nullptr; }
void set_template(std::function<optional<bool>()> &&f) { this->f_ = f; }
};
/** Optimized template binary sensor for stateless lambdas (no capture).
*
* Uses function pointer instead of std::function to reduce memory overhead.
* Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function).
*/
class StatelessTemplateBinarySensor : public TemplateBinarySensorBase<optional<bool> (*)()> {
public:
explicit StatelessTemplateBinarySensor(optional<bool> (*f)()) { this->f_ = f; }
std::function<optional<bool>()> f_{nullptr};
};
} // namespace template_

View File

@@ -23,9 +23,6 @@ from esphome.const import (
from .. import template_ns
TemplateCover = template_ns.class_("TemplateCover", cover.Cover, cg.Component)
StatelessTemplateCover = template_ns.class_(
"StatelessTemplateCover", cover.Cover, cg.Component
)
TemplateCoverRestoreMode = template_ns.enum("TemplateCoverRestoreMode")
RESTORE_MODES = {
@@ -66,22 +63,13 @@ CONFIG_SCHEMA = (
async def to_code(config):
var = await cover.new_cover(config)
await cg.register_component(var, config)
if CONF_LAMBDA in config:
# Use new_lambda_pvariable to create either TemplateCover or StatelessTemplateCover
template_ = await cg.process_lambda(
config[CONF_LAMBDA], [], return_type=cg.optional.template(float)
)
var = automation.new_lambda_pvariable(
config[CONF_ID], template_, StatelessTemplateCover
)
# Manually register as cover since we didn't use new_cover
await cover.register_cover(var, config)
await cg.register_component(var, config)
else:
# No state lambda - just create the base template cover
var = await cover.new_cover(config)
await cg.register_component(var, config)
cg.add(var.set_state_lambda(template_))
if CONF_OPEN_ACTION in config:
await automation.build_automation(
var.get_open_trigger(), [], config[CONF_OPEN_ACTION]

View File

@@ -8,8 +8,14 @@ using namespace esphome::cover;
static const char *const TAG = "template.cover";
// Template instantiations
template<typename StateF, typename TiltF> void TemplateCoverBase<StateF, TiltF>::setup() {
TemplateCover::TemplateCover()
: open_trigger_(new Trigger<>()),
close_trigger_(new Trigger<>),
stop_trigger_(new Trigger<>()),
toggle_trigger_(new Trigger<>()),
position_trigger_(new Trigger<float>()),
tilt_trigger_(new Trigger<float>()) {}
void TemplateCover::setup() {
switch (this->restore_mode_) {
case COVER_NO_RESTORE:
break;
@@ -28,12 +34,43 @@ template<typename StateF, typename TiltF> void TemplateCoverBase<StateF, TiltF>:
}
}
}
void TemplateCover::loop() {
bool changed = false;
template<typename StateF, typename TiltF> void TemplateCoverBase<StateF, TiltF>::dump_config() {
LOG_COVER("", "Template Cover", this);
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;
}
}
}
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;
}
}
}
if (changed)
this->publish_state();
}
template<typename StateF, typename TiltF> void TemplateCoverBase<StateF, TiltF>::control(const CoverCall &call) {
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(std::function<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_; }
Trigger<> *TemplateCover::get_stop_trigger() const { return this->stop_trigger_; }
Trigger<> *TemplateCover::get_toggle_trigger() const { return this->toggle_trigger_; }
void TemplateCover::dump_config() { LOG_COVER("", "Template Cover", this); }
void TemplateCover::control(const CoverCall &call) {
if (call.get_stop()) {
this->stop_prev_trigger_();
this->stop_trigger_->trigger();
@@ -76,8 +113,7 @@ template<typename StateF, typename TiltF> void TemplateCoverBase<StateF, TiltF>:
this->publish_state();
}
template<typename StateF, typename TiltF> CoverTraits TemplateCoverBase<StateF, TiltF>::get_traits() {
CoverTraits TemplateCover::get_traits() {
auto traits = CoverTraits();
traits.set_is_assumed_state(this->assumed_state_);
traits.set_supports_stop(this->has_stop_);
@@ -86,16 +122,19 @@ template<typename StateF, typename TiltF> CoverTraits TemplateCoverBase<StateF,
traits.set_supports_tilt(this->has_tilt_);
return traits;
}
template<typename StateF, typename TiltF> void TemplateCoverBase<StateF, TiltF>::stop_prev_trigger_() {
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(std::function<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; }
void TemplateCover::set_has_tilt(bool has_tilt) { this->has_tilt_ = has_tilt; }
void TemplateCover::stop_prev_trigger_() {
if (this->prev_command_trigger_ != nullptr) {
this->prev_command_trigger_->stop_action();
this->prev_command_trigger_ = nullptr;
}
}
template class TemplateCoverBase<std::function<optional<float>()>, std::function<optional<float>()>>;
template class TemplateCoverBase<optional<float> (*)(), optional<float> (*)()>;
} // namespace template_
} // namespace esphome

View File

@@ -13,59 +13,31 @@ enum TemplateCoverRestoreMode {
COVER_RESTORE_AND_CALL,
};
template<typename StateF, typename TiltF> class TemplateCoverBase : public cover::Cover, public Component {
class TemplateCover : public cover::Cover, public Component {
public:
TemplateCoverBase()
: open_trigger_(new Trigger<>()),
close_trigger_(new Trigger<>()),
stop_trigger_(new Trigger<>()),
toggle_trigger_(new Trigger<>()),
position_trigger_(new Trigger<float>()),
tilt_trigger_(new Trigger<float>()) {}
TemplateCover();
void loop() override {
bool changed = false;
if (this->state_f_.has_value()) {
auto s = (*this->state_f_)();
if (s.has_value()) {
auto pos = clamp(*s, 0.0f, 1.0f);
if (pos != this->position) {
this->position = pos;
changed = true;
}
}
}
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;
}
}
}
if (changed)
this->publish_state();
}
void set_state_lambda(std::function<optional<float>()> &&f);
Trigger<> *get_open_trigger() const;
Trigger<> *get_close_trigger() const;
Trigger<> *get_stop_trigger() const;
Trigger<> *get_toggle_trigger() const;
Trigger<float> *get_position_trigger() const;
Trigger<float> *get_tilt_trigger() const;
void set_optimistic(bool optimistic);
void set_assumed_state(bool assumed_state);
void set_tilt_lambda(std::function<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);
void set_has_toggle(bool has_toggle);
void set_restore_mode(TemplateCoverRestoreMode restore_mode) { restore_mode_ = restore_mode; }
void setup() override;
void loop() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::HARDWARE; }
Trigger<> *get_open_trigger() const { return this->open_trigger_; }
Trigger<> *get_close_trigger() const { return this->close_trigger_; }
Trigger<> *get_stop_trigger() const { return this->stop_trigger_; }
Trigger<> *get_toggle_trigger() const { return this->toggle_trigger_; }
Trigger<float> *get_position_trigger() const { return this->position_trigger_; }
Trigger<float> *get_tilt_trigger() const { return this->tilt_trigger_; }
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
void set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; }
void set_has_stop(bool has_stop) { this->has_stop_ = has_stop; }
void set_has_position(bool has_position) { this->has_position_ = has_position; }
void set_has_tilt(bool has_tilt) { this->has_tilt_ = has_tilt; }
void set_has_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; }
void set_restore_mode(TemplateCoverRestoreMode restore_mode) { restore_mode_ = restore_mode; }
float get_setup_priority() const override;
protected:
void control(const cover::CoverCall &call) override;
@@ -73,8 +45,8 @@ template<typename StateF, typename TiltF> class TemplateCoverBase : public cover
void stop_prev_trigger_();
TemplateCoverRestoreMode restore_mode_{COVER_RESTORE};
optional<StateF> state_f_;
optional<TiltF> tilt_f_;
optional<std::function<optional<float>()>> state_f_;
optional<std::function<optional<float>()>> tilt_f_;
bool assumed_state_{false};
bool optimistic_{false};
Trigger<> *open_trigger_;
@@ -90,22 +62,5 @@ template<typename StateF, typename TiltF> class TemplateCoverBase : public cover
bool has_tilt_{false};
};
class TemplateCover : public TemplateCoverBase<std::function<optional<float>()>, std::function<optional<float>()>> {
public:
void set_state_lambda(std::function<optional<float>()> &&f) { this->state_f_ = f; }
void set_tilt_lambda(std::function<optional<float>()> &&tilt_f) { this->tilt_f_ = tilt_f; }
};
/** Optimized template cover for stateless lambdas (no capture).
*
* Uses function pointers instead of std::function to reduce memory overhead.
* Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function) per lambda.
*/
class StatelessTemplateCover : public TemplateCoverBase<optional<float> (*)(), optional<float> (*)()> {
public:
explicit StatelessTemplateCover(optional<float> (*state_f)()) { this->state_f_ = state_f; }
void set_tilt_lambda(optional<float> (*tilt_f)()) { this->tilt_f_ = tilt_f; }
};
} // namespace template_
} // namespace esphome

View File

@@ -5,7 +5,6 @@ import esphome.config_validation as cv
from esphome.const import (
CONF_DAY,
CONF_HOUR,
CONF_ID,
CONF_INITIAL_VALUE,
CONF_LAMBDA,
CONF_MINUTE,
@@ -26,23 +25,14 @@ CODEOWNERS = ["@rfdarter"]
TemplateDate = template_ns.class_(
"TemplateDate", datetime.DateEntity, cg.PollingComponent
)
StatelessTemplateDate = template_ns.class_(
"StatelessTemplateDate", datetime.DateEntity, cg.PollingComponent
)
TemplateTime = template_ns.class_(
"TemplateTime", datetime.TimeEntity, cg.PollingComponent
)
StatelessTemplateTime = template_ns.class_(
"StatelessTemplateTime", datetime.TimeEntity, cg.PollingComponent
)
TemplateDateTime = template_ns.class_(
"TemplateDateTime", datetime.DateTimeEntity, cg.PollingComponent
)
StatelessTemplateDateTime = template_ns.class_(
"StatelessTemplateDateTime", datetime.DateTimeEntity, cg.PollingComponent
)
def validate(config):
@@ -109,30 +99,15 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
var = await datetime.new_datetime(config)
if CONF_LAMBDA in config:
# Use new_lambda_pvariable to create either Template* or StatelessTemplate*
template_ = await cg.process_lambda(
config[CONF_LAMBDA], [], return_type=cg.optional.template(cg.ESPTime)
)
# Determine the appropriate stateless class based on type
if config[CONF_TYPE] == "DATE":
stateless_class = StatelessTemplateDate
elif config[CONF_TYPE] == "TIME":
stateless_class = StatelessTemplateTime
else: # DATETIME
stateless_class = StatelessTemplateDateTime
cg.add(var.set_template(template_))
var = automation.new_lambda_pvariable(
config[CONF_ID], template_, stateless_class
)
# Manually register as datetime since we didn't use new_datetime
await datetime.register_datetime(var, config)
await cg.register_component(var, config)
else:
# No lambda - just create the base template datetime
var = await datetime.new_datetime(config)
await cg.register_component(var, config)
cg.add(var.set_optimistic(config[CONF_OPTIMISTIC]))
cg.add(var.set_restore_value(config[CONF_RESTORE_VALUE]))
@@ -171,3 +146,5 @@ async def to_code(config):
[(cg.ESPTime, "x")],
config[CONF_SET_ACTION],
)
await cg.register_component(var, config)

View File

@@ -9,8 +9,7 @@ namespace template_ {
static const char *const TAG = "template.date";
// Template instantiations
template<typename F> void TemplateDateBase<F>::setup() {
void TemplateDate::setup() {
if (this->f_.has_value())
return;
@@ -37,7 +36,21 @@ template<typename F> void TemplateDateBase<F>::setup() {
this->publish_state();
}
template<typename F> void TemplateDateBase<F>::control(const datetime::DateCall &call) {
void TemplateDate::update() {
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
if (!val.has_value())
return;
this->year_ = val->year;
this->month_ = val->month;
this->day_ = val->day_of_month;
this->publish_state();
}
void TemplateDate::control(const datetime::DateCall &call) {
bool has_year = call.get_year().has_value();
bool has_month = call.get_month().has_value();
bool has_day = call.get_day().has_value();
@@ -86,15 +99,12 @@ template<typename F> void TemplateDateBase<F>::control(const datetime::DateCall
}
}
template<typename F> void TemplateDateBase<F>::dump_config() {
void TemplateDate::dump_config() {
LOG_DATETIME_DATE("", "Template Date", this);
ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_));
LOG_UPDATE_INTERVAL(this);
}
template class TemplateDateBase<std::function<optional<ESPTime>()>>;
template class TemplateDateBase<optional<ESPTime> (*)()>;
} // namespace template_
} // namespace esphome

View File

@@ -13,23 +13,12 @@
namespace esphome {
namespace template_ {
template<typename F> class TemplateDateBase : public datetime::DateEntity, public PollingComponent {
class TemplateDate : public datetime::DateEntity, public PollingComponent {
public:
void update() override {
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
if (!val.has_value())
return;
this->year_ = val->year;
this->month_ = val->month;
this->day_ = val->day_of_month;
this->publish_state();
}
void set_template(std::function<optional<ESPTime>()> &&f) { this->f_ = f; }
void setup() override;
void update() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::HARDWARE; }
@@ -46,26 +35,11 @@ template<typename F> class TemplateDateBase : public datetime::DateEntity, publi
ESPTime initial_value_{};
bool restore_value_{false};
Trigger<ESPTime> *set_trigger_ = new Trigger<ESPTime>();
optional<F> f_;
optional<std::function<optional<ESPTime>()>> f_;
ESPPreferenceObject pref_;
};
class TemplateDate : public TemplateDateBase<std::function<optional<ESPTime>()>> {
public:
void set_template(std::function<optional<ESPTime>()> &&f) { this->f_ = f; }
};
/** Optimized template date for stateless lambdas (no capture).
*
* Uses function pointers instead of std::function to reduce memory overhead.
* Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function) per lambda.
*/
class StatelessTemplateDate : public TemplateDateBase<optional<ESPTime> (*)()> {
public:
explicit StatelessTemplateDate(optional<ESPTime> (*f)()) { this->f_ = f; }
};
} // namespace template_
} // namespace esphome

View File

@@ -9,8 +9,7 @@ namespace template_ {
static const char *const TAG = "template.datetime";
// Template instantiations
template<typename F> void TemplateDateTimeBase<F>::setup() {
void TemplateDateTime::setup() {
if (this->f_.has_value())
return;
@@ -40,7 +39,24 @@ template<typename F> void TemplateDateTimeBase<F>::setup() {
this->publish_state();
}
template<typename F> void TemplateDateTimeBase<F>::control(const datetime::DateTimeCall &call) {
void TemplateDateTime::update() {
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
if (!val.has_value())
return;
this->year_ = val->year;
this->month_ = val->month;
this->day_ = val->day_of_month;
this->hour_ = val->hour;
this->minute_ = val->minute;
this->second_ = val->second;
this->publish_state();
}
void TemplateDateTime::control(const datetime::DateTimeCall &call) {
bool has_year = call.get_year().has_value();
bool has_month = call.get_month().has_value();
bool has_day = call.get_day().has_value();
@@ -122,15 +138,12 @@ template<typename F> void TemplateDateTimeBase<F>::control(const datetime::DateT
}
}
template<typename F> void TemplateDateTimeBase<F>::dump_config() {
void TemplateDateTime::dump_config() {
LOG_DATETIME_DATETIME("", "Template DateTime", this);
ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_));
LOG_UPDATE_INTERVAL(this);
}
template class TemplateDateTimeBase<std::function<optional<ESPTime>()>>;
template class TemplateDateTimeBase<optional<ESPTime> (*)()>;
} // namespace template_
} // namespace esphome

View File

@@ -13,26 +13,12 @@
namespace esphome {
namespace template_ {
template<typename F> class TemplateDateTimeBase : public datetime::DateTimeEntity, public PollingComponent {
class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponent {
public:
void update() override {
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
if (!val.has_value())
return;
this->year_ = val->year;
this->month_ = val->month;
this->day_ = val->day_of_month;
this->hour_ = val->hour;
this->minute_ = val->minute;
this->second_ = val->second;
this->publish_state();
}
void set_template(std::function<optional<ESPTime>()> &&f) { this->f_ = f; }
void setup() override;
void update() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::HARDWARE; }
@@ -49,26 +35,11 @@ template<typename F> class TemplateDateTimeBase : public datetime::DateTimeEntit
ESPTime initial_value_{};
bool restore_value_{false};
Trigger<ESPTime> *set_trigger_ = new Trigger<ESPTime>();
optional<F> f_;
optional<std::function<optional<ESPTime>()>> f_;
ESPPreferenceObject pref_;
};
class TemplateDateTime : public TemplateDateTimeBase<std::function<optional<ESPTime>()>> {
public:
void set_template(std::function<optional<ESPTime>()> &&f) { this->f_ = f; }
};
/** Optimized template datetime for stateless lambdas (no capture).
*
* Uses function pointers instead of std::function to reduce memory overhead.
* Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function) per lambda.
*/
class StatelessTemplateDateTime : public TemplateDateTimeBase<optional<ESPTime> (*)()> {
public:
explicit StatelessTemplateDateTime(optional<ESPTime> (*f)()) { this->f_ = f; }
};
} // namespace template_
} // namespace esphome

View File

@@ -9,8 +9,7 @@ namespace template_ {
static const char *const TAG = "template.time";
// Template instantiations
template<typename F> void TemplateTimeBase<F>::setup() {
void TemplateTime::setup() {
if (this->f_.has_value())
return;
@@ -37,7 +36,21 @@ template<typename F> void TemplateTimeBase<F>::setup() {
this->publish_state();
}
template<typename F> void TemplateTimeBase<F>::control(const datetime::TimeCall &call) {
void TemplateTime::update() {
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
if (!val.has_value())
return;
this->hour_ = val->hour;
this->minute_ = val->minute;
this->second_ = val->second;
this->publish_state();
}
void TemplateTime::control(const datetime::TimeCall &call) {
bool has_hour = call.get_hour().has_value();
bool has_minute = call.get_minute().has_value();
bool has_second = call.get_second().has_value();
@@ -86,15 +99,12 @@ template<typename F> void TemplateTimeBase<F>::control(const datetime::TimeCall
}
}
template<typename F> void TemplateTimeBase<F>::dump_config() {
void TemplateTime::dump_config() {
LOG_DATETIME_TIME("", "Template Time", this);
ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_));
LOG_UPDATE_INTERVAL(this);
}
template class TemplateTimeBase<std::function<optional<ESPTime>()>>;
template class TemplateTimeBase<optional<ESPTime> (*)()>;
} // namespace template_
} // namespace esphome

View File

@@ -13,23 +13,12 @@
namespace esphome {
namespace template_ {
template<typename F> class TemplateTimeBase : public datetime::TimeEntity, public PollingComponent {
class TemplateTime : public datetime::TimeEntity, public PollingComponent {
public:
void update() override {
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
if (!val.has_value())
return;
this->hour_ = val->hour;
this->minute_ = val->minute;
this->second_ = val->second;
this->publish_state();
}
void set_template(std::function<optional<ESPTime>()> &&f) { this->f_ = f; }
void setup() override;
void update() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::HARDWARE; }
@@ -46,26 +35,11 @@ template<typename F> class TemplateTimeBase : public datetime::TimeEntity, publi
ESPTime initial_value_{};
bool restore_value_{false};
Trigger<ESPTime> *set_trigger_ = new Trigger<ESPTime>();
optional<F> f_;
optional<std::function<optional<ESPTime>()>> f_;
ESPPreferenceObject pref_;
};
class TemplateTime : public TemplateTimeBase<std::function<optional<ESPTime>()>> {
public:
void set_template(std::function<optional<ESPTime>()> &&f) { this->f_ = f; }
};
/** Optimized template time for stateless lambdas (no capture).
*
* Uses function pointers instead of std::function to reduce memory overhead.
* Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function) per lambda.
*/
class StatelessTemplateTime : public TemplateTimeBase<optional<ESPTime> (*)()> {
public:
explicit StatelessTemplateTime(optional<ESPTime> (*f)()) { this->f_ = f; }
};
} // namespace template_
} // namespace esphome

View File

@@ -1,6 +1,6 @@
#pragma once
#include <vector>
#include <set>
#include "esphome/core/component.h"
#include "esphome/components/fan/fan.h"
@@ -16,7 +16,7 @@ class TemplateFan : public Component, public fan::Fan {
void set_has_direction(bool has_direction) { this->has_direction_ = has_direction; }
void set_has_oscillating(bool has_oscillating) { this->has_oscillating_ = has_oscillating; }
void set_speed_count(int count) { this->speed_count_ = count; }
void set_preset_modes(const std::initializer_list<std::string> &presets) { this->preset_modes_ = presets; }
void set_preset_modes(const std::set<std::string> &presets) { this->preset_modes_ = presets; }
fan::FanTraits get_traits() override { return this->traits_; }
protected:
@@ -26,7 +26,7 @@ class TemplateFan : public Component, public fan::Fan {
bool has_direction_{false};
int speed_count_{0};
fan::FanTraits traits_;
std::vector<std::string> preset_modes_{};
std::set<std::string> preset_modes_{};
};
} // namespace template_

View File

@@ -16,9 +16,6 @@ from esphome.const import (
from .. import template_ns
TemplateLock = template_ns.class_("TemplateLock", lock.Lock, cg.Component)
StatelessTemplateLock = template_ns.class_(
"StatelessTemplateLock", lock.Lock, cg.Component
)
TemplateLockPublishAction = template_ns.class_(
"TemplateLockPublishAction",
@@ -58,22 +55,14 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
var = await lock.new_lock(config)
await cg.register_component(var, config)
if CONF_LAMBDA in config:
# Use new_lambda_pvariable to create either TemplateLock or StatelessTemplateLock
template_ = await cg.process_lambda(
config[CONF_LAMBDA], [], return_type=cg.optional.template(lock.LockState)
)
var = automation.new_lambda_pvariable(
config[CONF_ID], template_, StatelessTemplateLock
)
# Manually register as lock since we didn't use new_lock
await lock.register_lock(var, config)
await cg.register_component(var, config)
else:
# No lambda - just create the base template lock
var = await lock.new_lock(config)
await cg.register_component(var, config)
cg.add(var.set_state_lambda(template_))
if CONF_UNLOCK_ACTION in config:
await automation.build_automation(
var.get_unlock_trigger(), [], config[CONF_UNLOCK_ACTION]

View File

@@ -8,8 +8,19 @@ using namespace esphome::lock;
static const char *const TAG = "template.lock";
// Template instantiations
template<typename F> void TemplateLockBase<F>::control(const lock::LockCall &call) {
TemplateLock::TemplateLock()
: lock_trigger_(new Trigger<>()), unlock_trigger_(new Trigger<>()), open_trigger_(new Trigger<>()) {}
void TemplateLock::loop() {
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
if (!val.has_value())
return;
this->publish_state(*val);
}
void TemplateLock::control(const lock::LockCall &call) {
if (this->prev_trigger_ != nullptr) {
this->prev_trigger_->stop_action();
}
@@ -26,22 +37,23 @@ template<typename F> void TemplateLockBase<F>::control(const lock::LockCall &cal
if (this->optimistic_)
this->publish_state(state);
}
template<typename F> void TemplateLockBase<F>::open_latch() {
void TemplateLock::open_latch() {
if (this->prev_trigger_ != nullptr) {
this->prev_trigger_->stop_action();
}
this->prev_trigger_ = this->open_trigger_;
this->open_trigger_->trigger();
}
template<typename F> void TemplateLockBase<F>::dump_config() {
void TemplateLock::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
void TemplateLock::set_state_lambda(std::function<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_; }
Trigger<> *TemplateLock::get_open_trigger() const { return this->open_trigger_; }
void TemplateLock::dump_config() {
LOG_LOCK("", "Template Lock", this);
ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_));
}
template class TemplateLockBase<std::function<optional<lock::LockState>()>>;
template class TemplateLockBase<optional<lock::LockState> (*)()>;
} // namespace template_
} // namespace esphome

View File

@@ -7,35 +7,26 @@
namespace esphome {
namespace template_ {
template<typename F> class TemplateLockBase : public lock::Lock, public Component {
class TemplateLock : public lock::Lock, public Component {
public:
TemplateLockBase()
: lock_trigger_(new Trigger<>()), unlock_trigger_(new Trigger<>()), open_trigger_(new Trigger<>()) {}
void loop() override {
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
if (!val.has_value())
return;
this->publish_state(*val);
}
TemplateLock();
void dump_config() override;
Trigger<> *get_lock_trigger() const { return this->lock_trigger_; }
Trigger<> *get_unlock_trigger() const { return this->unlock_trigger_; }
Trigger<> *get_open_trigger() const { return this->open_trigger_; }
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
void set_state_lambda(std::function<optional<lock::LockState>()> &&f);
Trigger<> *get_lock_trigger() const;
Trigger<> *get_unlock_trigger() const;
Trigger<> *get_open_trigger() const;
void set_optimistic(bool optimistic);
void loop() override;
float get_setup_priority() const override { return setup_priority::HARDWARE; }
float get_setup_priority() const override;
protected:
void control(const lock::LockCall &call) override;
void open_latch() override;
optional<F> f_;
optional<std::function<optional<lock::LockState>()>> f_;
bool optimistic_{false};
Trigger<> *lock_trigger_;
Trigger<> *unlock_trigger_;
@@ -43,20 +34,5 @@ template<typename F> class TemplateLockBase : public lock::Lock, public Componen
Trigger<> *prev_trigger_{nullptr};
};
class TemplateLock : public TemplateLockBase<std::function<optional<lock::LockState>()>> {
public:
void set_state_lambda(std::function<optional<lock::LockState>()> &&f) { this->f_ = f; }
};
/** Optimized template lock for stateless lambdas (no capture).
*
* Uses function pointers instead of std::function to reduce memory overhead.
* Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function) per lambda.
*/
class StatelessTemplateLock : public TemplateLockBase<optional<lock::LockState> (*)()> {
public:
explicit StatelessTemplateLock(optional<lock::LockState> (*f)()) { this->f_ = f; }
};
} // namespace template_
} // namespace esphome

View File

@@ -19,9 +19,6 @@ from .. import template_ns
TemplateNumber = template_ns.class_(
"TemplateNumber", number.Number, cg.PollingComponent
)
StatelessTemplateNumber = template_ns.class_(
"StatelessTemplateNumber", number.Number, cg.PollingComponent
)
def validate_min_max(config):
@@ -69,33 +66,23 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await number.register_number(
var,
config,
min_value=config[CONF_MIN_VALUE],
max_value=config[CONF_MAX_VALUE],
step=config[CONF_STEP],
)
if CONF_LAMBDA in config:
# Use new_lambda_pvariable to create either TemplateNumber or StatelessTemplateNumber
template_ = await cg.process_lambda(
config[CONF_LAMBDA], [], return_type=cg.optional.template(float)
)
var = automation.new_lambda_pvariable(
config[CONF_ID], template_, StatelessTemplateNumber
)
await cg.register_component(var, config)
await number.register_number(
var,
config,
min_value=config[CONF_MIN_VALUE],
max_value=config[CONF_MAX_VALUE],
step=config[CONF_STEP],
)
cg.add(var.set_template(template_))
else:
# No lambda - just create the base template number
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await number.register_number(
var,
config,
min_value=config[CONF_MIN_VALUE],
max_value=config[CONF_MAX_VALUE],
step=config[CONF_STEP],
)
cg.add(var.set_optimistic(config[CONF_OPTIMISTIC]))
cg.add(var.set_initial_value(config[CONF_INITIAL_VALUE]))
if CONF_RESTORE_VALUE in config:

View File

@@ -6,8 +6,7 @@ namespace template_ {
static const char *const TAG = "template.number";
// Template instantiations
template<typename F> void TemplateNumberBase<F>::setup() {
void TemplateNumber::setup() {
if (this->f_.has_value())
return;
@@ -27,7 +26,18 @@ template<typename F> void TemplateNumberBase<F>::setup() {
this->publish_state(value);
}
template<typename F> void TemplateNumberBase<F>::control(float value) {
void TemplateNumber::update() {
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
if (!val.has_value())
return;
this->publish_state(*val);
}
void TemplateNumber::control(float value) {
this->set_trigger_->trigger(value);
if (this->optimistic_)
@@ -36,15 +46,11 @@ template<typename F> void TemplateNumberBase<F>::control(float value) {
if (this->restore_value_)
this->pref_.save(&value);
}
template<typename F> void TemplateNumberBase<F>::dump_config() {
void TemplateNumber::dump_config() {
LOG_NUMBER("", "Template Number", this);
ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_));
LOG_UPDATE_INTERVAL(this);
}
template class TemplateNumberBase<std::function<optional<float>()>>;
template class TemplateNumberBase<optional<float> (*)()>;
} // namespace template_
} // namespace esphome

View File

@@ -8,22 +8,13 @@
namespace esphome {
namespace template_ {
template<typename F> class TemplateNumberBase : public number::Number, public PollingComponent {
class TemplateNumber : public number::Number, public PollingComponent {
public:
TemplateNumberBase() : set_trigger_(new Trigger<float>()) {}
void set_template(std::function<optional<float>()> &&f) { this->f_ = f; }
void setup() override;
void update() override;
void dump_config() override;
void update() override {
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
if (!val.has_value())
return;
this->publish_state(*val);
}
float get_setup_priority() const override { return setup_priority::HARDWARE; }
Trigger<float> *get_set_trigger() const { return set_trigger_; }
@@ -36,26 +27,11 @@ template<typename F> class TemplateNumberBase : public number::Number, public Po
bool optimistic_{false};
float initial_value_{NAN};
bool restore_value_{false};
Trigger<float> *set_trigger_;
optional<F> f_;
Trigger<float> *set_trigger_ = new Trigger<float>();
optional<std::function<optional<float>()>> f_;
ESPPreferenceObject pref_;
};
class TemplateNumber : public TemplateNumberBase<std::function<optional<float>()>> {
public:
void set_template(std::function<optional<float>()> &&f) { this->f_ = f; }
};
/** Optimized template number for stateless lambdas (no capture).
*
* Uses function pointer instead of std::function to reduce memory overhead.
* Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function).
*/
class StatelessTemplateNumber : public TemplateNumberBase<optional<float> (*)()> {
public:
explicit StatelessTemplateNumber(optional<float> (*f)()) { this->f_ = f; }
};
} // namespace template_
} // namespace esphome

View File

@@ -17,9 +17,6 @@ from .. import template_ns
TemplateSelect = template_ns.class_(
"TemplateSelect", select.Select, cg.PollingComponent
)
StatelessTemplateSelect = template_ns.class_(
"StatelessTemplateSelect", select.Select, cg.PollingComponent
)
def validate(config):
@@ -65,34 +62,22 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await select.register_select(var, config, options=config[CONF_OPTIONS])
if CONF_LAMBDA in config:
# Use new_lambda_pvariable to create either TemplateSelect or StatelessTemplateSelect
template_ = await cg.process_lambda(
config[CONF_LAMBDA], [], return_type=cg.optional.template(cg.std_string)
)
var = automation.new_lambda_pvariable(
config[CONF_ID], template_, StatelessTemplateSelect
)
await cg.register_component(var, config)
await select.register_select(var, config, options=config[CONF_OPTIONS])
cg.add(var.set_template(template_))
else:
# No lambda - just create the base template select
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await select.register_select(var, config, options=config[CONF_OPTIONS])
cg.add(var.set_optimistic(config[CONF_OPTIMISTIC]))
cg.add(var.set_initial_option(config[CONF_INITIAL_OPTION]))
# Only set if non-default to avoid bloating setup() function
if config[CONF_OPTIMISTIC]:
cg.add(var.set_optimistic(True))
initial_option_index = config[CONF_OPTIONS].index(config[CONF_INITIAL_OPTION])
# Only set if non-zero to avoid bloating setup() function
# (initial_option_index_ is zero-initialized in the header)
if initial_option_index != 0:
cg.add(var.set_initial_option_index(initial_option_index))
# Only set if True (default is False)
if config.get(CONF_RESTORE_VALUE):
cg.add(var.set_restore_value(True))
if CONF_RESTORE_VALUE in config:
cg.add(var.set_restore_value(config[CONF_RESTORE_VALUE]))
if CONF_SET_ACTION in config:
await automation.build_automation(

View File

@@ -6,29 +6,49 @@ namespace template_ {
static const char *const TAG = "template.select";
// Template instantiations
template<typename F> void TemplateSelectBase<F>::setup() {
void TemplateSelect::setup() {
if (this->f_.has_value())
return;
size_t index = this->initial_option_index_;
if (this->restore_value_) {
this->pref_ = global_preferences->make_preference<size_t>(this->get_preference_hash());
size_t restored_index;
if (this->pref_.load(&restored_index) && this->has_index(restored_index)) {
index = restored_index;
ESP_LOGD(TAG, "State from restore: %s", this->at(index).value().c_str());
} else {
ESP_LOGD(TAG, "State from initial (could not load or invalid stored index): %s", this->at(index).value().c_str());
}
std::string value;
if (!this->restore_value_) {
value = this->initial_option_;
ESP_LOGD(TAG, "State from initial: %s", value.c_str());
} else {
ESP_LOGD(TAG, "State from initial: %s", this->at(index).value().c_str());
size_t index;
this->pref_ = global_preferences->make_preference<size_t>(this->get_preference_hash());
if (!this->pref_.load(&index)) {
value = this->initial_option_;
ESP_LOGD(TAG, "State from initial (could not load stored index): %s", value.c_str());
} else if (!this->has_index(index)) {
value = this->initial_option_;
ESP_LOGD(TAG, "State from initial (restored index %d out of bounds): %s", index, value.c_str());
} else {
value = this->at(index).value();
ESP_LOGD(TAG, "State from restore: %s", value.c_str());
}
}
this->publish_state(this->at(index).value());
this->publish_state(value);
}
template<typename F> void TemplateSelectBase<F>::control(const std::string &value) {
void TemplateSelect::update() {
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
if (!val.has_value())
return;
if (!this->has_option(*val)) {
ESP_LOGE(TAG, "Lambda returned an invalid option: %s", (*val).c_str());
return;
}
this->publish_state(*val);
}
void TemplateSelect::control(const std::string &value) {
this->set_trigger_->trigger(value);
if (this->optimistic_)
@@ -40,7 +60,7 @@ template<typename F> void TemplateSelectBase<F>::control(const std::string &valu
}
}
template<typename F> void TemplateSelectBase<F>::dump_config() {
void TemplateSelect::dump_config() {
LOG_SELECT("", "Template Select", this);
LOG_UPDATE_INTERVAL(this);
if (this->f_.has_value())
@@ -49,12 +69,8 @@ template<typename F> void TemplateSelectBase<F>::dump_config() {
" Optimistic: %s\n"
" Initial Option: %s\n"
" Restore Value: %s",
YESNO(this->optimistic_), this->at(this->initial_option_index_).value().c_str(),
YESNO(this->restore_value_));
YESNO(this->optimistic_), this->initial_option_.c_str(), YESNO(this->restore_value_));
}
template class TemplateSelectBase<std::function<optional<std::string>()>>;
template class TemplateSelectBase<optional<std::string> (*)()>;
} // namespace template_
} // namespace esphome

View File

@@ -8,58 +8,30 @@
namespace esphome {
namespace template_ {
template<typename F> class TemplateSelectBase : public select::Select, public PollingComponent {
class TemplateSelect : public select::Select, public PollingComponent {
public:
TemplateSelectBase() : set_trigger_(new Trigger<std::string>()) {}
void set_template(std::function<optional<std::string>()> &&f) { this->f_ = f; }
void setup() override;
void update() override;
void dump_config() override;
void update() override {
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
if (!val.has_value())
return;
if (!this->has_option(*val)) {
ESP_LOGE("template.select", "Lambda returned an invalid option: %s", (*val).c_str());
return;
}
this->publish_state(*val);
}
float get_setup_priority() const override { return setup_priority::HARDWARE; }
Trigger<std::string> *get_set_trigger() const { return this->set_trigger_; }
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
void set_initial_option_index(size_t initial_option_index) { this->initial_option_index_ = initial_option_index; }
void set_initial_option(const std::string &initial_option) { this->initial_option_ = initial_option; }
void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; }
protected:
void control(const std::string &value) override;
bool optimistic_ = false;
size_t initial_option_index_{0};
std::string initial_option_;
bool restore_value_ = false;
Trigger<std::string> *set_trigger_;
optional<F> f_;
Trigger<std::string> *set_trigger_ = new Trigger<std::string>();
optional<std::function<optional<std::string>()>> f_;
ESPPreferenceObject pref_;
};
class TemplateSelect : public TemplateSelectBase<std::function<optional<std::string>()>> {
public:
void set_template(std::function<optional<std::string>()> &&f) { this->f_ = f; }
};
/** Optimized template select for stateless lambdas (no capture).
*
* Uses function pointer instead of std::function to reduce memory overhead.
* Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function).
*/
class StatelessTemplateSelect : public TemplateSelectBase<optional<std::string> (*)()> {
public:
explicit StatelessTemplateSelect(optional<std::string> (*f)()) { this->f_ = f; }
};
} // namespace template_
} // namespace esphome

View File

@@ -9,9 +9,6 @@ from .. import template_ns
TemplateSensor = template_ns.class_(
"TemplateSensor", sensor.Sensor, cg.PollingComponent
)
StatelessTemplateSensor = template_ns.class_(
"StatelessTemplateSensor", sensor.Sensor, cg.PollingComponent
)
CONFIG_SCHEMA = (
sensor.sensor_schema(
@@ -28,21 +25,14 @@ CONFIG_SCHEMA = (
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
if CONF_LAMBDA in config:
# Use new_lambda_pvariable to create either TemplateSensor or StatelessTemplateSensor
template_ = await cg.process_lambda(
config[CONF_LAMBDA], [], return_type=cg.optional.template(float)
)
var = automation.new_lambda_pvariable(
config[CONF_ID], template_, StatelessTemplateSensor
)
# Manually register as sensor since we didn't use new_sensor
await sensor.register_sensor(var, config)
await cg.register_component(var, config)
else:
# No lambda - just create the base template sensor
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
cg.add(var.set_template(template_))
@automation.register_action(

View File

@@ -7,14 +7,21 @@ namespace template_ {
static const char *const TAG = "template.sensor";
// Template instantiations
template<typename F> void TemplateSensorBase<F>::dump_config() {
void TemplateSensor::update() {
if (!this->f_.has_value())
return;
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(std::function<optional<float>()> &&f) { this->f_ = f; }
void TemplateSensor::dump_config() {
LOG_SENSOR("", "Template Sensor", this);
LOG_UPDATE_INTERVAL(this);
}
template class TemplateSensorBase<std::function<optional<float>()>>;
template class TemplateSensorBase<optional<float> (*)()>;
} // namespace template_
} // namespace esphome

View File

@@ -6,38 +6,18 @@
namespace esphome {
namespace template_ {
template<typename F> class TemplateSensorBase : public sensor::Sensor, public PollingComponent {
class TemplateSensor : public sensor::Sensor, public PollingComponent {
public:
void update() override {
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
if (val.has_value()) {
this->publish_state(*val);
}
}
void set_template(std::function<optional<float>()> &&f);
void update() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::HARDWARE; }
float get_setup_priority() const override;
protected:
optional<F> f_;
};
class TemplateSensor : public TemplateSensorBase<std::function<optional<float>()>> {
public:
void set_template(std::function<optional<float>()> &&f) { this->f_ = f; }
};
/** Optimized template sensor for stateless lambdas (no capture).
*
* Uses function pointer instead of std::function to reduce memory overhead.
* Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function).
*/
class StatelessTemplateSensor : public TemplateSensorBase<optional<float> (*)()> {
public:
explicit StatelessTemplateSensor(optional<float> (*f)()) { this->f_ = f; }
optional<std::function<optional<float>()>> f_;
};
} // namespace template_

View File

@@ -16,9 +16,6 @@ from esphome.const import (
from .. import template_ns
TemplateSwitch = template_ns.class_("TemplateSwitch", switch.Switch, cg.Component)
StatelessTemplateSwitch = template_ns.class_(
"StatelessTemplateSwitch", switch.Switch, cg.Component
)
def validate(config):
@@ -58,22 +55,14 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
var = await switch.new_switch(config)
await cg.register_component(var, config)
if CONF_LAMBDA in config:
# Use new_lambda_pvariable to create either TemplateSwitch or StatelessTemplateSwitch
template_ = await cg.process_lambda(
config[CONF_LAMBDA], [], return_type=cg.optional.template(bool)
)
var = automation.new_lambda_pvariable(
config[CONF_ID], template_, StatelessTemplateSwitch
)
# Manually register as switch since we didn't use new_switch
await switch.register_switch(var, config)
await cg.register_component(var, config)
else:
# No lambda - just create the base template switch
var = await switch.new_switch(config)
await cg.register_component(var, config)
cg.add(var.set_state_lambda(template_))
if CONF_TURN_OFF_ACTION in config:
await automation.build_automation(
var.get_turn_off_trigger(), [], config[CONF_TURN_OFF_ACTION]

View File

@@ -6,8 +6,18 @@ namespace template_ {
static const char *const TAG = "template.switch";
// Template instantiations
template<typename F> void TemplateSwitchBase<F>::write_state(bool state) {
TemplateSwitch::TemplateSwitch() : turn_on_trigger_(new Trigger<>()), turn_off_trigger_(new Trigger<>()) {}
void TemplateSwitch::loop() {
if (!this->f_.has_value())
return;
auto s = (*this->f_)();
if (!s.has_value())
return;
this->publish_state(*s);
}
void TemplateSwitch::write_state(bool state) {
if (this->prev_trigger_ != nullptr) {
this->prev_trigger_->stop_action();
}
@@ -23,8 +33,13 @@ template<typename F> void TemplateSwitchBase<F>::write_state(bool state) {
if (this->optimistic_)
this->publish_state(state);
}
template<typename F> void TemplateSwitchBase<F>::setup() {
void TemplateSwitch::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
bool TemplateSwitch::assumed_state() { return this->assumed_state_; }
void TemplateSwitch::set_state_lambda(std::function<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() {
optional<bool> initial_state = this->get_initial_state_with_restore_mode();
if (initial_state.has_value()) {
@@ -37,14 +52,11 @@ template<typename F> void TemplateSwitchBase<F>::setup() {
}
}
}
template<typename F> void TemplateSwitchBase<F>::dump_config() {
void TemplateSwitch::dump_config() {
LOG_SWITCH("", "Template Switch", this);
ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_));
}
template class TemplateSwitchBase<std::function<optional<bool>()>>;
template class TemplateSwitchBase<optional<bool> (*)()>;
void TemplateSwitch::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; }
} // namespace template_
} // namespace esphome

View File

@@ -7,35 +7,28 @@
namespace esphome {
namespace template_ {
template<typename F> class TemplateSwitchBase : public switch_::Switch, public Component {
class TemplateSwitch : public switch_::Switch, public Component {
public:
TemplateSwitchBase() : turn_on_trigger_(new Trigger<>()), turn_off_trigger_(new Trigger<>()) {}
TemplateSwitch();
void setup() override;
void dump_config() override;
void loop() override {
if (!this->f_.has_value())
return;
auto s = (*this->f_)();
if (!s.has_value())
return;
this->publish_state(*s);
}
void set_state_lambda(std::function<optional<bool>()> &&f);
Trigger<> *get_turn_on_trigger() const;
Trigger<> *get_turn_off_trigger() const;
void set_optimistic(bool optimistic);
void set_assumed_state(bool assumed_state);
void loop() override;
Trigger<> *get_turn_on_trigger() const { return this->turn_on_trigger_; }
Trigger<> *get_turn_off_trigger() const { return this->turn_off_trigger_; }
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
void set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; }
float get_setup_priority() const override { return setup_priority::HARDWARE - 2.0f; }
float get_setup_priority() const override;
protected:
bool assumed_state() override { return this->assumed_state_; }
bool assumed_state() override;
void write_state(bool state) override;
optional<F> f_;
optional<std::function<optional<bool>()>> f_;
bool optimistic_{false};
bool assumed_state_{false};
Trigger<> *turn_on_trigger_;
@@ -43,20 +36,5 @@ template<typename F> class TemplateSwitchBase : public switch_::Switch, public C
Trigger<> *prev_trigger_{nullptr};
};
class TemplateSwitch : public TemplateSwitchBase<std::function<optional<bool>()>> {
public:
void set_state_lambda(std::function<optional<bool>()> &&f) { this->f_ = f; }
};
/** Optimized template switch for stateless lambdas (no capture).
*
* Uses function pointer instead of std::function to reduce memory overhead.
* Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function).
*/
class StatelessTemplateSwitch : public TemplateSwitchBase<optional<bool> (*)()> {
public:
explicit StatelessTemplateSwitch(optional<bool> (*f)()) { this->f_ = f; }
};
} // namespace template_
} // namespace esphome

View File

@@ -3,7 +3,6 @@ import esphome.codegen as cg
from esphome.components import text
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_INITIAL_VALUE,
CONF_LAMBDA,
CONF_MAX_LENGTH,
@@ -17,9 +16,6 @@ from esphome.const import (
from .. import template_ns
TemplateText = template_ns.class_("TemplateText", text.Text, cg.PollingComponent)
StatelessTemplateText = template_ns.class_(
"StatelessTemplateText", text.Text, cg.PollingComponent
)
TextSaverBase = template_ns.class_("TemplateTextSaverBase")
TextSaverTemplate = template_ns.class_("TextSaver", TextSaverBase)
@@ -69,31 +65,21 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
var = await text.new_text(
config,
min_length=config[CONF_MIN_LENGTH],
max_length=config[CONF_MAX_LENGTH],
pattern=config.get(CONF_PATTERN),
)
await cg.register_component(var, config)
if CONF_LAMBDA in config:
# Use new_lambda_pvariable to create either TemplateText or StatelessTemplateText
template_ = await cg.process_lambda(
config[CONF_LAMBDA], [], return_type=cg.optional.template(cg.std_string)
)
var = automation.new_lambda_pvariable(
config[CONF_ID], template_, StatelessTemplateText
)
await cg.register_component(var, config)
await text.register_text(
var,
config,
min_length=config[CONF_MIN_LENGTH],
max_length=config[CONF_MAX_LENGTH],
pattern=config.get(CONF_PATTERN),
)
cg.add(var.set_template(template_))
else:
# No lambda - just create the base template text
var = await text.new_text(
config,
min_length=config[CONF_MIN_LENGTH],
max_length=config[CONF_MAX_LENGTH],
pattern=config.get(CONF_PATTERN),
)
await cg.register_component(var, config)
cg.add(var.set_optimistic(config[CONF_OPTIMISTIC]))
if initial_value_config := config.get(CONF_INITIAL_VALUE):
cg.add(var.set_initial_value(initial_value_config))

View File

@@ -6,8 +6,7 @@ namespace template_ {
static const char *const TAG = "template.text";
// Template instantiations
template<typename F> void TemplateTextBase<F>::setup() {
void TemplateText::setup() {
if (!(this->f_ == nullptr)) {
if (this->f_.has_value())
return;
@@ -26,7 +25,21 @@ template<typename F> void TemplateTextBase<F>::setup() {
this->publish_state(value);
}
template<typename F> void TemplateTextBase<F>::control(const std::string &value) {
void TemplateText::update() {
if (this->f_ == nullptr)
return;
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
if (!val.has_value())
return;
this->publish_state(*val);
}
void TemplateText::control(const std::string &value) {
this->set_trigger_->trigger(value);
if (this->optimistic_)
@@ -38,15 +51,11 @@ template<typename F> void TemplateTextBase<F>::control(const std::string &value)
}
}
}
template<typename F> void TemplateTextBase<F>::dump_config() {
void TemplateText::dump_config() {
LOG_TEXT("", "Template Text Input", this);
ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_));
LOG_UPDATE_INTERVAL(this);
}
template class TemplateTextBase<std::function<optional<std::string>()>>;
template class TemplateTextBase<optional<std::string> (*)()>;
} // namespace template_
} // namespace esphome

View File

@@ -59,24 +59,13 @@ template<uint8_t SZ> class TextSaver : public TemplateTextSaverBase {
}
};
template<typename F> class TemplateTextBase : public text::Text, public PollingComponent {
class TemplateText : public text::Text, public PollingComponent {
public:
TemplateTextBase() : set_trigger_(new Trigger<std::string>()) {}
void set_template(std::function<optional<std::string>()> &&f) { this->f_ = f; }
void setup() override;
void update() override;
void dump_config() override;
void update() override {
if (this->f_ == nullptr)
return;
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
if (!val.has_value())
return;
this->publish_state(*val);
}
float get_setup_priority() const override { return setup_priority::HARDWARE; }
Trigger<std::string> *get_set_trigger() const { return this->set_trigger_; }
@@ -88,26 +77,11 @@ template<typename F> class TemplateTextBase : public text::Text, public PollingC
void control(const std::string &value) override;
bool optimistic_ = false;
std::string initial_value_;
Trigger<std::string> *set_trigger_;
optional<F> f_{nullptr};
Trigger<std::string> *set_trigger_ = new Trigger<std::string>();
optional<std::function<optional<std::string>()>> f_{nullptr};
TemplateTextSaverBase *pref_ = nullptr;
};
class TemplateText : public TemplateTextBase<std::function<optional<std::string>()>> {
public:
void set_template(std::function<optional<std::string>()> &&f) { this->f_ = f; }
};
/** Optimized template text for stateless lambdas (no capture).
*
* Uses function pointer instead of std::function to reduce memory overhead.
* Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function).
*/
class StatelessTemplateText : public TemplateTextBase<optional<std::string> (*)()> {
public:
explicit StatelessTemplateText(optional<std::string> (*f)()) { this->f_ = f; }
};
} // namespace template_
} // namespace esphome

View File

@@ -10,9 +10,6 @@ from .. import template_ns
TemplateTextSensor = template_ns.class_(
"TemplateTextSensor", text_sensor.TextSensor, cg.PollingComponent
)
StatelessTemplateTextSensor = template_ns.class_(
"StatelessTemplateTextSensor", text_sensor.TextSensor, cg.PollingComponent
)
CONFIG_SCHEMA = (
text_sensor.text_sensor_schema()
@@ -27,21 +24,14 @@ CONFIG_SCHEMA = (
async def to_code(config):
var = await text_sensor.new_text_sensor(config)
await cg.register_component(var, config)
if CONF_LAMBDA in config:
# Use new_lambda_pvariable to create either TemplateTextSensor or StatelessTemplateTextSensor
template_ = await cg.process_lambda(
config[CONF_LAMBDA], [], return_type=cg.optional.template(cg.std_string)
)
var = automation.new_lambda_pvariable(
config[CONF_ID], template_, StatelessTemplateTextSensor
)
# Manually register as text sensor since we didn't use new_text_sensor
await text_sensor.register_text_sensor(var, config)
await cg.register_component(var, config)
else:
# No lambda - just create the base template text sensor
var = await text_sensor.new_text_sensor(config)
await cg.register_component(var, config)
cg.add(var.set_template(template_))
@automation.register_action(

View File

@@ -6,11 +6,18 @@ namespace template_ {
static const char *const TAG = "template.text_sensor";
// Template instantiations
template<typename F> void TemplateTextSensorBase<F>::dump_config() { LOG_TEXT_SENSOR("", "Template Sensor", this); }
void TemplateTextSensor::update() {
if (!this->f_.has_value())
return;
template class TemplateTextSensorBase<std::function<optional<std::string>()>>;
template class TemplateTextSensorBase<optional<std::string> (*)()>;
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(std::function<optional<std::string>()> &&f) { this->f_ = f; }
void TemplateTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Template Sensor", this); }
} // namespace template_
} // namespace esphome

View File

@@ -7,38 +7,18 @@
namespace esphome {
namespace template_ {
template<typename F> class TemplateTextSensorBase : public text_sensor::TextSensor, public PollingComponent {
class TemplateTextSensor : public text_sensor::TextSensor, public PollingComponent {
public:
void update() override {
if (!this->f_.has_value())
return;
auto val = (*this->f_)();
if (val.has_value()) {
this->publish_state(*val);
}
}
void set_template(std::function<optional<std::string>()> &&f);
float get_setup_priority() const override { return setup_priority::HARDWARE; }
void update() override;
float get_setup_priority() const override;
void dump_config() override;
protected:
optional<F> f_;
};
class TemplateTextSensor : public TemplateTextSensorBase<std::function<optional<std::string>()>> {
public:
void set_template(std::function<optional<std::string>()> &&f) { this->f_ = f; }
};
/** Optimized template text sensor for stateless lambdas (no capture).
*
* Uses function pointer instead of std::function to reduce memory overhead.
* Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function).
*/
class StatelessTemplateTextSensor : public TemplateTextSensorBase<optional<std::string> (*)()> {
public:
explicit StatelessTemplateTextSensor(optional<std::string> (*f)()) { this->f_ = f; }
optional<std::function<optional<std::string>()>> f_{};
};
} // namespace template_

View File

@@ -20,9 +20,6 @@ from esphome.const import (
from .. import template_ns
TemplateValve = template_ns.class_("TemplateValve", valve.Valve, cg.Component)
StatelessTemplateValve = template_ns.class_(
"StatelessTemplateValve", valve.Valve, cg.Component
)
TemplateValvePublishAction = template_ns.class_(
"TemplateValvePublishAction", automation.Action, cg.Parented.template(TemplateValve)
@@ -65,22 +62,13 @@ CONFIG_SCHEMA = (
async def to_code(config):
var = await valve.new_valve(config)
await cg.register_component(var, config)
if lambda_config := config.get(CONF_LAMBDA):
# Use new_lambda_pvariable to create either TemplateValve or StatelessTemplateValve
template_ = await cg.process_lambda(
lambda_config, [], return_type=cg.optional.template(float)
)
var = automation.new_lambda_pvariable(
config[CONF_ID], template_, StatelessTemplateValve
)
# Manually register as valve since we didn't use new_valve
await valve.register_valve(var, config)
await cg.register_component(var, config)
else:
# No lambda - just create the base template valve
var = await valve.new_valve(config)
await cg.register_component(var, config)
cg.add(var.set_state_lambda(template_))
if open_action_config := config.get(CONF_OPEN_ACTION):
await automation.build_automation(
var.get_open_trigger(), [], open_action_config

View File

@@ -8,8 +8,14 @@ using namespace esphome::valve;
static const char *const TAG = "template.valve";
// Template instantiations
template<typename F> void TemplateValveBase<F>::setup() {
TemplateValve::TemplateValve()
: open_trigger_(new Trigger<>()),
close_trigger_(new Trigger<>),
stop_trigger_(new Trigger<>()),
toggle_trigger_(new Trigger<>()),
position_trigger_(new Trigger<float>()) {}
void TemplateValve::setup() {
switch (this->restore_mode_) {
case VALVE_NO_RESTORE:
break;
@@ -29,7 +35,35 @@ template<typename F> void TemplateValveBase<F>::setup() {
}
}
template<typename F> void TemplateValveBase<F>::dump_config() {
void TemplateValve::loop() {
bool changed = false;
if (this->state_f_.has_value()) {
auto s = (*this->state_f_)();
if (s.has_value()) {
auto pos = clamp(*s, 0.0f, 1.0f);
if (pos != this->position) {
this->position = pos;
changed = true;
}
}
}
if (changed)
this->publish_state();
}
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(std::function<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_; }
Trigger<> *TemplateValve::get_close_trigger() const { return this->close_trigger_; }
Trigger<> *TemplateValve::get_stop_trigger() const { return this->stop_trigger_; }
Trigger<> *TemplateValve::get_toggle_trigger() const { return this->toggle_trigger_; }
void TemplateValve::dump_config() {
LOG_VALVE("", "Template Valve", this);
ESP_LOGCONFIG(TAG,
" Has position: %s\n"
@@ -37,7 +71,7 @@ template<typename F> void TemplateValveBase<F>::dump_config() {
YESNO(this->has_position_), YESNO(this->optimistic_));
}
template<typename F> void TemplateValveBase<F>::control(const ValveCall &call) {
void TemplateValve::control(const ValveCall &call) {
if (call.get_stop()) {
this->stop_prev_trigger_();
this->stop_trigger_->trigger();
@@ -72,7 +106,7 @@ template<typename F> void TemplateValveBase<F>::control(const ValveCall &call) {
this->publish_state();
}
template<typename F> valve::ValveTraits TemplateValveBase<F>::get_traits() {
ValveTraits TemplateValve::get_traits() {
auto traits = ValveTraits();
traits.set_is_assumed_state(this->assumed_state_);
traits.set_supports_stop(this->has_stop_);
@@ -81,15 +115,18 @@ template<typename F> valve::ValveTraits TemplateValveBase<F>::get_traits() {
return traits;
}
template<typename F> void TemplateValveBase<F>::stop_prev_trigger_() {
Trigger<float> *TemplateValve::get_position_trigger() const { return this->position_trigger_; }
void TemplateValve::set_has_stop(bool has_stop) { this->has_stop_ = has_stop; }
void TemplateValve::set_has_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; }
void TemplateValve::set_has_position(bool has_position) { this->has_position_ = has_position; }
void TemplateValve::stop_prev_trigger_() {
if (this->prev_command_trigger_ != nullptr) {
this->prev_command_trigger_->stop_action();
this->prev_command_trigger_ = nullptr;
}
}
template class TemplateValveBase<std::function<optional<float>()>>;
template class TemplateValveBase<optional<float> (*)()>;
} // namespace template_
} // namespace esphome

View File

@@ -13,48 +13,28 @@ enum TemplateValveRestoreMode {
VALVE_RESTORE_AND_CALL,
};
template<typename F> class TemplateValveBase : public valve::Valve, public Component {
class TemplateValve : public valve::Valve, public Component {
public:
TemplateValveBase()
: open_trigger_(new Trigger<>()),
close_trigger_(new Trigger<>()),
stop_trigger_(new Trigger<>()),
toggle_trigger_(new Trigger<>()),
position_trigger_(new Trigger<float>()) {}
TemplateValve();
void loop() override {
bool changed = false;
if (this->state_f_.has_value()) {
auto s = (*this->state_f_)();
if (s.has_value()) {
auto pos = clamp(*s, 0.0f, 1.0f);
if (pos != this->position) {
this->position = pos;
changed = true;
}
}
}
if (changed)
this->publish_state();
}
void set_state_lambda(std::function<optional<float>()> &&f);
Trigger<> *get_open_trigger() const;
Trigger<> *get_close_trigger() const;
Trigger<> *get_stop_trigger() const;
Trigger<> *get_toggle_trigger() const;
Trigger<float> *get_position_trigger() const;
void set_optimistic(bool optimistic);
void set_assumed_state(bool assumed_state);
void set_has_stop(bool has_stop);
void set_has_position(bool has_position);
void set_has_toggle(bool has_toggle);
void set_restore_mode(TemplateValveRestoreMode restore_mode) { restore_mode_ = restore_mode; }
void setup() override;
void loop() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::HARDWARE; }
Trigger<> *get_open_trigger() const { return this->open_trigger_; }
Trigger<> *get_close_trigger() const { return this->close_trigger_; }
Trigger<> *get_stop_trigger() const { return this->stop_trigger_; }
Trigger<> *get_toggle_trigger() const { return this->toggle_trigger_; }
Trigger<float> *get_position_trigger() const { return this->position_trigger_; }
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
void set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; }
void set_has_stop(bool has_stop) { this->has_stop_ = has_stop; }
void set_has_position(bool has_position) { this->has_position_ = has_position; }
void set_has_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; }
void set_restore_mode(TemplateValveRestoreMode restore_mode) { restore_mode_ = restore_mode; }
float get_setup_priority() const override;
protected:
void control(const valve::ValveCall &call) override;
@@ -62,7 +42,7 @@ template<typename F> class TemplateValveBase : public valve::Valve, public Compo
void stop_prev_trigger_();
TemplateValveRestoreMode restore_mode_{VALVE_NO_RESTORE};
optional<F> state_f_;
optional<std::function<optional<float>()>> state_f_;
bool assumed_state_{false};
bool optimistic_{false};
Trigger<> *open_trigger_;
@@ -76,20 +56,5 @@ template<typename F> class TemplateValveBase : public valve::Valve, public Compo
bool has_position_{false};
};
class TemplateValve : public TemplateValveBase<std::function<optional<float>()>> {
public:
void set_state_lambda(std::function<optional<float>()> &&f) { this->state_f_ = f; }
};
/** Optimized template valve for stateless lambdas (no capture).
*
* Uses function pointers instead of std::function to reduce memory overhead.
* Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function) per lambda.
*/
class StatelessTemplateValve : public TemplateValveBase<optional<float> (*)()> {
public:
explicit StatelessTemplateValve(optional<float> (*f)()) { this->state_f_ = f; }
};
} // namespace template_
} // namespace esphome

View File

@@ -82,6 +82,12 @@ struct TransferStatus {
using transfer_cb_t = std::function<void(const TransferStatus &)>;
enum TransferResult : uint8_t {
TRANSFER_OK = 0,
TRANSFER_ERROR_NO_SLOTS,
TRANSFER_ERROR_SUBMIT_FAILED,
};
class USBClient;
// struct used to capture all data needed for a transfer
@@ -134,7 +140,7 @@ class USBClient : public Component {
void on_opened(uint8_t addr);
void on_removed(usb_device_handle_t handle);
void control_transfer_callback(const usb_transfer_t *xfer) const;
void transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length);
TransferResult transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length);
void transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length);
void dump_config() override;
void release_trq(TransferRequest *trq);

View File

@@ -334,7 +334,7 @@ static void control_callback(const usb_transfer_t *xfer) {
// This multi-threaded access is intentional for performance - USB task can
// immediately restart transfers without waiting for main loop scheduling.
TransferRequest *USBClient::get_trq_() {
trq_bitmask_t mask = this->trq_in_use_.load(std::memory_order_relaxed);
trq_bitmask_t mask = this->trq_in_use_.load(std::memory_order_acquire);
// Find first available slot (bit = 0) and try to claim it atomically
// We use a while loop to allow retrying the same slot after CAS failure
@@ -443,14 +443,15 @@ static void transfer_callback(usb_transfer_t *xfer) {
* @param ep_address The endpoint address.
* @param callback The callback function to be called when the transfer is complete.
* @param length The length of the data to be transferred.
* @return TransferResult indicating success or specific failure reason
*
* @throws None.
*/
void USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length) {
TransferResult USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length) {
auto *trq = this->get_trq_();
if (trq == nullptr) {
ESP_LOGE(TAG, "Too many requests queued");
return;
return TRANSFER_ERROR_NO_SLOTS;
}
trq->callback = callback;
trq->transfer->callback = transfer_callback;
@@ -460,7 +461,9 @@ void USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, u
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to submit transfer, address=%x, length=%d, err=%x", ep_address, length, err);
this->release_trq(trq);
return TRANSFER_ERROR_SUBMIT_FAILED;
}
return TRANSFER_OK;
}
/**

View File

@@ -169,6 +169,98 @@ bool USBUartChannel::read_array(uint8_t *data, size_t len) {
this->parent_->start_input(this);
return status;
}
void USBUartComponent::reset_input_state_(USBUartChannel *channel) {
channel->input_retry_count_.store(0);
channel->input_started_.store(false);
}
void USBUartComponent::restart_input_(USBUartChannel *channel) {
// Atomically verify it's still started (true) and keep it started
// This prevents the race window of toggling true->false->true
bool expected = true;
if (channel->input_started_.compare_exchange_strong(expected, true)) {
// Still started - do the actual restart work without toggling the flag
this->do_start_input_(channel);
}
}
void USBUartComponent::input_transfer_callback_(USBUartChannel *channel, const usb_host::TransferStatus &status) {
// CALLBACK CONTEXT: This function is executed in USB task via transfer_callback
ESP_LOGV(TAG, "Transfer result: length: %u; status %X", status.data_len, status.error_code);
if (!status.success) {
ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code));
// Transfer failed, slot already released
// Reset state so normal operations can restart later
this->reset_input_state_(channel);
return;
}
if (!channel->dummy_receiver_ && status.data_len > 0) {
// Allocate a chunk from the pool
UsbDataChunk *chunk = this->chunk_pool_.allocate();
if (chunk == nullptr) {
// No chunks available - queue is full, data dropped, slot already released
this->usb_data_queue_.increment_dropped_count();
// Reset state so normal operations can restart later
this->reset_input_state_(channel);
return;
}
// Copy data to chunk (this is fast, happens in USB task)
memcpy(chunk->data, status.data, status.data_len);
chunk->length = status.data_len;
chunk->channel = channel;
// Push to lock-free queue for main loop processing
// Push always succeeds because pool size == queue size
this->usb_data_queue_.push(chunk);
}
// On success, reset retry count and restart input immediately from USB task for performance
// The lock-free queue will handle backpressure
channel->input_retry_count_.store(0);
channel->input_started_.store(false);
this->start_input(channel);
}
void USBUartComponent::do_start_input_(USBUartChannel *channel) {
// This function does the actual work of starting input
// Caller must ensure input_started_ is already set to true
const auto *ep = channel->cdc_dev_.in_ep;
// input_started_ already set to true by caller
auto result = this->transfer_in(
ep->bEndpointAddress,
[this, channel](const usb_host::TransferStatus &status) { this->input_transfer_callback_(channel, status); },
ep->wMaxPacketSize);
if (result == usb_host::TRANSFER_ERROR_NO_SLOTS) {
// No slots available - defer retry to main loop
this->defer_input_retry_(channel);
} else if (result != usb_host::TRANSFER_OK) {
// Other error (submit failed) - don't retry, just reset state
// Error already logged by transfer_in()
this->reset_input_state_(channel);
}
}
void USBUartComponent::defer_input_retry_(USBUartChannel *channel) {
static constexpr uint8_t MAX_INPUT_RETRIES = 10;
// Atomically increment and get the NEW value (previous + 1)
uint8_t new_retry_count = channel->input_retry_count_.fetch_add(1) + 1;
if (new_retry_count > MAX_INPUT_RETRIES) {
ESP_LOGE(TAG, "Input retry limit reached for channel %d, stopping retries", channel->index_);
this->reset_input_state_(channel);
return;
}
// Keep input_started_ as true during defer to prevent multiple retries from queueing
// The deferred lambda will atomically restart
this->defer([this, channel] { this->restart_input_(channel); });
}
void USBUartComponent::setup() { USBClient::setup(); }
void USBUartComponent::loop() {
USBClient::loop();
@@ -214,8 +306,14 @@ void USBUartComponent::dump_config() {
}
}
void USBUartComponent::start_input(USBUartChannel *channel) {
if (!channel->initialised_.load() || channel->input_started_.load())
if (!channel->initialised_.load())
return;
// Atomically check if not started and set to started in one operation
bool expected = false;
if (!channel->input_started_.compare_exchange_strong(expected, true))
return; // Already started - prevents duplicate transfers from concurrent threads
// THREAD CONTEXT: Called from both USB task and main loop threads
// - USB task: Immediate restart after successful transfer for continuous data flow
// - Main loop: Controlled restart after consuming data (backpressure mechanism)
@@ -226,45 +324,9 @@ void USBUartComponent::start_input(USBUartChannel *channel) {
//
// The underlying transfer_in() uses lock-free atomic allocation from the
// TransferRequest pool, making this multi-threaded access safe
const auto *ep = channel->cdc_dev_.in_ep;
// CALLBACK CONTEXT: This lambda is executed in USB task via transfer_callback
auto callback = [this, channel](const usb_host::TransferStatus &status) {
ESP_LOGV(TAG, "Transfer result: length: %u; status %X", status.data_len, status.error_code);
if (!status.success) {
ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code));
// On failure, don't restart - let next read_array() trigger it
channel->input_started_.store(false);
return;
}
if (!channel->dummy_receiver_ && status.data_len > 0) {
// Allocate a chunk from the pool
UsbDataChunk *chunk = this->chunk_pool_.allocate();
if (chunk == nullptr) {
// No chunks available - queue is full or we're out of memory
this->usb_data_queue_.increment_dropped_count();
// Mark input as not started so we can retry
channel->input_started_.store(false);
return;
}
// Copy data to chunk (this is fast, happens in USB task)
memcpy(chunk->data, status.data, status.data_len);
chunk->length = status.data_len;
chunk->channel = channel;
// Push to lock-free queue for main loop processing
// Push always succeeds because pool size == queue size
this->usb_data_queue_.push(chunk);
}
// On success, restart input immediately from USB task for performance
// The lock-free queue will handle backpressure
channel->input_started_.store(false);
this->start_input(channel);
};
channel->input_started_.store(true);
this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize);
// Do the actual work (input_started_ already set to true by CAS above)
this->do_start_input_(channel);
}
void USBUartComponent::start_output(USBUartChannel *channel) {
@@ -370,7 +432,7 @@ void USBUartTypeCdcAcm::enable_channels() {
for (auto *channel : this->channels_) {
if (!channel->initialised_.load())
continue;
channel->input_started_.store(false);
this->reset_input_state_(channel);
channel->output_started_.store(false);
this->start_input(channel);
}

View File

@@ -111,10 +111,11 @@ class USBUartChannel : public uart::UARTComponent, public Parented<USBUartCompon
CdcEps cdc_dev_{};
// Enum (likely 4 bytes)
UARTParityOptions parity_{UART_CONFIG_PARITY_NONE};
// Group atomics together (each 1 byte)
// Group atomics together
std::atomic<bool> input_started_{true};
std::atomic<bool> output_started_{true};
std::atomic<bool> initialised_{false};
std::atomic<uint8_t> input_retry_count_{0};
// Group regular bytes together to minimize padding
const uint8_t index_;
bool debug_{};
@@ -140,6 +141,11 @@ class USBUartComponent : public usb_host::USBClient {
EventPool<UsbDataChunk, USB_DATA_QUEUE_SIZE> chunk_pool_;
protected:
void defer_input_retry_(USBUartChannel *channel);
void reset_input_state_(USBUartChannel *channel);
void restart_input_(USBUartChannel *channel);
void do_start_input_(USBUartChannel *channel);
void input_transfer_callback_(USBUartChannel *channel, const usb_host::TransferStatus &status);
std::vector<USBUartChannel *> channels_{};
};

View File

@@ -26,10 +26,10 @@ class ZephyrGPIOPin : public InternalGPIOPin {
protected:
void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
uint8_t pin_;
bool inverted_{};
gpio::Flags flags_{};
const device *gpio_{nullptr};
bool value_{false};
bool inverted_;
gpio::Flags flags_;
const device *gpio_ = nullptr;
bool value_ = false;
};
} // namespace zephyr

View File

@@ -79,18 +79,6 @@ template<typename... Ts> class LambdaCondition : public Condition<Ts...> {
std::function<bool(Ts...)> f_;
};
/// Optimized lambda condition for stateless lambdas (no capture).
/// Uses function pointer instead of std::function to reduce memory overhead.
/// Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function).
template<typename... Ts> class StatelessLambdaCondition : public Condition<Ts...> {
public:
explicit StatelessLambdaCondition(bool (*f)(Ts...)) : f_(f) {}
bool check(Ts... x) override { return this->f_(x...); }
protected:
bool (*f_)(Ts...);
};
template<typename... Ts> class ForCondition : public Condition<Ts...>, public Component {
public:
explicit ForCondition(Condition<> *condition) : condition_(condition) {}
@@ -202,19 +190,6 @@ template<typename... Ts> class LambdaAction : public Action<Ts...> {
std::function<void(Ts...)> f_;
};
/// Optimized lambda action for stateless lambdas (no capture).
/// Uses function pointer instead of std::function to reduce memory overhead.
/// Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function).
template<typename... Ts> class StatelessLambdaAction : public Action<Ts...> {
public:
explicit StatelessLambdaAction(void (*f)(Ts...)) : f_(f) {}
void play(Ts... x) override { this->f_(x...); }
protected:
void (*f_)(Ts...);
};
template<typename... Ts> class IfAction : public Action<Ts...> {
public:
explicit IfAction(Condition<Ts...> *condition) : condition_(condition) {}

View File

@@ -198,8 +198,6 @@ class LambdaExpression(Expression):
self.return_type = safe_exp(return_type) if return_type is not None else None
def __str__(self):
# Stateless lambdas (empty capture) implicitly convert to function pointers
# when assigned to function pointer types - no unary + needed
cpp = f"[{self.capture}]({self.parameters})"
if self.return_type is not None:
cpp += f" -> {self.return_type}"
@@ -702,12 +700,6 @@ async def process_lambda(
parts[i * 3 + 1] = var
parts[i * 3 + 2] = ""
# All id() references are global variables in generated C++ code.
# Global variables should not be captured - they're accessible everywhere.
# Use empty capture instead of capture-by-value.
if capture == "=":
capture = ""
if isinstance(value, ESPHomeDataBase) and value.esp_range is not None:
location = value.esp_range.start_mark
location.line += value.content_offset

View File

@@ -19,9 +19,7 @@ classifiers = [
"Programming Language :: Python :: 3",
"Topic :: Home Automation",
]
# Python 3.14 is currently not supported by IDF <= 5.5.1, see https://github.com/esphome/esphome/issues/11502
requires-python = ">=3.11.0,<3.14"
requires-python = ">=3.11.0"
dynamic = ["dependencies", "optional-dependencies", "version"]

View File

@@ -58,7 +58,7 @@ def test_text_config_value_mode_set(generate_main):
def test_text_config_lamda_is_set(generate_main):
"""
Test if lambda is set for lambda mode (optimized with stateless lambda)
Test if lambda is set for lambda mode
"""
# Given
@@ -66,5 +66,5 @@ def test_text_config_lamda_is_set(generate_main):
main_cpp = generate_main("tests/component_tests/text/test_text.yaml")
# Then
assert "it_4->set_template([]() -> esphome::optional<std::string> {" in main_cpp
assert "it_4->set_template([=]() -> esphome::optional<std::string> {" in main_cpp
assert 'return std::string{"Hello"};' in main_cpp

View File

@@ -173,61 +173,6 @@ class TestLambdaExpression:
"}"
)
def test_str__stateless_no_return(self):
"""Test stateless lambda (empty capture) generates correctly"""
target = cg.LambdaExpression(
('ESP_LOGD("main", "Test message");',),
(), # No parameters
"", # Empty capture (stateless)
)
actual = str(target)
assert actual == ('[]() {\n ESP_LOGD("main", "Test message");\n}')
def test_str__stateless_with_return(self):
"""Test stateless lambda with return type generates correctly"""
target = cg.LambdaExpression(
("return global_value > 0;",),
(), # No parameters
"", # Empty capture (stateless)
bool, # Return type
)
actual = str(target)
assert actual == ("[]() -> bool {\n return global_value > 0;\n}")
def test_str__stateless_with_params(self):
"""Test stateless lambda with parameters generates correctly"""
target = cg.LambdaExpression(
("return foo + bar;",),
((int, "foo"), (float, "bar")),
"", # Empty capture (stateless)
float,
)
actual = str(target)
assert actual == (
"[](int32_t foo, float bar) -> float {\n return foo + bar;\n}"
)
def test_str__with_capture(self):
"""Test lambda with capture generates correctly"""
target = cg.LambdaExpression(
("return captured_var + x;",),
((int, "x"),),
"captured_var", # Has capture (not stateless)
int,
)
actual = str(target)
assert actual == (
"[captured_var](int32_t x) -> int32_t {\n return captured_var + x;\n}"
)
class TestLiterals:
@pytest.mark.parametrize(