Merge remote-tracking branch 'origin/light-action-fold-fields-into-lambda' into integration

This commit is contained in:
J. Nick Koston
2026-04-29 07:41:12 -05:00
3 changed files with 46 additions and 76 deletions
+10 -44
View File
@@ -31,60 +31,26 @@ template<bool HasTransitionLength, typename... Ts> class ToggleAction : public A
transition_length_{};
};
// Unique Empty<Tag> per field so [[no_unique_address]] is guaranteed to coalesce.
namespace light_control_detail {
template<int Tag> struct Empty {};
} // namespace light_control_detail
// X-macro: (type, field_name, bit_index). Order and bit values must match
// the FIELDS table in automation.py.
#define LIGHT_CONTROL_FIELDS(X) \
X(ColorMode, color_mode, 0) \
X(bool, state, 1) \
X(uint32_t, transition_length, 2) \
X(uint32_t, flash_length, 3) \
X(float, brightness, 4) \
X(float, color_brightness, 5) \
X(float, red, 6) \
X(float, green, 7) \
X(float, blue, 8) \
X(float, white, 9) \
X(float, color_temperature, 10) \
X(float, cold_white, 11) \
X(float, warm_white, 12) \
X(uint32_t, effect, 13)
template<uint16_t Fields, typename... Ts> class LightControlAction : public Action<Ts...> {
// All configured fields are baked into a single stateless lambda whose
// constants live in flash. The action only stores one function pointer
// plus one parent pointer, regardless of how many fields the user set.
// Trigger args are forwarded to the apply function so user lambdas
// (e.g. `brightness: !lambda "return x;"`) keep working.
template<typename... Ts> class LightControlAction : public Action<Ts...> {
public:
explicit LightControlAction(LightState *parent) : parent_(parent) {}
#define LIGHT_FIELD_SETTER_(type, name, idx) \
template<typename V> void set_##name(V value) requires((Fields & (1 << (idx))) != 0) { this->name##_ = value; }
#define LIGHT_FIELD_APPLY_(type, name, idx) \
if constexpr ((Fields & (1 << (idx))) != 0) \
call.set_##name(this->name##_.value(x...));
#define LIGHT_FIELD_DECL_(type, name, idx) \
[[no_unique_address]] std::conditional_t<(Fields & (1 << (idx))) != 0, TemplatableFn<type, Ts...>, \
light_control_detail::Empty<(idx)>> \
name##_{};
LIGHT_CONTROL_FIELDS(LIGHT_FIELD_SETTER_)
using ApplyFn = void (*)(LightState *, LightCall &, const Ts &...);
LightControlAction(LightState *parent, ApplyFn apply) : parent_(parent), apply_(apply) {}
void play(const Ts &...x) override {
auto call = this->parent_->make_call();
LIGHT_CONTROL_FIELDS(LIGHT_FIELD_APPLY_)
this->apply_(this->parent_, call, x...);
call.perform();
}
protected:
LightState *parent_;
LIGHT_CONTROL_FIELDS(LIGHT_FIELD_DECL_)
#undef LIGHT_FIELD_DECL_
#undef LIGHT_FIELD_APPLY_
#undef LIGHT_FIELD_SETTER_
ApplyFn apply_;
};
#undef LIGHT_CONTROL_FIELDS
template<bool HasTransitionLength, typename... Ts> class DimRelativeAction : public Action<Ts...> {
public:
+35 -32
View File
@@ -37,6 +37,7 @@ from .types import (
AddressableSet,
ColorMode,
DimRelativeAction,
LightCall,
LightControlAction,
LightIsOffCondition,
LightIsOnCondition,
@@ -181,8 +182,8 @@ def _resolve_effect_index(config: ConfigType) -> int:
async def light_control_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
# Order/bits must match LIGHT_CONTROL_FIELDS in automation.h.
# EFFECT has special handling below; setter=None skips the generic loop.
# All configured fields are folded into a single stateless lambda whose
# constants live in flash; the action stores only a function pointer.
FIELDS = (
(CONF_COLOR_MODE, "set_color_mode", ColorMode),
(CONF_STATE, "set_state", cg.bool_),
@@ -197,49 +198,51 @@ async def light_control_to_code(config, action_id, template_arg, args):
(CONF_COLOR_TEMPERATURE, "set_color_temperature", cg.float_),
(CONF_COLD_WHITE, "set_cold_white", cg.float_),
(CONF_WARM_WHITE, "set_warm_white", cg.float_),
(CONF_EFFECT, None, cg.uint32),
)
# Bitmask is passed as uint16_t in C++ — must stay within 16 bits.
assert len(FIELDS) <= 16, "LightControlAction 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 conf_key in config and setter is not None:
template_ = await cg.templatable(config[conf_key], args, type_)
cg.add(getattr(var, setter)(template_))
if conf_key not in config:
continue
value = config[conf_key]
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)});")
if CONF_EFFECT in config:
if isinstance(config[CONF_EFFECT], Lambda):
# Lambda returns a string — wrap in a C++ lambda that resolves
# the effect name to its uint32_t index at runtime
inner_lambda = await cg.process_lambda(
config[CONF_EFFECT], args, return_type=cg.std_string
)
fwd_args = ", ".join(n for _, n in args)
# capture="" is correct: paren is a global variable name
# string-interpolated into the body at codegen time, not a
# C++ runtime capture.
wrapper = LambdaExpression(
f"auto __effect_s = ({inner_lambda})({fwd_args});\n"
f"return {paren}->get_effect_index("
f"__effect_s.c_str(), __effect_s.size());",
args,
capture="",
return_type=cg.uint32,
body_lines.append(
f"{{ auto __effect_s = ({inner_lambda})({fwd_args});\n"
f"call.set_effect(parent->get_effect_index("
f"__effect_s.c_str(), __effect_s.size())); }}"
)
cg.add(var.set_effect(wrapper))
else:
# Static string — resolve effect name to index at codegen time
template_ = await cg.templatable(
_resolve_effect_index(config), args, cg.uint32
# Cast disambiguates between set_effect(uint32_t) and
# set_effect(optional<uint32_t>) when the literal is an int.
body_lines.append(
f"call.set_effect(static_cast<uint32_t>({_resolve_effect_index(config)}));"
)
cg.add(var.set_effect(template_))
return var
# Match LightControlAction::ApplyFn signature: const Ts &... for trigger args.
apply_args = [
(LightState.operator("ptr"), "parent"),
(LightCall.operator("ref"), "call"),
*((t.operator("const").operator("ref"), n) for t, n in 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)
CONF_RELATIVE_BRIGHTNESS = "relative_brightness"
+1
View File
@@ -13,6 +13,7 @@ Color = cg.esphome_ns.class_("Color")
LightColorValues = light_ns.class_("LightColorValues")
LightStateRTCState = light_ns.struct("LightStateRTCState")
LightCall = light_ns.class_("LightCall")
# Color modes
ColorMode = light_ns.enum("ColorMode", is_class=True)