diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index 9d5b6ad49c..5a27ceb8fa 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -323,7 +323,11 @@ async def cover_control_to_code(config, action_id, template_arg, args): else: body_lines.append(f"call.{setter}({cg.safe_exp(value)});") - apply_args = [(CoverCall.operator("ref"), "call"), *args] + # Match ControlAction::ApplyFn signature: const Ts &... for trigger args. + apply_args = [ + (CoverCall.operator("ref"), "call"), + *((t.operator("const").operator("ref"), n) for t, n in args), + ] apply_lambda = LambdaExpression( ["\n".join(body_lines)], apply_args, diff --git a/esphome/components/cover/automation.h b/esphome/components/cover/automation.h index 42ca892142..e2384c2359 100644 --- a/esphome/components/cover/automation.h +++ b/esphome/components/cover/automation.h @@ -47,14 +47,14 @@ template class ToggleAction : public Action { }; // All configured fields are baked into a single stateless lambda whose -// constants live in flash. Each action stores only a function pointer -// (4 bytes) plus the parent (4 bytes), 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. +// constants live in flash. Each action stores only one function pointer +// 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. template class ControlAction : public Action { public: - using ApplyFn = void (*)(CoverCall &, Ts...); + using ApplyFn = void (*)(CoverCall &, const Ts &...); ControlAction(Cover *cover, ApplyFn apply) : cover_(cover), apply_(apply) {} void play(const Ts &...x) override { @@ -70,7 +70,7 @@ template class ControlAction : public Action { template class CoverPublishAction : public Action { public: - using ApplyFn = void (*)(Cover *, Ts...); + using ApplyFn = void (*)(Cover *, const Ts &...); CoverPublishAction(Cover *cover, ApplyFn apply) : cover_(cover), apply_(apply) {} void play(const Ts &...x) override { diff --git a/esphome/components/template/cover/__init__.py b/esphome/components/template/cover/__init__.py index 1af003d75d..bcf0c54c7f 100644 --- a/esphome/components/template/cover/__init__.py +++ b/esphome/components/template/cover/__init__.py @@ -155,7 +155,11 @@ async def cover_template_publish_to_code(config, action_id, template_arg, args): else: body_lines.append(f"cover->{field} = {cg.safe_exp(value)};") - apply_args = [(cover.Cover.operator("ptr"), "cover"), *args] + # 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, diff --git a/tests/integration/fixtures/cover_control_action.yaml b/tests/integration/fixtures/cover_control_action.yaml index f9eaa2ceee..085d632796 100644 --- a/tests/integration/fixtures/cover_control_action.yaml +++ b/tests/integration/fixtures/cover_control_action.yaml @@ -36,7 +36,7 @@ cover: id(test_cover).publish_state(); button: - # Test 1: cover.control with position only (mask 0b010 = 2) + # cover.control: position only - platform: template id: btn_position name: "Set Position" @@ -45,7 +45,7 @@ button: id: test_cover position: 50% - # Test 2: cover.control with tilt only (mask 0b100 = 4) + # cover.control: tilt only - platform: template id: btn_tilt name: "Set Tilt" @@ -54,7 +54,7 @@ button: id: test_cover tilt: 75% - # Test 3: cover.control with position + tilt (mask 0b110 = 6) + # cover.control: position + tilt - platform: template id: btn_pos_tilt name: "Set Pos Tilt" @@ -64,7 +64,7 @@ button: position: 25% tilt: 30% - # Test 4: cover.control with state alias (sets position bit via CONF_STATE) + # cover.control: state alias for position - platform: template id: btn_open_state name: "Open State" @@ -73,7 +73,7 @@ button: id: test_cover state: OPEN - # Test 5: cover.control with lambda position (exercises lambda path) + # cover.control: lambda position (exercises lambda path) - platform: template id: btn_lambda_position name: "Lambda Position" @@ -82,7 +82,7 @@ button: id: test_cover position: !lambda "return id(test_position);" - # Test 6: cover.template.publish position only (mask 0b001) + # cover.template.publish: position only - platform: template id: btn_publish_pos name: "Publish Pos" @@ -91,7 +91,7 @@ button: id: test_cover position: 0.6 - # Test 7: cover.template.publish current_operation only (mask 0b100) + # cover.template.publish: current_operation only - platform: template id: btn_publish_op name: "Publish Op" @@ -100,8 +100,8 @@ button: id: test_cover current_operation: OPENING - # Test 8: cover.control with stop only (mask 0b001 = 1) — runs after - # Publish Op so we can verify current_operation transitions OPENING -> IDLE + # cover.control: stop only — runs after Publish Op so the test can + # verify current_operation transitions OPENING -> IDLE. - platform: template id: btn_stop name: "Stop Cover" diff --git a/tests/integration/test_cover_control_action.py b/tests/integration/test_cover_control_action.py index 29ece37ce5..9c7395371b 100644 --- a/tests/integration/test_cover_control_action.py +++ b/tests/integration/test_cover_control_action.py @@ -1,8 +1,8 @@ """Integration test for cover ControlAction and CoverPublishAction. Tests that cover.control and cover.template.publish automation actions -work correctly with the per-instance bitmask field storage. Exercises -multiple field combinations to cover the bitmask variants. +work correctly with the single stateless apply lambda/function pointer +implementation. Exercises multiple field combinations and the lambda path. """ from __future__ import annotations @@ -55,38 +55,38 @@ async def test_cover_control_action( client.button_command(btn.key) return await wait_for_cover_state() - # Test 1: position only (mask 2) + # cover.control: position only state = await press_and_wait("Set Position") assert state.position == pytest.approx(0.5, abs=0.01) - # Test 2: tilt only (mask 4) + # cover.control: tilt only state = await press_and_wait("Set Tilt") assert state.tilt == pytest.approx(0.75, abs=0.01) - # Test 3: position + tilt (mask 6) + # cover.control: position + tilt state = await press_and_wait("Set Pos Tilt") assert state.position == pytest.approx(0.25, abs=0.01) assert state.tilt == pytest.approx(0.30, abs=0.01) - # Test 4: state: OPEN (CONF_STATE alias for position 1.0) + # cover.control: state alias for position 1.0 state = await press_and_wait("Open State") assert state.position == pytest.approx(1.0, abs=0.01) - # Test 5: lambda position (test_position global = 0.42) + # cover.control: lambda position (test_position global = 0.42) state = await press_and_wait("Lambda Position") assert state.position == pytest.approx(0.42, abs=0.01) - # Test 6: cover.template.publish position only + # cover.template.publish: position only state = await press_and_wait("Publish Pos") assert state.position == pytest.approx(0.6, abs=0.01) - # Test 7: cover.template.publish current_operation only + # cover.template.publish: current_operation only state = await press_and_wait("Publish Op") # CoverOperation.OPENING == 1 assert state.current_operation == 1 - # Test 8: cover.control stop only (mask 1) - # The template cover's stop_action publishes current_operation: IDLE + # cover.control: stop only — template cover's stop_action publishes + # current_operation: IDLE. state = await press_and_wait("Stop Cover") # CoverOperation.IDLE == 0 assert state.current_operation == 0