mirror of
https://github.com/esphome/esphome.git
synced 2026-09-28 07:20:24 +00:00
[light] Use register_apply_action for the light control actions (#19476)
This commit is contained in:
@@ -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) {}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]] = {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user