From 3522eef8ee4fd7f2a466b5e560544574e7a8bca9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 27 Apr 2026 05:02:58 -0500 Subject: [PATCH] [cover] Add integration test for ControlAction/CoverPublishAction --- .../fixtures/cover_control_action.yaml | 110 ++++++++++++++++++ .../integration/test_cover_control_action.py | 84 +++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 tests/integration/fixtures/cover_control_action.yaml create mode 100644 tests/integration/test_cover_control_action.py diff --git a/tests/integration/fixtures/cover_control_action.yaml b/tests/integration/fixtures/cover_control_action.yaml new file mode 100644 index 0000000000..da67e98f5a --- /dev/null +++ b/tests/integration/fixtures/cover_control_action.yaml @@ -0,0 +1,110 @@ +esphome: + name: cover-control-action-test +host: +api: +logger: + level: DEBUG + +globals: + - id: test_position + type: float + initial_value: "0.42" + +cover: + - platform: template + name: "Test Cover" + id: test_cover + has_position: true + optimistic: true + assumed_state: true + open_action: + - cover.template.publish: + id: test_cover + position: 1.0 + close_action: + - cover.template.publish: + id: test_cover + position: 0.0 + stop_action: + - cover.template.publish: + id: test_cover + current_operation: IDLE + tilt_action: + - lambda: |- + // Manually set tilt and publish + id(test_cover).tilt = tilt; + id(test_cover).publish_state(); + +button: + # Test 1: cover.control with position only (mask 0b010 = 2) + - platform: template + id: btn_position + name: "Set Position" + on_press: + - cover.control: + id: test_cover + position: 50% + + # Test 2: cover.control with tilt only (mask 0b100 = 4) + - platform: template + id: btn_tilt + name: "Set Tilt" + on_press: + - cover.control: + id: test_cover + tilt: 75% + + # Test 3: cover.control with position + tilt (mask 0b110 = 6) + - platform: template + id: btn_pos_tilt + name: "Set Pos Tilt" + on_press: + - cover.control: + id: test_cover + position: 25% + tilt: 30% + + # Test 4: cover.control with stop only (mask 0b001 = 1) + - platform: template + id: btn_stop + name: "Stop Cover" + on_press: + - cover.control: + id: test_cover + stop: true + + # Test 5: cover.control with state alias (sets position bit via CONF_STATE) + - platform: template + id: btn_open_state + name: "Open State" + on_press: + - cover.control: + id: test_cover + state: OPEN + + # Test 6: cover.control with lambda position (exercises lambda path) + - platform: template + id: btn_lambda_position + name: "Lambda Position" + on_press: + - cover.control: + id: test_cover + position: !lambda "return id(test_position);" + + # Test 7: cover.template.publish position only (mask 0b001) + - platform: template + id: btn_publish_pos + name: "Publish Pos" + on_press: + - cover.template.publish: + id: test_cover + position: 0.6 + + # Test 8: cover.template.publish current_operation only (mask 0b100) + - platform: template + id: btn_publish_op + name: "Publish Op" + on_press: + - cover.template.publish: + id: test_cover + current_operation: OPENING diff --git a/tests/integration/test_cover_control_action.py b/tests/integration/test_cover_control_action.py new file mode 100644 index 0000000000..fee89555fa --- /dev/null +++ b/tests/integration/test_cover_control_action.py @@ -0,0 +1,84 @@ +"""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. +""" + +import asyncio +from typing import Any + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_cover_control_action( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test cover ControlAction/CoverPublishAction with constants and lambdas.""" + async with run_compiled(yaml_config), api_client_connected() as client: + state_futures: dict[int, asyncio.Future[Any]] = {} + + def on_state(state: Any) -> None: + if state.key in state_futures and not state_futures[state.key].done(): + state_futures[state.key].set_result(state) + + client.subscribe_states(on_state) + + entities = await client.list_entities_services() + cover = next(e for e in entities[0] if e.object_id == "test_cover") + buttons = {e.name: e for e in entities[0] if hasattr(e, "name")} + + async def wait_for_state(key: int, timeout: float = 5.0) -> Any: + loop = asyncio.get_running_loop() + state_futures[key] = loop.create_future() + try: + return await asyncio.wait_for(state_futures[key], timeout) + finally: + state_futures.pop(key, None) + + async def press_and_wait(button_name: str) -> Any: + btn = buttons[button_name] + client.button_command(btn.key) + return await wait_for_state(cover.key) + + # Test 1: position only (mask 2) + state = await press_and_wait("Set Position") + assert state.position == pytest.approx(0.5, abs=0.01) + + # Test 2: tilt only (mask 4) + state = await press_and_wait("Set Tilt") + assert state.tilt == pytest.approx(0.75, abs=0.01) + + # Test 3: position + tilt (mask 6) + 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: stop (mask 1) — stop on a non-moving cover should not change state + # We just verify the action runs without error; no state change expected. + btn = buttons["Stop Cover"] + client.button_command(btn.key) + # Give the action a moment to execute + await asyncio.sleep(0.1) + + # Test 5: state: OPEN (CONF_STATE alias for position 1.0) + state = await press_and_wait("Open State") + assert state.position == pytest.approx(1.0, abs=0.01) + + # Test 6: lambda position + state = await press_and_wait("Lambda Position") + assert state.position == pytest.approx(0.42, abs=0.01) + + # Test 7: cover.template.publish position only + state = await press_and_wait("Publish Pos") + assert state.position == pytest.approx(0.6, abs=0.01) + + # Test 8: cover.template.publish current_operation only + state = await press_and_wait("Publish Op") + # Operation 1 = OPENING + assert state.current_operation == 1