[climate] Fold ControlAction fields into a single stateless lambda

This commit is contained in:
J. Nick Koston
2026-04-29 06:30:46 -05:00
parent 1ea7d20d98
commit a35aecefee
2 changed files with 40 additions and 65 deletions
+30 -23
View File
@@ -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<uint16_t>({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)
+10 -42
View File
@@ -5,58 +5,26 @@
namespace esphome::climate {
// Unique Empty<Tag> per field so [[no_unique_address]] is guaranteed to coalesce.
namespace climate_control_detail {
template<int Tag> 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<uint16_t Fields, typename... Ts> class ControlAction : public Action<Ts...> {
// 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<typename... Ts> class ControlAction : public Action<Ts...> {
public:
explicit ControlAction(Climate *climate) : climate_(climate) {}
#define CLIMATE_FIELD_SETTER_(type, name, call_setter, idx) \
template<typename V> 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<type, Ts...>, \
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<ClimateCall &> {
public: