From 964cfaa73099120f1f4522599cf68a36a476a394 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Apr 2026 07:06:55 -0500 Subject: [PATCH] [cover] Extract build_apply_lambda_action helper, add typing, use kwargs --- esphome/components/cover/__init__.py | 73 +++++++++++++------ esphome/components/template/cover/__init__.py | 69 ++++++++---------- 2 files changed, 81 insertions(+), 61 deletions(-) diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index 5a27ceb8fa..e6a3c29c1d 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -295,37 +295,38 @@ COVER_CONTROL_ACTION_SCHEMA = cv.Schema( ) -@automation.register_action( - "cover.control", ControlAction, COVER_CONTROL_ACTION_SCHEMA, synchronous=True -) -async def cover_control_to_code(config, action_id, template_arg, args): +async def build_apply_lambda_action( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, + fields: tuple[tuple[str, str, object], ...], + prefix_args: list[tuple[object, str]], + statement_fn, +) -> MockObj: + """Fold configured fields into a single stateless apply lambda action. + + Used by both `cover.control` and `cover.template.publish` (and shared + with the template/cover platform). Constants are emitted as flash + immediates; user lambdas are invoked inline so trigger args still flow. + The trigger arg types are wrapped as `const T &` to match the + `void (*)(..., const Ts &...)` ApplyFn signature. + """ 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. - # CONF_STATE and CONF_POSITION are cv.Exclusive in the schema, so at most - # one is present and both dispatch to set_position. - FIELDS = ( - (CONF_STOP, "set_stop", cg.bool_), - (CONF_STATE, "set_position", cg.float_), - (CONF_POSITION, "set_position", cg.float_), - (CONF_TILT, "set_tilt", cg.float_), - ) - fwd_args = ", ".join(name for _, name in args) body_lines: list[str] = [] - for conf_key, setter, type_ in FIELDS: + for conf_key, target, type_ in fields: if (value := config.get(conf_key)) is None: continue if isinstance(value, Lambda): inner = await cg.process_lambda(value, args, return_type=type_) - body_lines.append(f"call.{setter}(({inner})({fwd_args}));") + value_expr = f"({inner})({fwd_args})" else: - body_lines.append(f"call.{setter}({cg.safe_exp(value)});") + value_expr = str(cg.safe_exp(value)) + body_lines.append(statement_fn(target, value_expr)) - # Match ControlAction::ApplyFn signature: const Ts &... for trigger args. apply_args = [ - (CoverCall.operator("ref"), "call"), + *prefix_args, *((t.operator("const").operator("ref"), n) for t, n in args), ] apply_lambda = LambdaExpression( @@ -337,6 +338,36 @@ async def cover_control_to_code(config, action_id, template_arg, args): return cg.new_Pvariable(action_id, template_arg, paren, apply_lambda) +# CONF_STATE and CONF_POSITION are cv.Exclusive in the schema, so at most +# one is present and both dispatch to set_position. +_COVER_CONTROL_FIELDS = ( + (CONF_STOP, "set_stop", cg.bool_), + (CONF_STATE, "set_position", cg.float_), + (CONF_POSITION, "set_position", cg.float_), + (CONF_TILT, "set_tilt", cg.float_), +) + + +@automation.register_action( + "cover.control", ControlAction, COVER_CONTROL_ACTION_SCHEMA, synchronous=True +) +async def cover_control_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: + return await build_apply_lambda_action( + config=config, + action_id=action_id, + template_arg=template_arg, + args=args, + fields=_COVER_CONTROL_FIELDS, + prefix_args=[(CoverCall.operator("ref"), "call")], + statement_fn=lambda setter, expr: f"call.{setter}({expr});", + ) + + COVER_CONDITION_SCHEMA = cv.maybe_simple_value( {cv.Required(CONF_ID): cv.use_id(Cover)}, key=CONF_ID ) diff --git a/esphome/components/template/cover/__init__.py b/esphome/components/template/cover/__init__.py index bcf0c54c7f..3cf8779847 100644 --- a/esphome/components/template/cover/__init__.py +++ b/esphome/components/template/cover/__init__.py @@ -19,8 +19,9 @@ from esphome.const import ( CONF_TILT_ACTION, CONF_TILT_LAMBDA, ) -from esphome.core import Lambda -from esphome.cpp_generator import LambdaExpression +from esphome.core import ID +from esphome.cpp_generator import MockObj +from esphome.types import ConfigType, TemplateArgsType from .. import template_ns @@ -112,6 +113,16 @@ async def to_code(config): cg.add(var.set_restore_mode(config[CONF_RESTORE_MODE])) +# CONF_STATE and CONF_POSITION are cv.Exclusive in the schema, so at most +# one is present and both map to the position field. +_COVER_PUBLISH_FIELDS = ( + (CONF_STATE, "position", cg.float_), + (CONF_POSITION, "position", cg.float_), + (CONF_TILT, "tilt", cg.float_), + (CONF_CURRENT_OPERATION, "current_operation", cover.CoverOperation), +) + + @automation.register_action( "cover.template.publish", cover.CoverPublishAction, @@ -128,42 +139,20 @@ async def to_code(config): ), synchronous=True, ) -async def cover_template_publish_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. - # The lambda mutates Cover fields directly (no CoverCall) since publish - # is a state push, not a control request. - # CONF_STATE and CONF_POSITION are cv.Exclusive in the schema, so at most - # one is present and both map to the position field. - FIELDS = ( - (CONF_STATE, "position", cg.float_), - (CONF_POSITION, "position", cg.float_), - (CONF_TILT, "tilt", cg.float_), - (CONF_CURRENT_OPERATION, "current_operation", cover.CoverOperation), +async def cover_template_publish_to_code( + config: ConfigType, + action_id: ID, + template_arg: cg.TemplateArguments, + args: TemplateArgsType, +) -> MockObj: + # Mutates Cover fields directly (no CoverCall) since publish is a state + # push, not a control request. + return await cover.build_apply_lambda_action( + config=config, + action_id=action_id, + template_arg=template_arg, + args=args, + fields=_COVER_PUBLISH_FIELDS, + prefix_args=[(cover.Cover.operator("ptr"), "cover")], + statement_fn=lambda field, expr: f"cover->{field} = {expr};", ) - - fwd_args = ", ".join(name for _, name in args) - body_lines: list[str] = [] - for conf_key, field, type_ in FIELDS: - if (value := config.get(conf_key)) is None: - continue - if isinstance(value, Lambda): - inner = await cg.process_lambda(value, args, return_type=type_) - body_lines.append(f"cover->{field} = ({inner})({fwd_args});") - else: - body_lines.append(f"cover->{field} = {cg.safe_exp(value)};") - - # Match CoverPublishAction::ApplyFn: const Ts &... for trigger args. - apply_args = [ - (cover.Cover.operator("ptr"), "cover"), - *((t.operator("const").operator("ref"), n) for t, n in 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)