Initial commit: Main Gate Controller HACS integration
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user