diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 413a2a5b56..b033776f27 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -48,13 +48,13 @@ from esphome.const import ( CONF_VISUAL, CONF_WEB_SERVER, ) -from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.core import CORE, CoroPriority, Lambda, coroutine_with_priority from esphome.core.entity_helpers import ( entity_duplicate_validator, queue_entity_register, setup_entity, ) -from esphome.cpp_generator import MockObjClass +from esphome.cpp_generator import LambdaExpression, MockObjClass IS_PLATFORM_COMPONENT = True @@ -488,7 +488,11 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema( async def climate_control_to_code(config, action_id, template_arg, args): paren = await cg.get_variable(config[CONF_ID]) - # Order/bits must match CLIMATE_CONTROL_FIELDS in automation.h. + # All configured fields are folded into a single stateless lambda whose + # constants live in flash; the action stores only a function pointer. + # `call_setter` is the ClimateCall method invoked in the lambda body — + # for custom_fan_mode/custom_preset this dispatches to the std::string + # overload of set_fan_mode/set_preset respectively. FIELDS = ( (CONF_MODE, "set_mode", ClimateMode), (CONF_TARGET_TEMPERATURE, "set_target_temperature", cg.float_), @@ -496,32 +500,35 @@ async def climate_control_to_code(config, action_id, template_arg, args): (CONF_TARGET_TEMPERATURE_HIGH, "set_target_temperature_high", cg.float_), (CONF_TARGET_HUMIDITY, "set_target_humidity", cg.float_), (CONF_FAN_MODE, "set_fan_mode", ClimateFanMode), - ( - CONF_CUSTOM_FAN_MODE, - "set_custom_fan_mode", - cg.std_string, - ), # internal setter name + (CONF_CUSTOM_FAN_MODE, "set_fan_mode", cg.std_string), (CONF_PRESET, "set_preset", ClimatePreset), - ( - CONF_CUSTOM_PRESET, - "set_custom_preset", - cg.std_string, - ), # internal setter name + (CONF_CUSTOM_PRESET, "set_preset", cg.std_string), (CONF_SWING_MODE, "set_swing_mode", ClimateSwingMode), ) - assert len(FIELDS) <= 16, "ControlAction Fields bitmask exceeds uint16_t" - field_mask = sum(1 << i for i, (k, _, _) in enumerate(FIELDS) if k in config) - control_template_arg = cg.TemplateArguments( - cg.RawExpression(f"static_cast({field_mask})"), *template_arg - ) - var = cg.new_Pvariable(action_id, control_template_arg, paren) + fwd_args = ", ".join(name for _, name in args) + body_lines: list[str] = [] for conf_key, setter, type_ in FIELDS: - if (value := config.get(conf_key)) is not None: - template_ = await cg.templatable(value, args, type_) - cg.add(getattr(var, setter)(template_)) - return var + if (value := config.get(conf_key)) is None: + continue + if isinstance(value, Lambda): + inner = await cg.process_lambda(value, args, return_type=type_) + body_lines.append(f"call.{setter}(({inner})({fwd_args}));") + else: + body_lines.append(f"call.{setter}({cg.safe_exp(value)});") + + apply_args = [ + (ClimateCall.operator("ref"), "call"), + *args, + ] + apply_lambda = LambdaExpression( + ["\n".join(body_lines)], + apply_args, + capture="", + return_type=cg.void, + ) + return cg.new_Pvariable(action_id, template_arg, paren, apply_lambda) @coroutine_with_priority(CoroPriority.CORE) diff --git a/esphome/components/climate/automation.h b/esphome/components/climate/automation.h index 9e38ef6407..b0353a41f9 100644 --- a/esphome/components/climate/automation.h +++ b/esphome/components/climate/automation.h @@ -5,58 +5,26 @@ namespace esphome::climate { -// Unique Empty per field so [[no_unique_address]] is guaranteed to coalesce. -namespace climate_control_detail { -template struct Empty {}; -} // namespace climate_control_detail - -// X-macro: (type, field_name, call_setter, bit_index). Order and bit values must -// match the FIELDS table in __init__.py. call_setter is the ClimateCall method -// invoked in play() — for custom_fan_mode/custom_preset this dispatches to the -// std::string overload of set_fan_mode/set_preset respectively. -#define CLIMATE_CONTROL_FIELDS(X) \ - X(ClimateMode, mode, set_mode, 0) \ - X(float, target_temperature, set_target_temperature, 1) \ - X(float, target_temperature_low, set_target_temperature_low, 2) \ - X(float, target_temperature_high, set_target_temperature_high, 3) \ - X(float, target_humidity, set_target_humidity, 4) \ - X(ClimateFanMode, fan_mode, set_fan_mode, 5) \ - X(std::string, custom_fan_mode, set_fan_mode, 6) \ - X(ClimatePreset, preset, set_preset, 7) \ - X(std::string, custom_preset, set_preset, 8) \ - X(ClimateSwingMode, swing_mode, set_swing_mode, 9) - -template class ControlAction : public Action { +// All configured fields are baked into a single stateless lambda whose +// constants live in flash. The action only stores a function pointer +// (4 bytes) plus the parent (4 bytes), regardless of how many fields the +// user set. Trigger args are forwarded to the apply function so user +// lambdas (e.g. `target_temperature: !lambda "return x;"`) keep working. +template class ControlAction : public Action { public: - explicit ControlAction(Climate *climate) : climate_(climate) {} - -#define CLIMATE_FIELD_SETTER_(type, name, call_setter, idx) \ - template void set_##name(V value) requires((Fields & (1 << (idx))) != 0) { this->name##_ = value; } -#define CLIMATE_FIELD_APPLY_(type, name, call_setter, idx) \ - if constexpr ((Fields & (1 << (idx))) != 0) \ - call.call_setter(this->name##_.value(x...)); -#define CLIMATE_FIELD_DECL_(type, name, call_setter, idx) \ - [[no_unique_address]] std::conditional_t<(Fields & (1 << (idx))) != 0, TemplatableStorage, \ - climate_control_detail::Empty<(idx)>> \ - name##_{}; - - CLIMATE_CONTROL_FIELDS(CLIMATE_FIELD_SETTER_) + using ApplyFn = void (*)(ClimateCall &, Ts...); + ControlAction(Climate *climate, ApplyFn apply) : climate_(climate), apply_(apply) {} void play(const Ts &...x) override { auto call = this->climate_->make_call(); - CLIMATE_CONTROL_FIELDS(CLIMATE_FIELD_APPLY_) + this->apply_(call, x...); call.perform(); } protected: Climate *climate_; - CLIMATE_CONTROL_FIELDS(CLIMATE_FIELD_DECL_) - -#undef CLIMATE_FIELD_DECL_ -#undef CLIMATE_FIELD_APPLY_ -#undef CLIMATE_FIELD_SETTER_ + ApplyFn apply_; }; -#undef CLIMATE_CONTROL_FIELDS class ControlTrigger : public Trigger { public: