Initial commit: Main Gate Controller HACS integration
Tests / pytest + ruff (3.12) (push) Has been cancelled
Validate / HACS validation (push) Has been cancelled
Validate / Hassfest validation (push) Has been cancelled

This commit is contained in:
bot
2026-09-07 23:19:12 +02:00
commit 988a30cbf7
27 changed files with 2456 additions and 0 deletions
+169
View File
@@ -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