diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index 954ad7a345..839ca532e6 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -328,17 +328,28 @@ async def build_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. + Trigger arg types are normalized to `const std::remove_cvref_t &` + to match the ApplyFn signature for any T (value, ref, or const-ref). """ paren = await cg.get_variable(config[CONF_ID]) + # Normalize trigger args to `const std::remove_cvref_t &` so the + # apply lambda and any inner field lambdas (generated below via + # `process_lambda`) share one parameter spelling that's well-formed for + # any T. + normalized_args = [ + (cg.RawExpression(f"const std::remove_cvref_t<{cg.safe_exp(t)}> &"), n) + for t, n in args + ] + fwd_args = ", ".join(name for _, name in args) body_lines: list[str] = [] for field in fields: if (value := config.get(field.conf_key)) is None: continue if isinstance(value, Lambda): - inner = await cg.process_lambda(value, args, return_type=field.type_) + inner = await cg.process_lambda( + value, normalized_args, return_type=field.type_ + ) value_expr = f"({inner})({fwd_args})" else: value_expr = str(cg.safe_exp(value)) @@ -346,7 +357,7 @@ async def build_apply_lambda_action( apply_args = [ *prefix_args, - *((t.operator("const").operator("ref"), n) for t, n in args), + *normalized_args, ] apply_lambda = LambdaExpression( ["\n".join(body_lines)], diff --git a/esphome/components/cover/automation.h b/esphome/components/cover/automation.h index e2384c2359..ee7a4f5f76 100644 --- a/esphome/components/cover/automation.h +++ b/esphome/components/cover/automation.h @@ -51,10 +51,17 @@ template class ToggleAction : public Action { // plus one parent pointer, regardless of how many fields the user set. // Trigger args are forwarded to the apply function so user lambdas // (e.g. `position: !lambda "return x;"`) keep working. +// +// Trigger args are normalized to `const std::remove_cvref_t &...` so +// the codegen can emit a matching parameter list for both the apply lambda +// and any inner field lambdas without producing invalid C++ source text +// (e.g. `const T & &` if Ts already carries a reference, or `const const +// T &` if Ts already carries a const). This keeps trigger args no-copy +// regardless of whether the trigger supplies `T`, `T &`, or `const T &`. template class ControlAction : public Action { public: - using ApplyFn = void (*)(CoverCall &, const Ts &...); + using ApplyFn = void (*)(CoverCall &, const std::remove_cvref_t &...); ControlAction(Cover *cover, ApplyFn apply) : cover_(cover), apply_(apply) {} void play(const Ts &...x) override { @@ -70,7 +77,7 @@ template class ControlAction : public Action { template class CoverPublishAction : public Action { public: - using ApplyFn = void (*)(Cover *, const Ts &...); + using ApplyFn = void (*)(Cover *, const std::remove_cvref_t &...); CoverPublishAction(Cover *cover, ApplyFn apply) : cover_(cover), apply_(apply) {} void play(const Ts &...x) override { diff --git a/tests/components/template/common-base.yaml b/tests/components/template/common-base.yaml index 984ef129ad..b97cafd25c 100644 --- a/tests/components/template/common-base.yaml +++ b/tests/components/template/common-base.yaml @@ -369,6 +369,19 @@ number: - valve.control: id: template_valve position: !lambda "return x / 100.0f;" + # Same regression test for cover.control: forces the apply-lambda + # codegen to handle a non-empty trigger Ts (float). + - platform: template + id: template_cover_position_number + optimistic: true + min_value: 0 + max_value: 100 + step: 1 + on_value: + then: + - cover.control: + id: template_cover_with_triggers + position: !lambda "return x / 100.0f;" select: - platform: template