diff --git a/esphome/automation.py b/esphome/automation.py index 17966dc782c..afa4ac37875 100644 --- a/esphome/automation.py +++ b/esphome/automation.py @@ -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)) diff --git a/esphome/components/binary_sensor/__init__.py b/esphome/components/binary_sensor/__init__.py index 37cccc01be6..b9ca005d9f6 100644 --- a/esphome/components/binary_sensor/__init__.py +++ b/esphome/components/binary_sensor/__init__.py @@ -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"), diff --git a/esphome/components/button/__init__.py b/esphome/components/button/__init__.py index 12d9ebaba62..b64b549699a 100644 --- a/esphome/components/button/__init__.py +++ b/esphome/components/button/__init__.py @@ -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) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 9f3c1484b0c..bc2d5d25779 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -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) diff --git a/esphome/components/switch/__init__.py b/esphome/components/switch/__init__.py index bbafc54bd1e..bf38763616f 100644 --- a/esphome/components/switch/__init__.py +++ b/esphome/components/switch/__init__.py @@ -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") diff --git a/esphome/components/text_sensor/__init__.py b/esphome/components/text_sensor/__init__.py index 97f394ecf73..6f3807ec40a 100644 --- a/esphome/components/text_sensor/__init__.py +++ b/esphome/components/text_sensor/__init__.py @@ -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") diff --git a/esphome/core/automation.h b/esphome/core/automation.h index ca4a2c8b6b0..0002db5ddf1 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -470,6 +470,7 @@ template class ActionList { template class Automation { public: + Automation() = default; explicit Automation(Trigger *trigger) : trigger_(trigger) { this->trigger_->set_automation_parent(this); } void add_action(Action *action) { this->actions_.add_action(action); } @@ -487,7 +488,7 @@ template class Automation { int num_running() { return this->actions_.num_running(); } protected: - Trigger *trigger_; + Trigger *trigger_{nullptr}; ActionList actions_; };