"""Tests for the open/countdown/close sequence driven by the button.""" from __future__ import annotations from typing import Any from homeassistant.const import SERVICE_TURN_OFF, SERVICE_TURN_ON from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from pytest_homeassistant_custom_component.common import MockConfigEntry from custom_components.main_gate_controller.const import ( DOMAIN, GATE_STATE_CLOSED, GATE_STATE_CLOSING, GATE_STATE_OPEN, GATE_STATE_OPENING, NOTIFY_DOMAIN, SERVICE_SEND_MESSAGE, SWITCH_DOMAIN, ) from tests.conftest import ServiceCalls, make_entry_data async def _press_button(hass: HomeAssistant, entry) -> None: registry = er.async_get(hass) entity_id = registry.async_get_entity_id( "button", DOMAIN, f"{entry.entry_id}_open" ) assert entity_id is not None await hass.services.async_call( "button", "press", {"entity_id": entity_id}, blocking=True ) async def test_full_open_close_sequence_with_notifications( service_calls: ServiceCalls, loaded_entry ): """Notifications-enabled entry runs the canonical sequence. The spec dictates: ``Closing`` notification fires **before** the closing switch pulses (state-machine transition closing -> closed is logged from the coordinator inside the switch sequence). """ controller = loaded_entry.runtime_data assert controller.async_trigger() is True await controller.async_wait_until_idle() expected = [ (NOTIFY_DOMAIN, SERVICE_SEND_MESSAGE), # Opening notification (SWITCH_DOMAIN, SERVICE_TURN_OFF), # OPEN: OFF (SWITCH_DOMAIN, SERVICE_TURN_ON), # OPEN: ON (SWITCH_DOMAIN, SERVICE_TURN_OFF), # OPEN: OFF (NOTIFY_DOMAIN, SERVICE_SEND_MESSAGE), # Closing notification (before close pulse) (SWITCH_DOMAIN, SERVICE_TURN_ON), # CLOSE: ON (SWITCH_DOMAIN, SERVICE_TURN_OFF), # CLOSE: OFF ] assert service_calls.order() == expected notifies = service_calls.service(NOTIFY_DOMAIN, SERVICE_SEND_MESSAGE) assert notifies[0]["message"].startswith("Opening Main Gate") assert notifies[1]["message"] == "Closing Main Gate" targets = notifies[0]["target"] if isinstance(targets, str): targets = [targets] assert targets == ["notify.mobile_app_phone"] assert controller.status == GATE_STATE_CLOSED assert controller.is_running is False async def test_sequence_without_notifications( service_calls: ServiceCalls, hass: HomeAssistant, gate_switch: str ): """Notifications disabled → only switch calls happen.""" entry = MockConfigEntry( domain=DOMAIN, title="Main Gate", data=make_entry_data( switch_entity_id=gate_switch, name="Main Gate", notify_enabled=False ), unique_id=gate_switch, ) entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() controller = entry.runtime_data assert controller.async_trigger() is True await controller.async_wait_until_idle() expected = [ (SWITCH_DOMAIN, SERVICE_TURN_OFF), (SWITCH_DOMAIN, SERVICE_TURN_ON), (SWITCH_DOMAIN, SERVICE_TURN_OFF), (SWITCH_DOMAIN, SERVICE_TURN_ON), (SWITCH_DOMAIN, SERVICE_TURN_OFF), ] assert service_calls.order() == expected assert controller.status == GATE_STATE_CLOSED async def test_mode_single_ignores_second_trigger( service_calls: ServiceCalls, loaded_entry ): """A second trigger during a running cycle does nothing (mode: single).""" controller = loaded_entry.runtime_data first = controller.async_trigger() second = controller.async_trigger() assert first is True assert second is False await controller.async_wait_until_idle() switch_calls = sum( 1 for d, s, _ in service_calls.records if d == SWITCH_DOMAIN ) assert switch_calls == 5 async def test_button_press_starts_cycle( service_calls: ServiceCalls, hass: HomeAssistant, loaded_entry ): """Pressing the button entity starts the cycle via the service call.""" controller = loaded_entry.runtime_data await _press_button(hass, loaded_entry) await controller.async_wait_until_idle() switch_calls = sum( 1 for d, s, _ in service_calls.records if d == SWITCH_DOMAIN ) # 5 switch calls (no notify because loaded_entry is without notify targets). assert switch_calls == 5 async def test_state_transitions_during_cycle( hass: HomeAssistant, service_calls: ServiceCalls, loaded_entry ): """The status sensor goes opening → open → closing → closed.""" # Subscribe to state changes of the status sensor. observed: list[str] = [] def _capture(event): if event.data.get("entity_id", "").endswith("_status"): new_state: dict[str, Any] = event.data.get("new_state") if new_state is not None and new_state.state in { GATE_STATE_CLOSED, GATE_STATE_OPENING, GATE_STATE_OPEN, GATE_STATE_CLOSING, }: if not observed or observed[-1] != new_state.state: observed.append(new_state.state) hass.bus.async_listen("state_changed", _capture) controller = loaded_entry.runtime_data assert controller.async_trigger() is True await controller.async_wait_until_idle() # Let pending state_changed events propagate through the bus. await hass.async_block_till_done() # Opening and closing are short with pulse=1s; ``open`` is the dominant state. seen = set(observed) assert GATE_STATE_OPENING in seen assert GATE_STATE_OPEN in seen assert GATE_STATE_CLOSING in seen assert GATE_STATE_CLOSED in seen