mirror of
https://github.com/esphome/esphome.git
synced 2026-09-26 14:30:23 +00:00
Merge remote-tracking branch 'upstream/eliminate-trigger-trampolines' into integration
This commit is contained in:
@@ -137,6 +137,9 @@ UpdateComponentAction = cg.esphome_ns.class_("UpdateComponentAction", Action)
|
||||
SuspendComponentAction = cg.esphome_ns.class_("SuspendComponentAction", Action)
|
||||
ResumeComponentAction = cg.esphome_ns.class_("ResumeComponentAction", Action)
|
||||
Automation = cg.esphome_ns.class_("Automation")
|
||||
TriggerForwarder = cg.esphome_ns.class_("TriggerForwarder")
|
||||
TriggerOnTrueForwarder = cg.esphome_ns.class_("TriggerOnTrueForwarder")
|
||||
TriggerOnFalseForwarder = cg.esphome_ns.class_("TriggerOnFalseForwarder")
|
||||
|
||||
LambdaCondition = cg.esphome_ns.class_("LambdaCondition", Condition)
|
||||
StatelessLambdaCondition = cg.esphome_ns.class_("StatelessLambdaCondition", Condition)
|
||||
@@ -661,3 +664,56 @@ 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,
|
||||
forwarder: MockObjClass | None = None,
|
||||
forwarder_extra_args: list | 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.
|
||||
|
||||
Uses template forwarder structs so the compiler deduplicates the operator()
|
||||
body across all call sites with the same signature.
|
||||
|
||||
: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 forwarder: Optional forwarder type to use instead of the default
|
||||
TriggerForwarder<Ts...>. Pass any struct type whose aggregate init takes
|
||||
an Automation pointer as the first field (e.g., TriggerOnTrueForwarder,
|
||||
or a custom component-defined forwarder).
|
||||
:param forwarder_extra_args: Optional list of extra MockObj args to pass to the
|
||||
forwarder after the automation pointer in aggregate init. For example,
|
||||
a lock forwarder needs the lock entity pointer: [lock_var].
|
||||
"""
|
||||
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))
|
||||
# Use template forwarder structs for deduplication. The compiler generates
|
||||
# one operator() per forwarder type; different automation pointers are just
|
||||
# data in the struct.
|
||||
if forwarder is None:
|
||||
forwarder = (
|
||||
TriggerForwarder.template(templ)
|
||||
if arg_types
|
||||
else TriggerForwarder.template()
|
||||
)
|
||||
init_args = str(obj)
|
||||
if forwarder_extra_args:
|
||||
extra = ", ".join(str(a) for a in forwarder_extra_args)
|
||||
init_args = f"{init_args}, {extra}"
|
||||
cg.add(
|
||||
getattr(parent, callback_method)(
|
||||
cg.RawExpression(f"{forwarder}{{{init_args}}}")
|
||||
)
|
||||
)
|
||||
|
||||
@@ -557,12 +557,22 @@ 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,
|
||||
forwarder=automation.TriggerOnTrueForwarder,
|
||||
)
|
||||
|
||||
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,
|
||||
forwarder=automation.TriggerOnFalseForwarder,
|
||||
)
|
||||
|
||||
for conf in config.get(CONF_ON_CLICK, []):
|
||||
trigger = cg.new_Pvariable(
|
||||
@@ -593,13 +603,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"),
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ LockLockTrigger = lock_ns.class_("LockLockTrigger", automation.Trigger.template(
|
||||
LockUnlockTrigger = lock_ns.class_("LockUnlockTrigger", automation.Trigger.template())
|
||||
|
||||
LockState = lock_ns.enum("LockState")
|
||||
LockStateForwarder = lock_ns.class_("LockStateForwarder")
|
||||
|
||||
LOCK_STATES = {
|
||||
"LOCKED": LockState.LOCK_STATE_LOCKED,
|
||||
@@ -94,11 +95,23 @@ def lock_schema(
|
||||
@setup_entity("lock")
|
||||
async def _setup_lock_core(var, config):
|
||||
for conf in config.get(CONF_ON_LOCK, []):
|
||||
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,
|
||||
forwarder=LockStateForwarder.template(LockState.LOCK_STATE_LOCKED),
|
||||
forwarder_extra_args=[var],
|
||||
)
|
||||
for conf in config.get(CONF_ON_UNLOCK, []):
|
||||
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,
|
||||
forwarder=LockStateForwarder.template(LockState.LOCK_STATE_UNLOCKED),
|
||||
forwarder_extra_args=[var],
|
||||
)
|
||||
|
||||
if mqtt_id := config.get(CONF_MQTT_ID):
|
||||
mqtt_ = cg.new_Pvariable(mqtt_id, var)
|
||||
|
||||
@@ -66,4 +66,14 @@ template<LockState State> class LockStateTrigger : public Trigger<> {
|
||||
using LockLockTrigger = LockStateTrigger<LockState::LOCK_STATE_LOCKED>;
|
||||
using LockUnlockTrigger = LockStateTrigger<LockState::LOCK_STATE_UNLOCKED>;
|
||||
|
||||
/// Forwarder that triggers an Automation<> when a Lock reaches a specific state.
|
||||
template<LockState State> struct LockStateForwarder {
|
||||
Automation<> *automation;
|
||||
Lock *lock;
|
||||
void operator()() const {
|
||||
if (this->lock->state == State)
|
||||
this->automation->trigger();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::lock
|
||||
|
||||
@@ -248,8 +248,9 @@ def number_schema(
|
||||
@coroutine_with_priority(CoroPriority.AUTOMATION)
|
||||
async def _build_number_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_VALUE_RANGE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await cg.register_component(trigger, conf)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -148,14 +148,25 @@ 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,
|
||||
forwarder=automation.TriggerOnTrueForwarder,
|
||||
)
|
||||
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,
|
||||
forwarder=automation.TriggerOnFalseForwarder,
|
||||
)
|
||||
|
||||
|
||||
@setup_entity("switch")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -470,7 +470,8 @@ template<typename... Ts> class ActionList {
|
||||
|
||||
template<typename... Ts> class Automation {
|
||||
public:
|
||||
explicit Automation(Trigger<Ts...> *trigger) : trigger_(trigger) { this->trigger_->set_automation_parent(this); }
|
||||
Automation() = default;
|
||||
explicit Automation(Trigger<Ts...> *trigger) { trigger->set_automation_parent(this); }
|
||||
|
||||
void add_action(Action<Ts...> *action) { this->actions_.add_action(action); }
|
||||
void add_actions(const std::initializer_list<Action<Ts...> *> &actions) { this->actions_.add_actions(actions); }
|
||||
@@ -487,8 +488,32 @@ template<typename... Ts> class Automation {
|
||||
int num_running() { return this->actions_.num_running(); }
|
||||
|
||||
protected:
|
||||
Trigger<Ts...> *trigger_;
|
||||
ActionList<Ts...> actions_;
|
||||
};
|
||||
|
||||
/// Callback forwarder that triggers an Automation directly.
|
||||
/// One operator() instantiation per Automation<Ts...> signature, shared across all call sites.
|
||||
template<typename... Ts> struct TriggerForwarder {
|
||||
Automation<Ts...> *automation;
|
||||
void operator()(Ts... args) const { this->automation->trigger(args...); }
|
||||
};
|
||||
|
||||
/// Callback forwarder that triggers an Automation<> only when the bool arg is true.
|
||||
struct TriggerOnTrueForwarder {
|
||||
Automation<> *automation;
|
||||
void operator()(bool state) const {
|
||||
if (state)
|
||||
this->automation->trigger();
|
||||
}
|
||||
};
|
||||
|
||||
/// Callback forwarder that triggers an Automation<> only when the bool arg is false.
|
||||
struct TriggerOnFalseForwarder {
|
||||
Automation<> *automation;
|
||||
void operator()(bool state) const {
|
||||
if (!state)
|
||||
this->automation->trigger();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@@ -5,7 +5,13 @@ from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.automation import has_non_synchronous_actions
|
||||
from esphome.automation import (
|
||||
TriggerForwarder,
|
||||
TriggerOnFalseForwarder,
|
||||
TriggerOnTrueForwarder,
|
||||
has_non_synchronous_actions,
|
||||
)
|
||||
from esphome.cpp_generator import MockObj, RawExpression
|
||||
from esphome.util import RegistryEntry
|
||||
|
||||
|
||||
@@ -175,3 +181,82 @@ def test_has_non_synchronous_actions_dict_input(
|
||||
"""Direct dict input (single action)."""
|
||||
assert has_non_synchronous_actions({"delay": "1s"}) is True
|
||||
assert has_non_synchronous_actions({"logger.log": "hello"}) is False
|
||||
|
||||
|
||||
def _build_forwarder(
|
||||
automation_name: str,
|
||||
args: list[tuple[str, str]],
|
||||
forwarder: MockObj | None = None,
|
||||
extra_args: list[str] | None = None,
|
||||
) -> str:
|
||||
"""Build a trigger forwarder expression the same way build_callback_automation does.
|
||||
|
||||
Mirrors the forwarder selection logic in automation.build_callback_automation.
|
||||
"""
|
||||
import esphome.codegen as cg
|
||||
|
||||
obj = MockObj(automation_name, "->")
|
||||
if forwarder is None:
|
||||
arg_types = [RawExpression(t) for t, _ in args]
|
||||
templ = (
|
||||
cg.TemplateArguments(*arg_types) if arg_types else cg.TemplateArguments()
|
||||
)
|
||||
forwarder = TriggerForwarder.template(templ)
|
||||
init_args = str(obj)
|
||||
if extra_args:
|
||||
init_args += ", " + ", ".join(extra_args)
|
||||
return f"{forwarder}{{{init_args}}}"
|
||||
|
||||
|
||||
def test_trigger_forwarder_no_args() -> None:
|
||||
"""Button on_press: TriggerForwarder<> with no args."""
|
||||
result = _build_forwarder("auto_1", [])
|
||||
assert result == "TriggerForwarder<>{auto_1}"
|
||||
|
||||
|
||||
def test_trigger_forwarder_single_float_arg() -> None:
|
||||
"""Sensor on_value: TriggerForwarder<float>."""
|
||||
result = _build_forwarder("auto_1", [("float", "x")])
|
||||
assert result == "TriggerForwarder<float>{auto_1}"
|
||||
|
||||
|
||||
def test_trigger_forwarder_single_bool_arg() -> None:
|
||||
"""Switch on_state: TriggerForwarder<bool>."""
|
||||
result = _build_forwarder("auto_1", [("bool", "x")])
|
||||
assert result == "TriggerForwarder<bool>{auto_1}"
|
||||
|
||||
|
||||
def test_trigger_forwarder_on_true() -> None:
|
||||
"""Binary_sensor on_press / switch on_turn_on: TriggerOnTrueForwarder."""
|
||||
result = _build_forwarder("auto_1", [], forwarder=TriggerOnTrueForwarder)
|
||||
assert result == "TriggerOnTrueForwarder{auto_1}"
|
||||
|
||||
|
||||
def test_trigger_forwarder_on_false() -> None:
|
||||
"""Binary_sensor on_release / switch on_turn_off: TriggerOnFalseForwarder."""
|
||||
result = _build_forwarder("auto_1", [], forwarder=TriggerOnFalseForwarder)
|
||||
assert result == "TriggerOnFalseForwarder{auto_1}"
|
||||
|
||||
|
||||
def test_trigger_forwarder_multiple_args() -> None:
|
||||
"""Binary_sensor on_state_change: TriggerForwarder with two args."""
|
||||
result = _build_forwarder(
|
||||
"auto_1",
|
||||
[("optional<bool>", "x_previous"), ("optional<bool>", "x")],
|
||||
)
|
||||
assert result == "TriggerForwarder<optional<bool>, optional<bool>>{auto_1}"
|
||||
|
||||
|
||||
def test_trigger_forwarder_string_arg() -> None:
|
||||
"""Text_sensor on_value: TriggerForwarder<std::string>."""
|
||||
result = _build_forwarder("auto_1", [("std::string", "x")])
|
||||
assert result == "TriggerForwarder<std::string>{auto_1}"
|
||||
|
||||
|
||||
def test_trigger_forwarder_custom_with_extra_args() -> None:
|
||||
"""Lock on_lock: custom forwarder with extra args for entity pointer."""
|
||||
lock_forwarder = MockObj("LockStateForwarder<LOCK_STATE_LOCKED>", "")
|
||||
result = _build_forwarder(
|
||||
"auto_1", [], forwarder=lock_forwarder, extra_args=["lock_var"]
|
||||
)
|
||||
assert result == "LockStateForwarder<LOCK_STATE_LOCKED>{auto_1, lock_var}"
|
||||
|
||||
Reference in New Issue
Block a user