170 lines
5.8 KiB
Python
170 lines
5.8 KiB
Python
"""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
|