From 1a02f7e41f1a495b67291be4135e83121c774497 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 27 Apr 2026 04:36:44 -0500 Subject: [PATCH] [climate] Use bitmask template parameter for ControlAction unused fields Apply the bitmask pattern from LightControlAction (#16039) to climate::ControlAction. Parameterize on a uint16_t Fields bitmask encoding which of the 10 templatable fields are configured. Unused fields are elided via [[no_unique_address]] and skipped in play() via if constexpr. Also drop the unused `away` TEMPLATABLE_VALUE -- its YAML key is cv.invalid("Use preset instead") and no caller of set_away exists in the repo. Per-instance: 20 B (mode only) to 28 B (mode + 2 temps), down from ~64-72 B baseline. --- esphome/components/climate/__init__.py | 65 +++++++++++++------------ esphome/components/climate/automation.h | 65 +++++++++++++++---------- 2 files changed, 74 insertions(+), 56 deletions(-) diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 0fdb18a92c8..413a2a5b569 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -487,37 +487,40 @@ 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]) - var = cg.new_Pvariable(action_id, template_arg, paren) - if (mode := config.get(CONF_MODE)) is not None: - template_ = await cg.templatable(mode, args, ClimateMode) - cg.add(var.set_mode(template_)) - if (target_temp := config.get(CONF_TARGET_TEMPERATURE)) is not None: - template_ = await cg.templatable(target_temp, args, cg.float_) - cg.add(var.set_target_temperature(template_)) - if (target_temp_low := config.get(CONF_TARGET_TEMPERATURE_LOW)) is not None: - template_ = await cg.templatable(target_temp_low, args, cg.float_) - cg.add(var.set_target_temperature_low(template_)) - if (target_temp_high := config.get(CONF_TARGET_TEMPERATURE_HIGH)) is not None: - template_ = await cg.templatable(target_temp_high, args, cg.float_) - cg.add(var.set_target_temperature_high(template_)) - if (target_humidity := config.get(CONF_TARGET_HUMIDITY)) is not None: - template_ = await cg.templatable(target_humidity, args, cg.float_) - cg.add(var.set_target_humidity(template_)) - if (fan_mode := config.get(CONF_FAN_MODE)) is not None: - template_ = await cg.templatable(fan_mode, args, ClimateFanMode) - cg.add(var.set_fan_mode(template_)) - if (custom_fan_mode := config.get(CONF_CUSTOM_FAN_MODE)) is not None: - template_ = await cg.templatable(custom_fan_mode, args, cg.std_string) - cg.add(var.set_custom_fan_mode(template_)) - if (preset := config.get(CONF_PRESET)) is not None: - template_ = await cg.templatable(preset, args, ClimatePreset) - cg.add(var.set_preset(template_)) - if (custom_preset := config.get(CONF_CUSTOM_PRESET)) is not None: - template_ = await cg.templatable(custom_preset, args, cg.std_string) - cg.add(var.set_custom_preset(template_)) - if (swing_mode := config.get(CONF_SWING_MODE)) is not None: - template_ = await cg.templatable(swing_mode, args, ClimateSwingMode) - cg.add(var.set_swing_mode(template_)) + + # Order/bits must match CLIMATE_CONTROL_FIELDS in automation.h. + FIELDS = ( + (CONF_MODE, "set_mode", ClimateMode), + (CONF_TARGET_TEMPERATURE, "set_target_temperature", cg.float_), + (CONF_TARGET_TEMPERATURE_LOW, "set_target_temperature_low", cg.float_), + (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_PRESET, "set_preset", ClimatePreset), + ( + CONF_CUSTOM_PRESET, + "set_custom_preset", + cg.std_string, + ), # internal setter name + (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) + + 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 diff --git a/esphome/components/climate/automation.h b/esphome/components/climate/automation.h index fac56d9d9e9..9e38ef64079 100644 --- a/esphome/components/climate/automation.h +++ b/esphome/components/climate/automation.h @@ -5,43 +5,58 @@ namespace esphome::climate { -template class ControlAction : public Action { +// 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 { public: explicit ControlAction(Climate *climate) : climate_(climate) {} - TEMPLATABLE_VALUE(ClimateMode, mode) - TEMPLATABLE_VALUE(float, target_temperature) - TEMPLATABLE_VALUE(float, target_temperature_low) - TEMPLATABLE_VALUE(float, target_temperature_high) - TEMPLATABLE_VALUE(float, target_humidity) - TEMPLATABLE_VALUE(bool, away) - TEMPLATABLE_VALUE(ClimateFanMode, fan_mode) - TEMPLATABLE_VALUE(std::string, custom_fan_mode) - TEMPLATABLE_VALUE(ClimatePreset, preset) - TEMPLATABLE_VALUE(std::string, custom_preset) - TEMPLATABLE_VALUE(ClimateSwingMode, swing_mode) +#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_) void play(const Ts &...x) override { auto call = this->climate_->make_call(); - call.set_mode(this->mode_.optional_value(x...)); - call.set_target_temperature(this->target_temperature_.optional_value(x...)); - call.set_target_temperature_low(this->target_temperature_low_.optional_value(x...)); - call.set_target_temperature_high(this->target_temperature_high_.optional_value(x...)); - call.set_target_humidity(this->target_humidity_.optional_value(x...)); - if (away_.has_value()) { - call.set_preset(away_.value(x...) ? CLIMATE_PRESET_AWAY : CLIMATE_PRESET_HOME); - } - call.set_fan_mode(this->fan_mode_.optional_value(x...)); - call.set_fan_mode(this->custom_fan_mode_.optional_value(x...)); - call.set_preset(this->preset_.optional_value(x...)); - call.set_preset(this->custom_preset_.optional_value(x...)); - call.set_swing_mode(this->swing_mode_.optional_value(x...)); + CLIMATE_CONTROL_FIELDS(CLIMATE_FIELD_APPLY_) call.perform(); } protected: Climate *climate_; + CLIMATE_CONTROL_FIELDS(CLIMATE_FIELD_DECL_) + +#undef CLIMATE_FIELD_DECL_ +#undef CLIMATE_FIELD_APPLY_ +#undef CLIMATE_FIELD_SETTER_ }; +#undef CLIMATE_CONTROL_FIELDS class ControlTrigger : public Trigger { public: