mirror of
https://github.com/esphome/esphome.git
synced 2026-09-14 08:38:39 +00:00
Accept custom forwarder types, migrate number and lock
Replace bool_filter with generic forwarder parameter that accepts any struct type. Components can define their own forwarders with custom fields (e.g., LockStateForwarder needs both automation and lock entity pointers). Migrates number (NumberStateTrigger) and lock (LockStateTrigger) to prove the API is flexible enough for diverse patterns.
This commit is contained in:
+22
-19
@@ -666,33 +666,33 @@ async def build_automation(
|
||||
return obj
|
||||
|
||||
|
||||
TRIGGER_ON_TRUE = "on_true"
|
||||
TRIGGER_ON_FALSE = "on_false"
|
||||
|
||||
|
||||
async def build_callback_automation(
|
||||
parent: MockObj,
|
||||
callback_method: str,
|
||||
args: TemplateArgsType,
|
||||
config: ConfigType,
|
||||
bool_filter: str | None = None,
|
||||
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 (TriggerForwarder, TriggerOnTrueForwarder,
|
||||
TriggerOnFalseForwarder) so the compiler deduplicates the operator() body
|
||||
across all call sites with the same signature.
|
||||
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 bool_filter: Optional bool filter. Use TRIGGER_ON_TRUE to trigger only
|
||||
when the bool callback arg is true, TRIGGER_ON_FALSE for false.
|
||||
The automation will be Automation<> (no args) while the callback receives bool.
|
||||
: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)
|
||||
@@ -702,15 +702,18 @@ async def build_callback_automation(
|
||||
# Use template forwarder structs for deduplication. The compiler generates
|
||||
# one operator() per forwarder type; different automation pointers are just
|
||||
# data in the struct.
|
||||
if bool_filter == TRIGGER_ON_TRUE:
|
||||
forwarder = cg.RawExpression(f"{TriggerOnTrueForwarder}{{{obj}}}")
|
||||
elif bool_filter == TRIGGER_ON_FALSE:
|
||||
forwarder = cg.RawExpression(f"{TriggerOnFalseForwarder}{{{obj}}}")
|
||||
else:
|
||||
forwarder_type = (
|
||||
if forwarder is None:
|
||||
forwarder = (
|
||||
TriggerForwarder.template(templ)
|
||||
if arg_types
|
||||
else TriggerForwarder.template()
|
||||
)
|
||||
forwarder = cg.RawExpression(f"{forwarder_type}{{{obj}}}")
|
||||
cg.add(getattr(parent, callback_method)(forwarder))
|
||||
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}}}")
|
||||
)
|
||||
)
|
||||
|
||||
@@ -562,7 +562,7 @@ async def _build_binary_sensor_automations(var, config):
|
||||
"add_on_state_callback",
|
||||
[],
|
||||
conf,
|
||||
bool_filter=automation.TRIGGER_ON_TRUE,
|
||||
forwarder=automation.TriggerOnTrueForwarder,
|
||||
)
|
||||
|
||||
for conf in config.get(CONF_ON_RELEASE, []):
|
||||
@@ -571,7 +571,7 @@ async def _build_binary_sensor_automations(var, config):
|
||||
"add_on_state_callback",
|
||||
[],
|
||||
conf,
|
||||
bool_filter=automation.TRIGGER_ON_FALSE,
|
||||
forwarder=automation.TriggerOnFalseForwarder,
|
||||
)
|
||||
|
||||
for conf in config.get(CONF_ON_CLICK, []):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -157,7 +157,7 @@ async def _build_switch_automations(var, config):
|
||||
"add_on_state_callback",
|
||||
[],
|
||||
conf,
|
||||
bool_filter=automation.TRIGGER_ON_TRUE,
|
||||
forwarder=automation.TriggerOnTrueForwarder,
|
||||
)
|
||||
for conf in config.get(CONF_ON_TURN_OFF, []):
|
||||
await automation.build_callback_automation(
|
||||
@@ -165,7 +165,7 @@ async def _build_switch_automations(var, config):
|
||||
"add_on_state_callback",
|
||||
[],
|
||||
conf,
|
||||
bool_filter=automation.TRIGGER_ON_FALSE,
|
||||
forwarder=automation.TriggerOnFalseForwarder,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -6,8 +6,6 @@ from unittest.mock import patch
|
||||
import pytest
|
||||
|
||||
from esphome.automation import (
|
||||
TRIGGER_ON_FALSE,
|
||||
TRIGGER_ON_TRUE,
|
||||
TriggerForwarder,
|
||||
TriggerOnFalseForwarder,
|
||||
TriggerOnTrueForwarder,
|
||||
@@ -188,7 +186,8 @@ def test_has_non_synchronous_actions_dict_input(
|
||||
def _build_forwarder(
|
||||
automation_name: str,
|
||||
args: list[tuple[str, str]],
|
||||
bool_filter: str | None = None,
|
||||
forwarder: MockObj | None = None,
|
||||
extra_args: list[str] | None = None,
|
||||
) -> str:
|
||||
"""Build a trigger forwarder expression the same way build_callback_automation does.
|
||||
|
||||
@@ -197,14 +196,16 @@ def _build_forwarder(
|
||||
import esphome.codegen as cg
|
||||
|
||||
obj = MockObj(automation_name, "->")
|
||||
if bool_filter == TRIGGER_ON_TRUE:
|
||||
return f"{TriggerOnTrueForwarder}{{{obj}}}"
|
||||
if bool_filter == TRIGGER_ON_FALSE:
|
||||
return f"{TriggerOnFalseForwarder}{{{obj}}}"
|
||||
arg_types = [RawExpression(t) for t, _ in args]
|
||||
templ = cg.TemplateArguments(*arg_types) if arg_types else cg.TemplateArguments()
|
||||
forwarder_type = TriggerForwarder.template(templ)
|
||||
return f"{forwarder_type}{{{obj}}}"
|
||||
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:
|
||||
@@ -227,13 +228,13 @@ def test_trigger_forwarder_single_bool_arg() -> None:
|
||||
|
||||
def test_trigger_forwarder_on_true() -> None:
|
||||
"""Binary_sensor on_press / switch on_turn_on: TriggerOnTrueForwarder."""
|
||||
result = _build_forwarder("auto_1", [], bool_filter=TRIGGER_ON_TRUE)
|
||||
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", [], bool_filter=TRIGGER_ON_FALSE)
|
||||
result = _build_forwarder("auto_1", [], forwarder=TriggerOnFalseForwarder)
|
||||
assert result == "TriggerOnFalseForwarder{auto_1}"
|
||||
|
||||
|
||||
@@ -250,3 +251,12 @@ 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