[core] Eliminate trigger trampolines for common entity automations

Add build_callback_automation() to register automation callbacks
directly on parent components, bypassing the Trigger wrapper object.

Migrates button, sensor, binary_sensor, switch, and text_sensor
to the new pattern, eliminating 12 thin wrapper trigger classes
from runtime instantiation.
This commit is contained in:
J. Nick Koston
2026-03-25 13:58:30 -10:00
parent a075f63b59
commit 6354d4ad97
7 changed files with 103 additions and 26 deletions
+45
View File
@@ -661,3 +661,48 @@ async def build_automation(
actions = await build_action_list(config[CONF_THEN], templ, args)
cg.add(obj.add_actions(actions))
return obj
async def build_callback_automation(
parent: MockObj,
callback_method: str,
args: TemplateArgsType,
config: ConfigType,
callback_args: TemplateArgsType | None = None,
condition: str | None = None,
) -> None:
"""Build an Automation and register it as a callback on the parent.
Eliminates the need for a Trigger wrapper object by registering the
automation's trigger() directly as a callback on the parent component.
:param parent: The component object (e.g., button, sensor).
:param callback_method: Name of the callback method (e.g., "add_on_press_callback").
:param args: Automation template args as list of (type, name) tuples.
:param config: The automation config dict.
:param callback_args: Lambda parameter types if different from args (e.g., for
conditional triggers where the callback receives (bool state) but the
automation is Automation<> with no args). Defaults to args.
:param condition: Optional C++ condition. Use callback arg names directly
(e.g., "state", "!state").
"""
arg_types = [arg[0] for arg in args]
templ = cg.TemplateArguments(*arg_types)
obj = cg.new_Pvariable(config[CONF_AUTOMATION_ID], templ)
actions = await build_action_list(config[CONF_THEN], templ, args)
cg.add(obj.add_actions(actions))
# Build trigger call expression: automation->trigger(arg1, arg2, ...)
trigger_args = [MockObj(arg[1], "") for arg in args]
trigger_expr = obj.trigger(*trigger_args)
if condition is not None:
body = [f"if ({condition}) {{ ", trigger_expr, "; }"]
else:
body = [trigger_expr, ";"]
# Use callback_args for the lambda parameters if provided (e.g., when the
# callback signature differs from the automation args due to filtering).
lambda_params = callback_args if callback_args is not None else args
# ESPHome codegen allocates all variables as static pointers, so they
# are accessible without explicit lambda capture. Using "" avoids
# -Wcapture-of-non-automatic-storage-duration warnings.
lambda_expr = LambdaExpression(body, lambda_params, capture="")
cg.add(getattr(parent, callback_method)(lambda_expr))
+22 -9
View File
@@ -557,12 +557,24 @@ def binary_sensor_schema(
@coroutine_with_priority(CoroPriority.AUTOMATION)
async def _build_binary_sensor_automations(var, config):
for conf in config.get(CONF_ON_PRESS, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
await automation.build_callback_automation(
var,
"add_on_state_callback",
[],
conf,
callback_args=[(bool, "state")],
condition="state",
)
for conf in config.get(CONF_ON_RELEASE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
await automation.build_callback_automation(
var,
"add_on_state_callback",
[],
conf,
callback_args=[(bool, "state")],
condition="!state",
)
for conf in config.get(CONF_ON_CLICK, []):
trigger = cg.new_Pvariable(
@@ -593,13 +605,14 @@ async def _build_binary_sensor_automations(var, config):
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_STATE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(bool, "x")], conf)
await automation.build_callback_automation(
var, "add_on_state_callback", [(bool, "x")], conf
)
for conf in config.get(CONF_ON_STATE_CHANGE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger,
await automation.build_callback_automation(
var,
"add_full_state_callback",
[
(cg.optional.template(bool), "x_previous"),
(cg.optional.template(bool), "x"),
+3 -2
View File
@@ -91,8 +91,9 @@ def button_schema(
@setup_entity("button")
async def setup_button_core_(var, config):
for conf in config.get(CONF_ON_PRESS, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
await automation.build_callback_automation(
var, "add_on_press_callback", [], conf
)
setup_device_class(config)
+6 -4
View File
@@ -898,11 +898,13 @@ async def build_filters(config):
@coroutine_with_priority(CoroPriority.AUTOMATION)
async def _build_sensor_automations(var, config):
for conf in config.get(CONF_ON_VALUE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(float, "x")], conf)
await automation.build_callback_automation(
var, "add_on_state_callback", [(float, "x")], conf
)
for conf in config.get(CONF_ON_RAW_VALUE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(float, "x")], conf)
await automation.build_callback_automation(
var, "add_on_raw_state_callback", [(float, "x")], conf
)
for conf in config.get(CONF_ON_VALUE_RANGE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await cg.register_component(trigger, conf)
+19 -6
View File
@@ -148,14 +148,27 @@ def switch_schema(
@coroutine_with_priority(CoroPriority.AUTOMATION)
async def _build_switch_automations(var, config):
for conf in config.get(CONF_ON_STATE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(bool, "x")], conf)
await automation.build_callback_automation(
var, "add_on_state_callback", [(bool, "x")], conf
)
for conf in config.get(CONF_ON_TURN_ON, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
await automation.build_callback_automation(
var,
"add_on_state_callback",
[],
conf,
callback_args=[(bool, "state")],
condition="state",
)
for conf in config.get(CONF_ON_TURN_OFF, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [], conf)
await automation.build_callback_automation(
var,
"add_on_state_callback",
[],
conf,
callback_args=[(bool, "state")],
condition="!state",
)
@setup_entity("switch")
+6 -4
View File
@@ -204,12 +204,14 @@ async def build_filters(config):
@coroutine_with_priority(CoroPriority.AUTOMATION)
async def _build_text_sensor_automations(var, config):
for conf in config.get(CONF_ON_VALUE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.std_string, "x")], conf)
await automation.build_callback_automation(
var, "add_on_state_callback", [(cg.std_string, "x")], conf
)
for conf in config.get(CONF_ON_RAW_VALUE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(cg.std_string, "x")], conf)
await automation.build_callback_automation(
var, "add_on_raw_state_callback", [(cg.std_string, "x")], conf
)
@setup_entity("text_sensor")
+2 -1
View File
@@ -470,6 +470,7 @@ template<typename... Ts> class ActionList {
template<typename... Ts> class Automation {
public:
Automation() = default;
explicit Automation(Trigger<Ts...> *trigger) : trigger_(trigger) { this->trigger_->set_automation_parent(this); }
void add_action(Action<Ts...> *action) { this->actions_.add_action(action); }
@@ -487,7 +488,7 @@ template<typename... Ts> class Automation {
int num_running() { return this->actions_.num_running(); }
protected:
Trigger<Ts...> *trigger_;
Trigger<Ts...> *trigger_{nullptr};
ActionList<Ts...> actions_;
};