mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	merge
This commit is contained in:
		| @@ -10,6 +10,9 @@ 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) | ||||
| @@ -26,15 +29,22 @@ CONFIG_SCHEMA = ( | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = await binary_sensor.new_binary_sensor(config) | ||||
|     await cg.register_component(var, config) | ||||
|  | ||||
|     # Check if we have a lambda first - determines which class to instantiate | ||||
|     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) | ||||
|         ) | ||||
|         cg.add(var.set_template(template_)) | ||||
|     if condition := config.get(CONF_CONDITION): | ||||
|         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) | ||||
|         condition = await automation.build_condition( | ||||
|             condition, cg.TemplateArguments(), [] | ||||
|         ) | ||||
| @@ -42,6 +52,10 @@ 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( | ||||
|   | ||||
| @@ -6,18 +6,13 @@ namespace template_ { | ||||
|  | ||||
| static const char *const TAG = "template.binary_sensor"; | ||||
|  | ||||
| void TemplateBinarySensor::setup() { this->loop(); } | ||||
|  | ||||
| void TemplateBinarySensor::loop() { | ||||
|   if (this->f_ == nullptr) | ||||
|     return; | ||||
|  | ||||
|   auto s = this->f_(); | ||||
|   if (s.has_value()) { | ||||
|     this->publish_state(*s); | ||||
|   } | ||||
| // Template instantiations | ||||
| template<typename F> void TemplateBinarySensorBase<F>::dump_config() { | ||||
|   LOG_BINARY_SENSOR("", "Template Binary Sensor", this); | ||||
| } | ||||
| void TemplateBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Template Binary Sensor", this); } | ||||
|  | ||||
| template class TemplateBinarySensorBase<std::function<optional<bool>()>>; | ||||
| template class TemplateBinarySensorBase<optional<bool> (*)()>; | ||||
|  | ||||
| }  // namespace template_ | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -6,18 +6,41 @@ | ||||
| namespace esphome { | ||||
| namespace template_ { | ||||
|  | ||||
| class TemplateBinarySensor : public Component, public binary_sensor::BinarySensor { | ||||
| template<typename F> class TemplateBinarySensorBase : public Component, public binary_sensor::BinarySensor { | ||||
|  public: | ||||
|   void set_template(std::function<optional<bool>()> &&f) { this->f_ = f; } | ||||
|   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 setup() override; | ||||
|   void loop() override; | ||||
|   void dump_config() override; | ||||
|  | ||||
|   float get_setup_priority() const override { return setup_priority::HARDWARE; } | ||||
|  | ||||
|  protected: | ||||
|   std::function<optional<bool>()> f_{nullptr}; | ||||
|   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; } | ||||
| }; | ||||
|  | ||||
| }  // namespace template_ | ||||
|   | ||||
| @@ -23,6 +23,9 @@ 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 = { | ||||
| @@ -63,13 +66,22 @@ 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) | ||||
|         ) | ||||
|         cg.add(var.set_state_lambda(template_)) | ||||
|         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) | ||||
|  | ||||
|     if CONF_OPEN_ACTION in config: | ||||
|         await automation.build_automation( | ||||
|             var.get_open_trigger(), [], config[CONF_OPEN_ACTION] | ||||
|   | ||||
| @@ -8,14 +8,8 @@ using namespace esphome::cover; | ||||
|  | ||||
| static const char *const TAG = "template.cover"; | ||||
|  | ||||
| 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() { | ||||
| // Template instantiations | ||||
| template<typename StateF, typename TiltF> void TemplateCoverBase<StateF, TiltF>::setup() { | ||||
|   switch (this->restore_mode_) { | ||||
|     case COVER_NO_RESTORE: | ||||
|       break; | ||||
| @@ -34,43 +28,12 @@ void TemplateCover::setup() { | ||||
|     } | ||||
|   } | ||||
| } | ||||
| void TemplateCover::loop() { | ||||
|   bool changed = false; | ||||
|  | ||||
|   if (this->state_f_.has_value()) { | ||||
|     auto s = (*this->state_f_)(); | ||||
|     if (s.has_value()) { | ||||
|       auto pos = clamp(*s, 0.0f, 1.0f); | ||||
|       if (pos != this->position) { | ||||
|         this->position = pos; | ||||
|         changed = true; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   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>::dump_config() { | ||||
|   LOG_COVER("", "Template Cover", this); | ||||
| } | ||||
| 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) { | ||||
|  | ||||
| template<typename StateF, typename TiltF> void TemplateCoverBase<StateF, TiltF>::control(const CoverCall &call) { | ||||
|   if (call.get_stop()) { | ||||
|     this->stop_prev_trigger_(); | ||||
|     this->stop_trigger_->trigger(); | ||||
| @@ -113,7 +76,8 @@ void TemplateCover::control(const CoverCall &call) { | ||||
|  | ||||
|   this->publish_state(); | ||||
| } | ||||
| CoverTraits TemplateCover::get_traits() { | ||||
|  | ||||
| template<typename StateF, typename TiltF> CoverTraits TemplateCoverBase<StateF, TiltF>::get_traits() { | ||||
|   auto traits = CoverTraits(); | ||||
|   traits.set_is_assumed_state(this->assumed_state_); | ||||
|   traits.set_supports_stop(this->has_stop_); | ||||
| @@ -122,19 +86,16 @@ CoverTraits TemplateCover::get_traits() { | ||||
|   traits.set_supports_tilt(this->has_tilt_); | ||||
|   return 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(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_() { | ||||
|  | ||||
| template<typename StateF, typename TiltF> void TemplateCoverBase<StateF, TiltF>::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 | ||||
|   | ||||
| @@ -13,31 +13,59 @@ enum TemplateCoverRestoreMode { | ||||
|   COVER_RESTORE_AND_CALL, | ||||
| }; | ||||
|  | ||||
| class TemplateCover : public cover::Cover, public Component { | ||||
| template<typename StateF, typename TiltF> class TemplateCoverBase : public cover::Cover, public Component { | ||||
|  public: | ||||
|   TemplateCover(); | ||||
|   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>()) {} | ||||
|  | ||||
|   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 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 setup() override; | ||||
|   void loop() override; | ||||
|   void dump_config() override; | ||||
|   float get_setup_priority() const override { return setup_priority::HARDWARE; } | ||||
|  | ||||
|   float get_setup_priority() const override; | ||||
|   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; } | ||||
|  | ||||
|  protected: | ||||
|   void control(const cover::CoverCall &call) override; | ||||
| @@ -45,8 +73,8 @@ class TemplateCover : public cover::Cover, public Component { | ||||
|   void stop_prev_trigger_(); | ||||
|  | ||||
|   TemplateCoverRestoreMode restore_mode_{COVER_RESTORE}; | ||||
|   optional<std::function<optional<float>()>> state_f_; | ||||
|   optional<std::function<optional<float>()>> tilt_f_; | ||||
|   optional<StateF> state_f_; | ||||
|   optional<TiltF> tilt_f_; | ||||
|   bool assumed_state_{false}; | ||||
|   bool optimistic_{false}; | ||||
|   Trigger<> *open_trigger_; | ||||
| @@ -62,5 +90,22 @@ class TemplateCover : public cover::Cover, public Component { | ||||
|   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 | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_DAY, | ||||
|     CONF_HOUR, | ||||
|     CONF_ID, | ||||
|     CONF_INITIAL_VALUE, | ||||
|     CONF_LAMBDA, | ||||
|     CONF_MINUTE, | ||||
| @@ -25,14 +26,23 @@ 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): | ||||
| @@ -99,15 +109,30 @@ 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) | ||||
|         ) | ||||
|         cg.add(var.set_template(template_)) | ||||
|         # 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 | ||||
|  | ||||
|         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])) | ||||
|  | ||||
| @@ -146,5 +171,3 @@ async def to_code(config): | ||||
|             [(cg.ESPTime, "x")], | ||||
|             config[CONF_SET_ACTION], | ||||
|         ) | ||||
|  | ||||
|     await cg.register_component(var, config) | ||||
|   | ||||
| @@ -9,7 +9,8 @@ namespace template_ { | ||||
|  | ||||
| static const char *const TAG = "template.date"; | ||||
|  | ||||
| void TemplateDate::setup() { | ||||
| // Template instantiations | ||||
| template<typename F> void TemplateDateBase<F>::setup() { | ||||
|   if (this->f_.has_value()) | ||||
|     return; | ||||
|  | ||||
| @@ -36,21 +37,7 @@ void TemplateDate::setup() { | ||||
|   this->publish_state(); | ||||
| } | ||||
|  | ||||
| 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) { | ||||
| template<typename F> void TemplateDateBase<F>::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(); | ||||
| @@ -99,12 +86,15 @@ void TemplateDate::control(const datetime::DateCall &call) { | ||||
|   } | ||||
| } | ||||
|  | ||||
| void TemplateDate::dump_config() { | ||||
| template<typename F> void TemplateDateBase<F>::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 | ||||
|  | ||||
|   | ||||
| @@ -13,12 +13,23 @@ | ||||
| namespace esphome { | ||||
| namespace template_ { | ||||
|  | ||||
| class TemplateDate : public datetime::DateEntity, public PollingComponent { | ||||
| template<typename F> class TemplateDateBase : public datetime::DateEntity, public PollingComponent { | ||||
|  public: | ||||
|   void set_template(std::function<optional<ESPTime>()> &&f) { this->f_ = f; } | ||||
|   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 setup() override; | ||||
|   void update() override; | ||||
|   void dump_config() override; | ||||
|   float get_setup_priority() const override { return setup_priority::HARDWARE; } | ||||
|  | ||||
| @@ -35,11 +46,26 @@ class TemplateDate : public datetime::DateEntity, public PollingComponent { | ||||
|   ESPTime initial_value_{}; | ||||
|   bool restore_value_{false}; | ||||
|   Trigger<ESPTime> *set_trigger_ = new Trigger<ESPTime>(); | ||||
|   optional<std::function<optional<ESPTime>()>> f_; | ||||
|   optional<F> 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 | ||||
|  | ||||
|   | ||||
| @@ -9,7 +9,8 @@ namespace template_ { | ||||
|  | ||||
| static const char *const TAG = "template.datetime"; | ||||
|  | ||||
| void TemplateDateTime::setup() { | ||||
| // Template instantiations | ||||
| template<typename F> void TemplateDateTimeBase<F>::setup() { | ||||
|   if (this->f_.has_value()) | ||||
|     return; | ||||
|  | ||||
| @@ -39,24 +40,7 @@ void TemplateDateTime::setup() { | ||||
|   this->publish_state(); | ||||
| } | ||||
|  | ||||
| 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) { | ||||
| template<typename F> void TemplateDateTimeBase<F>::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(); | ||||
| @@ -138,12 +122,15 @@ void TemplateDateTime::control(const datetime::DateTimeCall &call) { | ||||
|   } | ||||
| } | ||||
|  | ||||
| void TemplateDateTime::dump_config() { | ||||
| template<typename F> void TemplateDateTimeBase<F>::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 | ||||
|  | ||||
|   | ||||
| @@ -13,12 +13,26 @@ | ||||
| namespace esphome { | ||||
| namespace template_ { | ||||
|  | ||||
| class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponent { | ||||
| template<typename F> class TemplateDateTimeBase : public datetime::DateTimeEntity, public PollingComponent { | ||||
|  public: | ||||
|   void set_template(std::function<optional<ESPTime>()> &&f) { this->f_ = f; } | ||||
|   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 setup() override; | ||||
|   void update() override; | ||||
|   void dump_config() override; | ||||
|   float get_setup_priority() const override { return setup_priority::HARDWARE; } | ||||
|  | ||||
| @@ -35,11 +49,26 @@ class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponen | ||||
|   ESPTime initial_value_{}; | ||||
|   bool restore_value_{false}; | ||||
|   Trigger<ESPTime> *set_trigger_ = new Trigger<ESPTime>(); | ||||
|   optional<std::function<optional<ESPTime>()>> f_; | ||||
|   optional<F> 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 | ||||
|  | ||||
|   | ||||
| @@ -9,7 +9,8 @@ namespace template_ { | ||||
|  | ||||
| static const char *const TAG = "template.time"; | ||||
|  | ||||
| void TemplateTime::setup() { | ||||
| // Template instantiations | ||||
| template<typename F> void TemplateTimeBase<F>::setup() { | ||||
|   if (this->f_.has_value()) | ||||
|     return; | ||||
|  | ||||
| @@ -36,21 +37,7 @@ void TemplateTime::setup() { | ||||
|   this->publish_state(); | ||||
| } | ||||
|  | ||||
| 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) { | ||||
| template<typename F> void TemplateTimeBase<F>::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(); | ||||
| @@ -99,12 +86,15 @@ void TemplateTime::control(const datetime::TimeCall &call) { | ||||
|   } | ||||
| } | ||||
|  | ||||
| void TemplateTime::dump_config() { | ||||
| template<typename F> void TemplateTimeBase<F>::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 | ||||
|  | ||||
|   | ||||
| @@ -13,12 +13,23 @@ | ||||
| namespace esphome { | ||||
| namespace template_ { | ||||
|  | ||||
| class TemplateTime : public datetime::TimeEntity, public PollingComponent { | ||||
| template<typename F> class TemplateTimeBase : public datetime::TimeEntity, public PollingComponent { | ||||
|  public: | ||||
|   void set_template(std::function<optional<ESPTime>()> &&f) { this->f_ = f; } | ||||
|   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 setup() override; | ||||
|   void update() override; | ||||
|   void dump_config() override; | ||||
|   float get_setup_priority() const override { return setup_priority::HARDWARE; } | ||||
|  | ||||
| @@ -35,11 +46,26 @@ class TemplateTime : public datetime::TimeEntity, public PollingComponent { | ||||
|   ESPTime initial_value_{}; | ||||
|   bool restore_value_{false}; | ||||
|   Trigger<ESPTime> *set_trigger_ = new Trigger<ESPTime>(); | ||||
|   optional<std::function<optional<ESPTime>()>> f_; | ||||
|   optional<F> 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 | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <set> | ||||
| #include <vector> | ||||
|  | ||||
| #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::set<std::string> &presets) { this->preset_modes_ = presets; } | ||||
|   void set_preset_modes(const std::initializer_list<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::set<std::string> preset_modes_{}; | ||||
|   std::vector<std::string> preset_modes_{}; | ||||
| }; | ||||
|  | ||||
| }  // namespace template_ | ||||
|   | ||||
| @@ -16,6 +16,9 @@ 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", | ||||
| @@ -55,14 +58,22 @@ 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) | ||||
|         ) | ||||
|         cg.add(var.set_state_lambda(template_)) | ||||
|         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) | ||||
|  | ||||
|     if CONF_UNLOCK_ACTION in config: | ||||
|         await automation.build_automation( | ||||
|             var.get_unlock_trigger(), [], config[CONF_UNLOCK_ACTION] | ||||
|   | ||||
| @@ -8,19 +8,8 @@ using namespace esphome::lock; | ||||
|  | ||||
| static const char *const TAG = "template.lock"; | ||||
|  | ||||
| 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) { | ||||
| // Template instantiations | ||||
| template<typename F> void TemplateLockBase<F>::control(const lock::LockCall &call) { | ||||
|   if (this->prev_trigger_ != nullptr) { | ||||
|     this->prev_trigger_->stop_action(); | ||||
|   } | ||||
| @@ -37,23 +26,22 @@ void TemplateLock::control(const lock::LockCall &call) { | ||||
|   if (this->optimistic_) | ||||
|     this->publish_state(state); | ||||
| } | ||||
| void TemplateLock::open_latch() { | ||||
|  | ||||
| template<typename F> void TemplateLockBase<F>::open_latch() { | ||||
|   if (this->prev_trigger_ != nullptr) { | ||||
|     this->prev_trigger_->stop_action(); | ||||
|   } | ||||
|   this->prev_trigger_ = this->open_trigger_; | ||||
|   this->open_trigger_->trigger(); | ||||
| } | ||||
| 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() { | ||||
|  | ||||
| template<typename F> void TemplateLockBase<F>::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 | ||||
|   | ||||
| @@ -7,26 +7,35 @@ | ||||
| namespace esphome { | ||||
| namespace template_ { | ||||
|  | ||||
| class TemplateLock : public lock::Lock, public Component { | ||||
| template<typename F> class TemplateLockBase : public lock::Lock, public Component { | ||||
|  public: | ||||
|   TemplateLock(); | ||||
|   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); | ||||
|   } | ||||
|  | ||||
|   void dump_config() override; | ||||
|  | ||||
|   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; | ||||
|   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; } | ||||
|  | ||||
|   float get_setup_priority() const override; | ||||
|   float get_setup_priority() const override { return setup_priority::HARDWARE; } | ||||
|  | ||||
|  protected: | ||||
|   void control(const lock::LockCall &call) override; | ||||
|   void open_latch() override; | ||||
|  | ||||
|   optional<std::function<optional<lock::LockState>()>> f_; | ||||
|   optional<F> f_; | ||||
|   bool optimistic_{false}; | ||||
|   Trigger<> *lock_trigger_; | ||||
|   Trigger<> *unlock_trigger_; | ||||
| @@ -34,5 +43,20 @@ class TemplateLock : public lock::Lock, public Component { | ||||
|   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 | ||||
|   | ||||
| @@ -19,6 +19,9 @@ 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): | ||||
| @@ -66,23 +69,33 @@ 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) | ||||
|         ) | ||||
|         cg.add(var.set_template(template_)) | ||||
|  | ||||
|         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], | ||||
|         ) | ||||
|     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: | ||||
|   | ||||
| @@ -6,7 +6,8 @@ namespace template_ { | ||||
|  | ||||
| static const char *const TAG = "template.number"; | ||||
|  | ||||
| void TemplateNumber::setup() { | ||||
| // Template instantiations | ||||
| template<typename F> void TemplateNumberBase<F>::setup() { | ||||
|   if (this->f_.has_value()) | ||||
|     return; | ||||
|  | ||||
| @@ -26,18 +27,7 @@ void TemplateNumber::setup() { | ||||
|   this->publish_state(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) { | ||||
| template<typename F> void TemplateNumberBase<F>::control(float value) { | ||||
|   this->set_trigger_->trigger(value); | ||||
|  | ||||
|   if (this->optimistic_) | ||||
| @@ -46,11 +36,15 @@ void TemplateNumber::control(float value) { | ||||
|   if (this->restore_value_) | ||||
|     this->pref_.save(&value); | ||||
| } | ||||
| void TemplateNumber::dump_config() { | ||||
|  | ||||
| template<typename F> void TemplateNumberBase<F>::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 | ||||
|   | ||||
| @@ -8,13 +8,22 @@ | ||||
| namespace esphome { | ||||
| namespace template_ { | ||||
|  | ||||
| class TemplateNumber : public number::Number, public PollingComponent { | ||||
| template<typename F> class TemplateNumberBase : public number::Number, public PollingComponent { | ||||
|  public: | ||||
|   void set_template(std::function<optional<float>()> &&f) { this->f_ = f; } | ||||
|   TemplateNumberBase() : set_trigger_(new Trigger<float>()) {} | ||||
|  | ||||
|   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_; } | ||||
| @@ -27,11 +36,26 @@ class TemplateNumber : public number::Number, public PollingComponent { | ||||
|   bool optimistic_{false}; | ||||
|   float initial_value_{NAN}; | ||||
|   bool restore_value_{false}; | ||||
|   Trigger<float> *set_trigger_ = new Trigger<float>(); | ||||
|   optional<std::function<optional<float>()>> f_; | ||||
|   Trigger<float> *set_trigger_; | ||||
|   optional<F> 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 | ||||
|   | ||||
| @@ -17,6 +17,9 @@ 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): | ||||
| @@ -62,22 +65,34 @@ 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) | ||||
|         ) | ||||
|         cg.add(var.set_template(template_)) | ||||
|  | ||||
|         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]) | ||||
|     else: | ||||
|         cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) | ||||
|         cg.add(var.set_initial_option(config[CONF_INITIAL_OPTION])) | ||||
|         # 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]) | ||||
|  | ||||
|         if CONF_RESTORE_VALUE in config: | ||||
|             cg.add(var.set_restore_value(config[CONF_RESTORE_VALUE])) | ||||
|         # 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_SET_ACTION in config: | ||||
|         await automation.build_automation( | ||||
|   | ||||
| @@ -6,49 +6,29 @@ namespace template_ { | ||||
|  | ||||
| static const char *const TAG = "template.select"; | ||||
|  | ||||
| void TemplateSelect::setup() { | ||||
| // Template instantiations | ||||
| template<typename F> void TemplateSelectBase<F>::setup() { | ||||
|   if (this->f_.has_value()) | ||||
|     return; | ||||
|  | ||||
|   std::string value; | ||||
|   if (!this->restore_value_) { | ||||
|     value = this->initial_option_; | ||||
|     ESP_LOGD(TAG, "State from initial: %s", value.c_str()); | ||||
|   } else { | ||||
|     size_t index; | ||||
|   size_t index = this->initial_option_index_; | ||||
|   if (this->restore_value_) { | ||||
|     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()); | ||||
|     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 { | ||||
|       value = this->at(index).value(); | ||||
|       ESP_LOGD(TAG, "State from restore: %s", value.c_str()); | ||||
|       ESP_LOGD(TAG, "State from initial (could not load or invalid stored index): %s", this->at(index).value().c_str()); | ||||
|     } | ||||
|   } else { | ||||
|     ESP_LOGD(TAG, "State from initial: %s", this->at(index).value().c_str()); | ||||
|   } | ||||
|  | ||||
|   this->publish_state(value); | ||||
|   this->publish_state(this->at(index).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) { | ||||
| template<typename F> void TemplateSelectBase<F>::control(const std::string &value) { | ||||
|   this->set_trigger_->trigger(value); | ||||
|  | ||||
|   if (this->optimistic_) | ||||
| @@ -60,7 +40,7 @@ void TemplateSelect::control(const std::string &value) { | ||||
|   } | ||||
| } | ||||
|  | ||||
| void TemplateSelect::dump_config() { | ||||
| template<typename F> void TemplateSelectBase<F>::dump_config() { | ||||
|   LOG_SELECT("", "Template Select", this); | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
|   if (this->f_.has_value()) | ||||
| @@ -69,8 +49,12 @@ void TemplateSelect::dump_config() { | ||||
|                 "  Optimistic: %s\n" | ||||
|                 "  Initial Option: %s\n" | ||||
|                 "  Restore Value: %s", | ||||
|                 YESNO(this->optimistic_), this->initial_option_.c_str(), YESNO(this->restore_value_)); | ||||
|                 YESNO(this->optimistic_), this->at(this->initial_option_index_).value().c_str(), | ||||
|                 YESNO(this->restore_value_)); | ||||
| } | ||||
|  | ||||
| template class TemplateSelectBase<std::function<optional<std::string>()>>; | ||||
| template class TemplateSelectBase<optional<std::string> (*)()>; | ||||
|  | ||||
| }  // namespace template_ | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -8,30 +8,58 @@ | ||||
| namespace esphome { | ||||
| namespace template_ { | ||||
|  | ||||
| class TemplateSelect : public select::Select, public PollingComponent { | ||||
| template<typename F> class TemplateSelectBase : public select::Select, public PollingComponent { | ||||
|  public: | ||||
|   void set_template(std::function<optional<std::string>()> &&f) { this->f_ = f; } | ||||
|   TemplateSelectBase() : set_trigger_(new Trigger<std::string>()) {} | ||||
|  | ||||
|   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(const std::string &initial_option) { this->initial_option_ = initial_option; } | ||||
|   void set_initial_option_index(size_t initial_option_index) { this->initial_option_index_ = initial_option_index; } | ||||
|   void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } | ||||
|  | ||||
|  protected: | ||||
|   void control(const std::string &value) override; | ||||
|   bool optimistic_ = false; | ||||
|   std::string initial_option_; | ||||
|   size_t initial_option_index_{0}; | ||||
|   bool restore_value_ = false; | ||||
|   Trigger<std::string> *set_trigger_ = new Trigger<std::string>(); | ||||
|   optional<std::function<optional<std::string>()>> f_; | ||||
|   Trigger<std::string> *set_trigger_; | ||||
|   optional<F> 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 | ||||
|   | ||||
| @@ -9,6 +9,9 @@ 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( | ||||
| @@ -25,14 +28,21 @@ 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) | ||||
|         ) | ||||
|         cg.add(var.set_template(template_)) | ||||
|         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) | ||||
|  | ||||
|  | ||||
| @automation.register_action( | ||||
|   | ||||
| @@ -7,21 +7,14 @@ namespace template_ { | ||||
|  | ||||
| static const char *const TAG = "template.sensor"; | ||||
|  | ||||
| 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() { | ||||
| // Template instantiations | ||||
| template<typename F> void TemplateSensorBase<F>::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 | ||||
|   | ||||
| @@ -6,18 +6,38 @@ | ||||
| namespace esphome { | ||||
| namespace template_ { | ||||
|  | ||||
| class TemplateSensor : public sensor::Sensor, public PollingComponent { | ||||
| template<typename F> class TemplateSensorBase : public sensor::Sensor, public PollingComponent { | ||||
|  public: | ||||
|   void set_template(std::function<optional<float>()> &&f); | ||||
|  | ||||
|   void update() override; | ||||
|   void update() override { | ||||
|     if (!this->f_.has_value()) | ||||
|       return; | ||||
|     auto val = (*this->f_)(); | ||||
|     if (val.has_value()) { | ||||
|       this->publish_state(*val); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void dump_config() override; | ||||
|  | ||||
|   float get_setup_priority() const override; | ||||
|   float get_setup_priority() const override { return setup_priority::HARDWARE; } | ||||
|  | ||||
|  protected: | ||||
|   optional<std::function<optional<float>()>> f_; | ||||
|   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; } | ||||
| }; | ||||
|  | ||||
| }  // namespace template_ | ||||
|   | ||||
| @@ -16,6 +16,9 @@ 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): | ||||
| @@ -55,14 +58,22 @@ 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) | ||||
|         ) | ||||
|         cg.add(var.set_state_lambda(template_)) | ||||
|         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) | ||||
|  | ||||
|     if CONF_TURN_OFF_ACTION in config: | ||||
|         await automation.build_automation( | ||||
|             var.get_turn_off_trigger(), [], config[CONF_TURN_OFF_ACTION] | ||||
|   | ||||
| @@ -6,18 +6,8 @@ namespace template_ { | ||||
|  | ||||
| static const char *const TAG = "template.switch"; | ||||
|  | ||||
| TemplateSwitch::TemplateSwitch() : turn_on_trigger_(new Trigger<>()), turn_off_trigger_(new Trigger<>()) {} | ||||
|  | ||||
| void TemplateSwitch::loop() { | ||||
|   if (!this->f_.has_value()) | ||||
|     return; | ||||
|   auto s = (*this->f_)(); | ||||
|   if (!s.has_value()) | ||||
|     return; | ||||
|  | ||||
|   this->publish_state(*s); | ||||
| } | ||||
| void TemplateSwitch::write_state(bool state) { | ||||
| // Template instantiations | ||||
| template<typename F> void TemplateSwitchBase<F>::write_state(bool state) { | ||||
|   if (this->prev_trigger_ != nullptr) { | ||||
|     this->prev_trigger_->stop_action(); | ||||
|   } | ||||
| @@ -33,13 +23,8 @@ void TemplateSwitch::write_state(bool state) { | ||||
|   if (this->optimistic_) | ||||
|     this->publish_state(state); | ||||
| } | ||||
| 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() { | ||||
|  | ||||
| template<typename F> void TemplateSwitchBase<F>::setup() { | ||||
|   optional<bool> initial_state = this->get_initial_state_with_restore_mode(); | ||||
|  | ||||
|   if (initial_state.has_value()) { | ||||
| @@ -52,11 +37,14 @@ void TemplateSwitch::setup() { | ||||
|     } | ||||
|   } | ||||
| } | ||||
| void TemplateSwitch::dump_config() { | ||||
|  | ||||
| template<typename F> void TemplateSwitchBase<F>::dump_config() { | ||||
|   LOG_SWITCH("", "Template Switch", this); | ||||
|   ESP_LOGCONFIG(TAG, "  Optimistic: %s", YESNO(this->optimistic_)); | ||||
| } | ||||
| void TemplateSwitch::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } | ||||
|  | ||||
| template class TemplateSwitchBase<std::function<optional<bool>()>>; | ||||
| template class TemplateSwitchBase<optional<bool> (*)()>; | ||||
|  | ||||
| }  // namespace template_ | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -7,28 +7,35 @@ | ||||
| namespace esphome { | ||||
| namespace template_ { | ||||
|  | ||||
| class TemplateSwitch : public switch_::Switch, public Component { | ||||
| template<typename F> class TemplateSwitchBase : public switch_::Switch, public Component { | ||||
|  public: | ||||
|   TemplateSwitch(); | ||||
|   TemplateSwitchBase() : turn_on_trigger_(new Trigger<>()), turn_off_trigger_(new Trigger<>()) {} | ||||
|  | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|  | ||||
|   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; | ||||
|   void loop() override { | ||||
|     if (!this->f_.has_value()) | ||||
|       return; | ||||
|     auto s = (*this->f_)(); | ||||
|     if (!s.has_value()) | ||||
|       return; | ||||
|     this->publish_state(*s); | ||||
|   } | ||||
|  | ||||
|   float get_setup_priority() const 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; } | ||||
|  | ||||
|  protected: | ||||
|   bool assumed_state() override; | ||||
|   bool assumed_state() override { return this->assumed_state_; } | ||||
|  | ||||
|   void write_state(bool state) override; | ||||
|  | ||||
|   optional<std::function<optional<bool>()>> f_; | ||||
|   optional<F> f_; | ||||
|   bool optimistic_{false}; | ||||
|   bool assumed_state_{false}; | ||||
|   Trigger<> *turn_on_trigger_; | ||||
| @@ -36,5 +43,20 @@ class TemplateSwitch : public switch_::Switch, public Component { | ||||
|   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 | ||||
|   | ||||
| @@ -3,6 +3,7 @@ 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, | ||||
| @@ -16,6 +17,9 @@ 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) | ||||
| @@ -65,21 +69,31 @@ 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) | ||||
|         ) | ||||
|         cg.add(var.set_template(template_)) | ||||
|  | ||||
|         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), | ||||
|         ) | ||||
|     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)) | ||||
|   | ||||
| @@ -6,7 +6,8 @@ namespace template_ { | ||||
|  | ||||
| static const char *const TAG = "template.text"; | ||||
|  | ||||
| void TemplateText::setup() { | ||||
| // Template instantiations | ||||
| template<typename F> void TemplateTextBase<F>::setup() { | ||||
|   if (!(this->f_ == nullptr)) { | ||||
|     if (this->f_.has_value()) | ||||
|       return; | ||||
| @@ -25,21 +26,7 @@ void TemplateText::setup() { | ||||
|     this->publish_state(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) { | ||||
| template<typename F> void TemplateTextBase<F>::control(const std::string &value) { | ||||
|   this->set_trigger_->trigger(value); | ||||
|  | ||||
|   if (this->optimistic_) | ||||
| @@ -51,11 +38,15 @@ void TemplateText::control(const std::string &value) { | ||||
|     } | ||||
|   } | ||||
| } | ||||
| void TemplateText::dump_config() { | ||||
|  | ||||
| template<typename F> void TemplateTextBase<F>::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 | ||||
|   | ||||
| @@ -59,13 +59,24 @@ template<uint8_t SZ> class TextSaver : public TemplateTextSaverBase { | ||||
|   } | ||||
| }; | ||||
|  | ||||
| class TemplateText : public text::Text, public PollingComponent { | ||||
| template<typename F> class TemplateTextBase : public text::Text, public PollingComponent { | ||||
|  public: | ||||
|   void set_template(std::function<optional<std::string>()> &&f) { this->f_ = f; } | ||||
|   TemplateTextBase() : set_trigger_(new Trigger<std::string>()) {} | ||||
|  | ||||
|   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_; } | ||||
| @@ -77,11 +88,26 @@ class TemplateText : public text::Text, public PollingComponent { | ||||
|   void control(const std::string &value) override; | ||||
|   bool optimistic_ = false; | ||||
|   std::string initial_value_; | ||||
|   Trigger<std::string> *set_trigger_ = new Trigger<std::string>(); | ||||
|   optional<std::function<optional<std::string>()>> f_{nullptr}; | ||||
|   Trigger<std::string> *set_trigger_; | ||||
|   optional<F> 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 | ||||
|   | ||||
| @@ -10,6 +10,9 @@ 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() | ||||
| @@ -24,14 +27,21 @@ 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) | ||||
|         ) | ||||
|         cg.add(var.set_template(template_)) | ||||
|         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) | ||||
|  | ||||
|  | ||||
| @automation.register_action( | ||||
|   | ||||
| @@ -6,18 +6,11 @@ namespace template_ { | ||||
|  | ||||
| static const char *const TAG = "template.text_sensor"; | ||||
|  | ||||
| void TemplateTextSensor::update() { | ||||
|   if (!this->f_.has_value()) | ||||
|     return; | ||||
| // Template instantiations | ||||
| template<typename F> void TemplateTextSensorBase<F>::dump_config() { LOG_TEXT_SENSOR("", "Template Sensor", this); } | ||||
|  | ||||
|   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); } | ||||
| template class TemplateTextSensorBase<std::function<optional<std::string>()>>; | ||||
| template class TemplateTextSensorBase<optional<std::string> (*)()>; | ||||
|  | ||||
| }  // namespace template_ | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -7,18 +7,38 @@ | ||||
| namespace esphome { | ||||
| namespace template_ { | ||||
|  | ||||
| class TemplateTextSensor : public text_sensor::TextSensor, public PollingComponent { | ||||
| template<typename F> class TemplateTextSensorBase : public text_sensor::TextSensor, public PollingComponent { | ||||
|  public: | ||||
|   void set_template(std::function<optional<std::string>()> &&f); | ||||
|   void update() override { | ||||
|     if (!this->f_.has_value()) | ||||
|       return; | ||||
|     auto val = (*this->f_)(); | ||||
|     if (val.has_value()) { | ||||
|       this->publish_state(*val); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void update() override; | ||||
|  | ||||
|   float get_setup_priority() const override; | ||||
|   float get_setup_priority() const override { return setup_priority::HARDWARE; } | ||||
|  | ||||
|   void dump_config() override; | ||||
|  | ||||
|  protected: | ||||
|   optional<std::function<optional<std::string>()>> f_{}; | ||||
|   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; } | ||||
| }; | ||||
|  | ||||
| }  // namespace template_ | ||||
|   | ||||
| @@ -20,6 +20,9 @@ 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) | ||||
| @@ -62,13 +65,22 @@ 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) | ||||
|         ) | ||||
|         cg.add(var.set_state_lambda(template_)) | ||||
|         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) | ||||
|  | ||||
|     if open_action_config := config.get(CONF_OPEN_ACTION): | ||||
|         await automation.build_automation( | ||||
|             var.get_open_trigger(), [], open_action_config | ||||
|   | ||||
| @@ -8,14 +8,8 @@ using namespace esphome::valve; | ||||
|  | ||||
| static const char *const TAG = "template.valve"; | ||||
|  | ||||
| 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() { | ||||
| // Template instantiations | ||||
| template<typename F> void TemplateValveBase<F>::setup() { | ||||
|   switch (this->restore_mode_) { | ||||
|     case VALVE_NO_RESTORE: | ||||
|       break; | ||||
| @@ -35,35 +29,7 @@ void TemplateValve::setup() { | ||||
|   } | ||||
| } | ||||
|  | ||||
| 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() { | ||||
| template<typename F> void TemplateValveBase<F>::dump_config() { | ||||
|   LOG_VALVE("", "Template Valve", this); | ||||
|   ESP_LOGCONFIG(TAG, | ||||
|                 "  Has position: %s\n" | ||||
| @@ -71,7 +37,7 @@ void TemplateValve::dump_config() { | ||||
|                 YESNO(this->has_position_), YESNO(this->optimistic_)); | ||||
| } | ||||
|  | ||||
| void TemplateValve::control(const ValveCall &call) { | ||||
| template<typename F> void TemplateValveBase<F>::control(const ValveCall &call) { | ||||
|   if (call.get_stop()) { | ||||
|     this->stop_prev_trigger_(); | ||||
|     this->stop_trigger_->trigger(); | ||||
| @@ -106,7 +72,7 @@ void TemplateValve::control(const ValveCall &call) { | ||||
|   this->publish_state(); | ||||
| } | ||||
|  | ||||
| ValveTraits TemplateValve::get_traits() { | ||||
| template<typename F> valve::ValveTraits TemplateValveBase<F>::get_traits() { | ||||
|   auto traits = ValveTraits(); | ||||
|   traits.set_is_assumed_state(this->assumed_state_); | ||||
|   traits.set_supports_stop(this->has_stop_); | ||||
| @@ -115,18 +81,15 @@ ValveTraits TemplateValve::get_traits() { | ||||
|   return traits; | ||||
| } | ||||
|  | ||||
| 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_() { | ||||
| template<typename F> void TemplateValveBase<F>::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 | ||||
|   | ||||
| @@ -13,28 +13,48 @@ enum TemplateValveRestoreMode { | ||||
|   VALVE_RESTORE_AND_CALL, | ||||
| }; | ||||
|  | ||||
| class TemplateValve : public valve::Valve, public Component { | ||||
| template<typename F> class TemplateValveBase : public valve::Valve, public Component { | ||||
|  public: | ||||
|   TemplateValve(); | ||||
|   TemplateValveBase() | ||||
|       : open_trigger_(new Trigger<>()), | ||||
|         close_trigger_(new Trigger<>()), | ||||
|         stop_trigger_(new Trigger<>()), | ||||
|         toggle_trigger_(new Trigger<>()), | ||||
|         position_trigger_(new Trigger<float>()) {} | ||||
|  | ||||
|   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 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 setup() override; | ||||
|   void loop() override; | ||||
|   void dump_config() override; | ||||
|   float get_setup_priority() const override { return setup_priority::HARDWARE; } | ||||
|  | ||||
|   float get_setup_priority() const override; | ||||
|   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; } | ||||
|  | ||||
|  protected: | ||||
|   void control(const valve::ValveCall &call) override; | ||||
| @@ -42,7 +62,7 @@ class TemplateValve : public valve::Valve, public Component { | ||||
|   void stop_prev_trigger_(); | ||||
|  | ||||
|   TemplateValveRestoreMode restore_mode_{VALVE_NO_RESTORE}; | ||||
|   optional<std::function<optional<float>()>> state_f_; | ||||
|   optional<F> state_f_; | ||||
|   bool assumed_state_{false}; | ||||
|   bool optimistic_{false}; | ||||
|   Trigger<> *open_trigger_; | ||||
| @@ -56,5 +76,20 @@ class TemplateValve : public valve::Valve, public Component { | ||||
|   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 | ||||
|   | ||||
| @@ -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 | ||||
|     Test if lambda is set for lambda mode (optimized with stateless lambda) | ||||
|     """ | ||||
|     # 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 | ||||
|   | ||||
| @@ -41,6 +41,17 @@ select: | ||||
|       - ""  # Empty string at the end | ||||
|     initial_option: "Choice X" | ||||
|  | ||||
|   - platform: template | ||||
|     name: "Select Initial Option Test" | ||||
|     id: select_initial_option_test | ||||
|     optimistic: true | ||||
|     options: | ||||
|       - "First" | ||||
|       - "Second" | ||||
|       - "Third" | ||||
|       - "Fourth" | ||||
|     initial_option: "Third"  # Test non-default initial option | ||||
|  | ||||
| # Add a sensor to ensure we have other entities in the list | ||||
| sensor: | ||||
|   - platform: template | ||||
|   | ||||
| @@ -44,6 +44,7 @@ class InitialStateHelper: | ||||
|         helper = InitialStateHelper(entities) | ||||
|         client.subscribe_states(helper.on_state_wrapper(user_callback)) | ||||
|         await helper.wait_for_initial_states() | ||||
|         # Access initial states via helper.initial_states[key] | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, entities: list[EntityInfo]) -> None: | ||||
| @@ -63,6 +64,8 @@ class InitialStateHelper: | ||||
|         self._entities_by_id = { | ||||
|             (entity.device_id, entity.key): entity for entity in entities | ||||
|         } | ||||
|         # Store initial states by key for test access | ||||
|         self.initial_states: dict[int, EntityState] = {} | ||||
|  | ||||
|         # Log all entities | ||||
|         _LOGGER.debug( | ||||
| @@ -127,6 +130,9 @@ class InitialStateHelper: | ||||
|  | ||||
|             # If this entity is waiting for initial state | ||||
|             if entity_id in self._wait_initial_states: | ||||
|                 # Store the initial state for test access | ||||
|                 self.initial_states[state.key] = state | ||||
|  | ||||
|                 # Remove from waiting set | ||||
|                 self._wait_initial_states.discard(entity_id) | ||||
|  | ||||
|   | ||||
| @@ -2,12 +2,11 @@ | ||||
|  | ||||
| from __future__ import annotations | ||||
|  | ||||
| import asyncio | ||||
|  | ||||
| import aioesphomeapi | ||||
| from aioesphomeapi import ClimateAction, ClimateMode, ClimatePreset, EntityState | ||||
| from aioesphomeapi import ClimateAction, ClimateInfo, ClimateMode, ClimatePreset | ||||
| import pytest | ||||
|  | ||||
| from .state_utils import InitialStateHelper | ||||
| from .types import APIClientConnectedFactory, RunCompiledFunction | ||||
|  | ||||
|  | ||||
| @@ -18,26 +17,27 @@ async def test_host_mode_climate_basic_state( | ||||
|     api_client_connected: APIClientConnectedFactory, | ||||
| ) -> None: | ||||
|     """Test basic climate state reporting.""" | ||||
|     loop = asyncio.get_running_loop() | ||||
|     async with run_compiled(yaml_config), api_client_connected() as client: | ||||
|         states: dict[int, EntityState] = {} | ||||
|         climate_future: asyncio.Future[EntityState] = loop.create_future() | ||||
|         # Get entities and set up state synchronization | ||||
|         entities, services = await client.list_entities_services() | ||||
|         initial_state_helper = InitialStateHelper(entities) | ||||
|         climate_infos = [e for e in entities if isinstance(e, ClimateInfo)] | ||||
|         assert len(climate_infos) >= 1, "Expected at least 1 climate entity" | ||||
|  | ||||
|         def on_state(state: EntityState) -> None: | ||||
|             states[state.key] = state | ||||
|             if ( | ||||
|                 isinstance(state, aioesphomeapi.ClimateState) | ||||
|                 and not climate_future.done() | ||||
|             ): | ||||
|                 climate_future.set_result(state) | ||||
|  | ||||
|         client.subscribe_states(on_state) | ||||
|         # Subscribe with the wrapper (no-op callback since we just want initial states) | ||||
|         client.subscribe_states(initial_state_helper.on_state_wrapper(lambda _: None)) | ||||
|  | ||||
|         # Wait for all initial states to be broadcast | ||||
|         try: | ||||
|             climate_state = await asyncio.wait_for(climate_future, timeout=5.0) | ||||
|             await initial_state_helper.wait_for_initial_states() | ||||
|         except TimeoutError: | ||||
|             pytest.fail("Climate state not received within 5 seconds") | ||||
|             pytest.fail("Timeout waiting for initial states") | ||||
|  | ||||
|         # Get the climate entity and its initial state | ||||
|         test_climate = climate_infos[0] | ||||
|         climate_state = initial_state_helper.initial_states.get(test_climate.key) | ||||
|  | ||||
|         assert climate_state is not None, "Climate initial state not found" | ||||
|         assert isinstance(climate_state, aioesphomeapi.ClimateState) | ||||
|         assert climate_state.mode == ClimateMode.OFF | ||||
|         assert climate_state.action == ClimateAction.OFF | ||||
|   | ||||
| @@ -36,8 +36,8 @@ async def test_host_mode_empty_string_options( | ||||
|  | ||||
|         # Find our select entities | ||||
|         select_entities = [e for e in entity_info if isinstance(e, SelectInfo)] | ||||
|         assert len(select_entities) == 3, ( | ||||
|             f"Expected 3 select entities, got {len(select_entities)}" | ||||
|         assert len(select_entities) == 4, ( | ||||
|             f"Expected 4 select entities, got {len(select_entities)}" | ||||
|         ) | ||||
|  | ||||
|         # Verify each select entity by name and check their options | ||||
| @@ -71,6 +71,15 @@ async def test_host_mode_empty_string_options( | ||||
|         assert empty_last.options[2] == "Choice Z" | ||||
|         assert empty_last.options[3] == ""  # Empty string at end | ||||
|  | ||||
|         # Check "Select Initial Option Test" - verify non-default initial option | ||||
|         assert "Select Initial Option Test" in selects_by_name | ||||
|         initial_option_test = selects_by_name["Select Initial Option Test"] | ||||
|         assert len(initial_option_test.options) == 4 | ||||
|         assert initial_option_test.options[0] == "First" | ||||
|         assert initial_option_test.options[1] == "Second" | ||||
|         assert initial_option_test.options[2] == "Third" | ||||
|         assert initial_option_test.options[3] == "Fourth" | ||||
|  | ||||
|         # If we got here without protobuf decoding errors, the fix is working | ||||
|         # The bug would have caused "Invalid protobuf message" errors with trailing bytes | ||||
|  | ||||
| @@ -78,7 +87,12 @@ async def test_host_mode_empty_string_options( | ||||
|         # This ensures empty strings work properly in state messages too | ||||
|         states: dict[int, EntityState] = {} | ||||
|         states_received_future: asyncio.Future[None] = loop.create_future() | ||||
|         expected_select_keys = {empty_first.key, empty_middle.key, empty_last.key} | ||||
|         expected_select_keys = { | ||||
|             empty_first.key, | ||||
|             empty_middle.key, | ||||
|             empty_last.key, | ||||
|             initial_option_test.key, | ||||
|         } | ||||
|         received_select_keys = set() | ||||
|  | ||||
|         def on_state(state: EntityState) -> None: | ||||
| @@ -109,6 +123,14 @@ async def test_host_mode_empty_string_options( | ||||
|         assert empty_first.key in states | ||||
|         assert empty_middle.key in states | ||||
|         assert empty_last.key in states | ||||
|         assert initial_option_test.key in states | ||||
|  | ||||
|         # Verify the initial option is set correctly to "Third" (not the default "First") | ||||
|         initial_state = states[initial_option_test.key] | ||||
|         assert initial_state.state == "Third", ( | ||||
|             f"Expected initial state 'Third' but got '{initial_state.state}' - " | ||||
|             f"initial_option not correctly applied" | ||||
|         ) | ||||
|  | ||||
|         # The main test is that we got here without protobuf errors | ||||
|         # The select entities with empty string options were properly encoded | ||||
|   | ||||
| @@ -670,3 +670,45 @@ class TestEsphomeCore: | ||||
|             os.environ.pop("ESPHOME_IS_HA_ADDON", None) | ||||
|             os.environ.pop("ESPHOME_DATA_DIR", None) | ||||
|             assert target.data_dir == Path(expected_default) | ||||
|  | ||||
|     def test_platformio_cache_dir_with_env_var(self): | ||||
|         """Test platformio_cache_dir when PLATFORMIO_CACHE_DIR env var is set.""" | ||||
|         target = core.EsphomeCore() | ||||
|         test_cache_dir = "/custom/cache/dir" | ||||
|  | ||||
|         with patch.dict(os.environ, {"PLATFORMIO_CACHE_DIR": test_cache_dir}): | ||||
|             assert target.platformio_cache_dir == test_cache_dir | ||||
|  | ||||
|     def test_platformio_cache_dir_without_env_var(self): | ||||
|         """Test platformio_cache_dir defaults to ~/.platformio/.cache.""" | ||||
|         target = core.EsphomeCore() | ||||
|  | ||||
|         with patch.dict(os.environ, {}, clear=True): | ||||
|             # Ensure env var is not set | ||||
|             os.environ.pop("PLATFORMIO_CACHE_DIR", None) | ||||
|             expected = os.path.expanduser("~/.platformio/.cache") | ||||
|             assert target.platformio_cache_dir == expected | ||||
|  | ||||
|     def test_platformio_cache_dir_empty_env_var(self): | ||||
|         """Test platformio_cache_dir with empty env var falls back to default.""" | ||||
|         target = core.EsphomeCore() | ||||
|  | ||||
|         with patch.dict(os.environ, {"PLATFORMIO_CACHE_DIR": ""}): | ||||
|             expected = os.path.expanduser("~/.platformio/.cache") | ||||
|             assert target.platformio_cache_dir == expected | ||||
|  | ||||
|     def test_platformio_cache_dir_whitespace_env_var(self): | ||||
|         """Test platformio_cache_dir with whitespace-only env var falls back to default.""" | ||||
|         target = core.EsphomeCore() | ||||
|  | ||||
|         with patch.dict(os.environ, {"PLATFORMIO_CACHE_DIR": "   "}): | ||||
|             expected = os.path.expanduser("~/.platformio/.cache") | ||||
|             assert target.platformio_cache_dir == expected | ||||
|  | ||||
|     def test_platformio_cache_dir_docker_addon_path(self): | ||||
|         """Test platformio_cache_dir in Docker/HA addon environment.""" | ||||
|         target = core.EsphomeCore() | ||||
|         addon_cache = "/data/cache/platformio" | ||||
|  | ||||
|         with patch.dict(os.environ, {"PLATFORMIO_CACHE_DIR": addon_cache}): | ||||
|             assert target.platformio_cache_dir == addon_cache | ||||
|   | ||||
| @@ -173,6 +173,61 @@ 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( | ||||
|   | ||||
| @@ -355,6 +355,7 @@ def test_clean_build( | ||||
|     mock_core.relative_pioenvs_path.return_value = pioenvs_dir | ||||
|     mock_core.relative_piolibdeps_path.return_value = piolibdeps_dir | ||||
|     mock_core.relative_build_path.return_value = dependencies_lock | ||||
|     mock_core.platformio_cache_dir = str(platformio_cache_dir) | ||||
|  | ||||
|     # Verify all exist before | ||||
|     assert pioenvs_dir.exists() | ||||
|   | ||||
		Reference in New Issue
	
	Block a user