[light] Use register_apply_action for the light control actions (#19476)

This commit is contained in:
J. Nick Koston
2026-09-24 10:28:36 +12:00
committed by GitHub
parent b7b1507daf
commit 438307f548
6 changed files with 55 additions and 116 deletions
-28
View File
@@ -31,34 +31,6 @@ template<bool HasTransitionLength, typename... Ts> class ToggleAction final : pu
transition_length_{};
};
// 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.
//
// Trigger args are normalized to `const std::remove_cvref_t<Ts> &...` so
// the codegen can emit a matching parameter list for both the apply lambda
// and any inner field lambdas without producing invalid C++ source text
// (e.g. `const T & &` if Ts already carries a reference, or `const const
// T &` if Ts already carries a const). This keeps trigger args no-copy
// regardless of whether the trigger supplies `T`, `T &`, or `const T &`.
template<typename... Ts> class LightControlAction final : public Action<Ts...> {
public:
using ApplyFn = void (*)(LightState *, LightCall &, const std::remove_cvref_t<Ts> &...);
LightControlAction(LightState *parent, ApplyFn apply) : parent_(parent), apply_(apply) {}
void play(const Ts &...x) override {
auto call = this->parent_->make_call();
this->apply_(this->parent_, call, x...);
call.perform();
}
protected:
LightState *parent_;
ApplyFn apply_;
};
template<bool HasTransitionLength, typename... Ts> class DimRelativeAction final : public Action<Ts...> {
public:
explicit DimRelativeAction(LightState *parent) : parent_(parent) {}
+42 -83
View File
@@ -27,7 +27,7 @@ from esphome.const import (
CONF_WHITE,
)
from esphome.core import CORE, ID, EsphomeError, Lambda
from esphome.cpp_generator import LambdaExpression, MockObj, TemplateArgsType
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.types import ConfigType
from .types import (
@@ -37,8 +37,6 @@ from .types import (
AddressableSet,
ColorMode,
DimRelativeAction,
LightCall,
LightControlAction,
LightEffectCycleAction,
LightIsOffCondition,
LightIsOnCondition,
@@ -147,7 +145,7 @@ LIGHT_TURN_ON_ACTION_SCHEMA = automation.maybe_simple_id(
)
def _resolve_effect_index(config: ConfigType) -> int:
def _resolve_effect_index(config: ConfigType, original_name: str) -> int:
"""Resolve a static effect name to its 1-based index at codegen time.
Effect index 0 means "None" (no effect). Effects are 1-indexed matching
@@ -155,7 +153,6 @@ def _resolve_effect_index(config: ConfigType) -> int:
"""
from . import available_effects_str, find_effect_index
original_name = config[CONF_EFFECT]
if original_name.lower() == "none":
return 0
light_id = config[CONF_ID]
@@ -173,87 +170,49 @@ def _resolve_effect_index(config: ConfigType) -> int:
)
@automation.register_action(
"light.turn_off", LightControlAction, LIGHT_TURN_OFF_ACTION_SCHEMA, synchronous=True
def _effect_index(config: ConfigType, value: str) -> str:
# Resolved at codegen time; the cast picks set_effect(uint32_t) over the optional overload.
return f"static_cast<uint32_t>({_resolve_effect_index(config, value)})"
_LIGHT_CONTROL_FIELDS = (
automation.ApplyField(CONF_COLOR_MODE, "set_color_mode", ColorMode),
automation.ApplyField(CONF_STATE, "set_state", cg.bool_),
automation.ApplyField(CONF_TRANSITION_LENGTH, "set_transition_length", cg.uint32),
automation.ApplyField(CONF_FLASH_LENGTH, "set_flash_length", cg.uint32),
automation.ApplyField(CONF_BRIGHTNESS, "set_brightness", cg.float_),
automation.ApplyField(CONF_COLOR_BRIGHTNESS, "set_color_brightness", cg.float_),
automation.ApplyField(CONF_RED, "set_red", cg.float_),
automation.ApplyField(CONF_GREEN, "set_green", cg.float_),
automation.ApplyField(CONF_BLUE, "set_blue", cg.float_),
automation.ApplyField(CONF_WHITE, "set_white", cg.float_),
automation.ApplyField(CONF_COLOR_TEMPERATURE, "set_color_temperature", cg.float_),
automation.ApplyField(CONF_COLD_WHITE, "set_cold_white", cg.float_),
automation.ApplyField(CONF_WARM_WHITE, "set_warm_white", cg.float_),
automation.ApplyField(
CONF_EFFECT, "set_effect", cg.std_string, const_fn=_effect_index
),
)
@automation.register_action(
"light.turn_on", LightControlAction, LIGHT_TURN_ON_ACTION_SCHEMA, synchronous=True
automation.register_apply_action(
"light.turn_off",
LIGHT_TURN_OFF_ACTION_SCHEMA,
automation.ApplyField(CONF_STATE, "set_state", cg.bool_),
automation.ApplyField(CONF_TRANSITION_LENGTH, "set_transition_length", cg.uint32),
call="make_call",
)
@automation.register_action(
"light.control", LightControlAction, LIGHT_CONTROL_ACTION_SCHEMA, synchronous=True
automation.register_apply_action(
"light.turn_on",
LIGHT_TURN_ON_ACTION_SCHEMA,
*_LIGHT_CONTROL_FIELDS,
call="make_call",
)
automation.register_apply_action(
"light.control",
LIGHT_CONTROL_ACTION_SCHEMA,
*_LIGHT_CONTROL_FIELDS,
call="make_call",
)
async def light_control_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
# 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_),
(CONF_TRANSITION_LENGTH, "set_transition_length", cg.uint32),
(CONF_FLASH_LENGTH, "set_flash_length", cg.uint32),
(CONF_BRIGHTNESS, "set_brightness", cg.float_),
(CONF_COLOR_BRIGHTNESS, "set_color_brightness", cg.float_),
(CONF_RED, "set_red", cg.float_),
(CONF_GREEN, "set_green", cg.float_),
(CONF_BLUE, "set_blue", cg.float_),
(CONF_WHITE, "set_white", cg.float_),
(CONF_COLOR_TEMPERATURE, "set_color_temperature", cg.float_),
(CONF_COLD_WHITE, "set_cold_white", cg.float_),
(CONF_WARM_WHITE, "set_warm_white", cg.float_),
)
# Normalize trigger args to `const std::remove_cvref_t<T> &` so the
# apply lambda and any inner field lambdas (generated below via
# `process_lambda`) share one parameter spelling that's well-formed for
# any T (value, ref, or const-ref). Matches LightControlAction::ApplyFn.
normalized_args = [
(cg.RawExpression(f"const std::remove_cvref_t<{cg.safe_exp(t)}> &"), n)
for t, n in args
]
fwd_args = ", ".join(name for _, name in args)
body_lines: list[str] = []
for conf_key, setter, type_ in FIELDS:
if conf_key not in config:
continue
value = config[conf_key]
if isinstance(value, Lambda):
inner = await cg.process_lambda(value, normalized_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):
inner_lambda = await cg.process_lambda(
config[CONF_EFFECT], normalized_args, return_type=cg.std_string
)
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())); }}"
)
else:
# 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)}));"
)
apply_args = [
(LightState.operator("ptr"), "parent"),
(LightCall.operator("ref"), "call"),
*normalized_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)
def _record_effect_cycle_ref(config: ConfigType) -> ConfigType:
-2
View File
@@ -14,7 +14,6 @@ Color = cg.esphome_ns.class_("Color")
LightColorValues = light_ns.class_("LightColorValues")
LightStateRTCState = light_ns.struct("LightStateRTCState")
LightCall = light_ns.class_("LightCall")
# Addressable strips
ChannelColors = light_ns.struct("ChannelColors")
@@ -42,7 +41,6 @@ LIMIT_MODES = {
# Actions
ToggleAction = light_ns.class_("ToggleAction", automation.Action)
LightControlAction = light_ns.class_("LightControlAction", automation.Action)
LightEffectCycleAction = light_ns.class_("LightEffectCycleAction", automation.Action)
DimRelativeAction = light_ns.class_("DimRelativeAction", automation.Action)
AddressableSet = light_ns.class_("AddressableSet", automation.Action)
@@ -50,7 +50,7 @@ esphome:
format: "After delay, body still: %s"
args:
- body.c_str()
# Regression test for esphome/esphome#16224: a LightControlAction
# Regression test for esphome/esphome#16224: a light.turn_on action
# nested inside on_response with capture_response: true puts
# `std::string &` into the trigger's Ts..., which exposed a codegen
# bug where the apply lambda's parameter list did not match the
+10
View File
@@ -77,6 +77,16 @@ esphome:
- light.control:
id: test_monochromatic_light
state: on
# flash_length and a turn_off with its own transition
- light.turn_on:
id: test_rgb_light
flash_length: 750ms
- light.turn_on:
id: test_rgb_light
flash_length: !lambda return 250;
- light.turn_off:
id: test_rgb_light
transition_length: 300ms
# Test static effect name resolution at codegen time
- light.turn_on:
id: test_monochromatic_light
@@ -1,4 +1,4 @@
"""Integration test for LightControlAction.
"""Integration test for the light control apply actions.
Tests that light.turn_on, light.turn_off, and light.control automation actions
work correctly with the compact per-field union storage. Exercises both constant
@@ -19,7 +19,7 @@ async def test_light_control_action(
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test LightControlAction with constants and lambdas."""
"""Test light.turn_on, light.turn_off and light.control with constants and lambdas."""
async with run_compiled(yaml_config), api_client_connected() as client:
state_futures: dict[int, asyncio.Future[Any]] = {}