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
+188
View File
@@ -0,0 +1,188 @@
"""Tests covering error paths in the controller."""
from __future__ import annotations
import logging
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from pytest_homeassistant_custom_component.common import MockConfigEntry
from custom_components.main_gate_controller.const import (
DOMAIN,
GATE_STATE_CLOSED,
NOTIFY_DOMAIN,
SERVICE_SEND_MESSAGE,
)
from tests.conftest import ServiceCalls, make_entry_data
def _make_failing_handler(fail_on_domain: str, fail_on_service: str, message: str):
async def _handler(call) -> None: # type: ignore[no-untyped-def]
if call.domain == fail_on_domain and call.service == fail_on_service:
raise HomeAssistantError(message)
# Recording handled elsewhere – this helper is only for failure simulation.
return _handler
async def test_notification_failure_continues_cycle(
hass: HomeAssistant, service_calls: ServiceCalls
):
"""If a notify.send_message call fails, the cycle still finishes normally."""
switch = "switch.main_gate_relay"
hass.states.async_set(switch, "off")
hass.states.async_set("notify.mobile_app_phone", "unknown")
entry = MockConfigEntry(
domain=DOMAIN,
title="Main Gate",
data=make_entry_data(
switch_entity_id=switch,
name="Main Gate",
open_duration=1,
pulse_duration=0.1,
notify_enabled=True,
notify_entities=["notify.mobile_app_phone"],
),
unique_id=switch,
)
entry.add_to_hass(hass)
# Replace the recording notify handler with one that fails.
hass.services.async_remove(NOTIFY_DOMAIN, SERVICE_SEND_MESSAGE)
hass.services.async_register(
NOTIFY_DOMAIN,
SERVICE_SEND_MESSAGE,
_make_failing_handler(
NOTIFY_DOMAIN, SERVICE_SEND_MESSAGE, "deliberate failure"
),
)
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()
assert controller.status == GATE_STATE_CLOSED
assert controller.is_running is False
async def test_notification_log_warning_emitted(
hass: HomeAssistant, service_calls: ServiceCalls, caplog
):
"""The notify failure path emits a warning via the module logger."""
switch = "switch.main_gate_relay"
hass.states.async_set(switch, "off")
hass.states.async_set("notify.mobile_app_phone", "unknown")
entry = MockConfigEntry(
domain=DOMAIN,
title="Main Gate",
data=make_entry_data(
switch_entity_id=switch,
name="Main Gate",
open_duration=0.5,
pulse_duration=0.1,
notify_enabled=True,
notify_entities=["notify.mobile_app_phone"],
),
unique_id=switch,
)
entry.add_to_hass(hass)
hass.services.async_remove(NOTIFY_DOMAIN, SERVICE_SEND_MESSAGE)
async def _failing_notify(call) -> None:
raise HomeAssistantError("boom")
hass.services.async_register(NOTIFY_DOMAIN, SERVICE_SEND_MESSAGE, _failing_notify)
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
controller = entry.runtime_data
with caplog.at_level(logging.WARNING):
controller.async_trigger()
await controller.async_wait_until_idle()
assert any(
"notification" in r.message.lower() and "boom" in r.message
for r in caplog.records
)
async def test_switch_failure_aborts_cycle(
hass: HomeAssistant, service_calls: ServiceCalls, caplog
):
"""A switch.turn_on failure aborts the cycle and resets state to closed."""
switch = "switch.main_gate_relay"
hass.states.async_set(switch, "off")
entry = MockConfigEntry(
domain=DOMAIN,
title="Main Gate",
data=make_entry_data(
switch_entity_id=switch,
name="Main Gate",
open_duration=20,
pulse_duration=0.1,
notify_enabled=False,
notify_entities=[],
),
unique_id=switch,
)
entry.add_to_hass(hass)
# Replace the recording handler for switch.turn_off with a failing one.
hass.services.async_remove("switch", "turn_off")
async def _failing_turn_off(call) -> None:
raise HomeAssistantError("relay offline")
hass.services.async_register("switch", "turn_off", _failing_turn_off)
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
controller = entry.runtime_data
with caplog.at_level(logging.ERROR):
controller.async_trigger()
await controller.async_wait_until_idle()
assert controller.status == GATE_STATE_CLOSED
assert controller.is_running is False
assert any(
"aborted" in r.message.lower() or "cycle aborted" in r.message.lower()
for r in caplog.records
)
async def test_unload_cancels_running_cycle(
hass: HomeAssistant, service_calls: ServiceCalls
):
"""Unloading an entry while a cycle runs cancels the background task."""
switch = "switch.main_gate_relay"
hass.states.async_set(switch, "off")
entry = MockConfigEntry(
domain=DOMAIN,
title="Main Gate",
data=make_entry_data(
switch_entity_id=switch,
name="Main Gate",
open_duration=3,
pulse_duration=0.2,
notify_enabled=False,
notify_entities=[],
),
unique_id=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
controller.async_trigger()
# Immediately start unloading.
assert await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
assert controller.is_running is False
assert controller.status == GATE_STATE_CLOSED