1
0
mirror of https://github.com/home-assistant/core.git synced 2026-07-13 01:27:57 +01:00
Files
core/tests/components/neopool/test_init.py
T
2026-07-07 16:41:27 +02:00

193 lines
7.1 KiB
Python

"""Test the NeoPool integration setup, unload, and lifecycle."""
from datetime import timedelta
from unittest.mock import AsyncMock, MagicMock
from freezegun.api import FrozenDateTimeFactory
from neopool_modbus.registers import MAX_RELAY_GPIO
import pytest
from homeassistant.components.neopool.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, issue_registry as ir
from . import setup_integration
from .conftest import MOCK_POOL_DATA, MOCK_SERIAL
from tests.common import MockConfigEntry, async_fire_time_changed
@pytest.mark.usefixtures("mock_neopool_client")
async def test_setup_and_unload(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> None:
"""Set up the integration end-to-end and tear it down again."""
await setup_integration(hass, mock_config_entry)
assert mock_config_entry.state is ConfigEntryState.LOADED
assert await hass.config_entries.async_unload(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
async def test_setup_first_refresh_fails_marks_retry(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_neopool_client: MagicMock,
) -> None:
"""Setup re-tries when the first Modbus read raises."""
mock_neopool_client.async_read_all = AsyncMock(
side_effect=ConnectionError("Modbus down")
)
await setup_integration(hass, mock_config_entry)
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
@pytest.mark.usefixtures("mock_neopool_client")
async def test_device_registered_with_firmware(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
mock_config_entry: MockConfigEntry,
) -> None:
"""The first successful read populates firmware on the device entry."""
await setup_integration(hass, mock_config_entry)
device = device_registry.async_get_device(identifiers={(DOMAIN, MOCK_SERIAL)})
assert device is not None
assert "18.52" in (device.sw_version or "")
async def test_transient_modbus_failure_after_first_success_marks_unavailable(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_neopool_client: MagicMock,
freezer: FrozenDateTimeFactory,
) -> None:
"""Entities transition to unavailable when polling fails after a good read."""
await setup_integration(hass, mock_config_entry)
entity_id = "sensor.neopool_water_temperature"
assert hass.states.get(entity_id).state != STATE_UNAVAILABLE
mock_neopool_client.async_read_all.side_effect = ConnectionError("Modbus fail")
freezer.tick(timedelta(seconds=60))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
assert mock_config_entry.state is ConfigEntryState.LOADED
async def test_corrupt_gpio_creates_repair_issue(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_neopool_client: MagicMock,
issue_registry: ir.IssueRegistry,
) -> None:
"""A GPIO register outside 0..MAX_RELAY_GPIO opens a corrupted_gpio issue."""
bad_data = dict(MOCK_POOL_DATA)
bad_data["MBF_PAR_FILT_GPIO"] = MAX_RELAY_GPIO + 1
mock_neopool_client.async_read_all = AsyncMock(return_value=bad_data)
await setup_integration(hass, mock_config_entry)
issue = issue_registry.async_get_issue(DOMAIN, "corrupted_gpio")
assert issue is not None
assert issue.severity is ir.IssueSeverity.ERROR
@pytest.mark.usefixtures("mock_neopool_client")
async def test_clean_gpio_does_not_create_issue(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
issue_registry: ir.IssueRegistry,
) -> None:
"""A clean read does not open a corrupted_gpio issue."""
await setup_integration(hass, mock_config_entry)
assert issue_registry.async_get_issue(DOMAIN, "corrupted_gpio") is None
@pytest.mark.usefixtures("mock_neopool_client")
async def test_corrupt_gpio_clears_stale_issue_from_previous_session(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
issue_registry: ir.IssueRegistry,
) -> None:
"""A stale issue from a previous session clears on the first clean poll."""
ir.async_create_issue(
hass,
DOMAIN,
"corrupted_gpio",
is_fixable=False,
severity=ir.IssueSeverity.ERROR,
translation_key="corrupted_gpio",
translation_placeholders={"details": "- stale"},
)
assert issue_registry.async_get_issue(DOMAIN, "corrupted_gpio") is not None
await setup_integration(hass, mock_config_entry)
assert issue_registry.async_get_issue(DOMAIN, "corrupted_gpio") is None
async def test_corrupt_gpio_logs_error_only_on_state_change(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_neopool_client: MagicMock,
issue_registry: ir.IssueRegistry,
freezer: FrozenDateTimeFactory,
caplog: pytest.LogCaptureFixture,
) -> None:
"""The ERROR log fires on entering corruption and clears on healing."""
bad_data = dict(MOCK_POOL_DATA)
bad_data["MBF_PAR_FILT_GPIO"] = MAX_RELAY_GPIO + 1
mock_neopool_client.async_read_all = AsyncMock(return_value=bad_data)
await setup_integration(hass, mock_config_entry)
assert sum("Corrupted GPIO register" in r.message for r in caplog.records) == 1
caplog.clear()
freezer.tick(timedelta(seconds=60))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert not any("Corrupted GPIO register" in r.message for r in caplog.records)
mock_neopool_client.async_read_all = AsyncMock(return_value=dict(MOCK_POOL_DATA))
freezer.tick(timedelta(seconds=60))
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert issue_registry.async_get_issue(DOMAIN, "corrupted_gpio") is None
async def test_corrupt_gpio_updates_issue_on_value_change(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_neopool_client: MagicMock,
issue_registry: ir.IssueRegistry,
freezer: FrozenDateTimeFactory,
) -> None:
"""The repair issue details refresh when a corrupted register value changes."""
first = dict(MOCK_POOL_DATA)
first["MBF_PAR_FILT_GPIO"] = MAX_RELAY_GPIO + 1
mock_neopool_client.async_read_all = AsyncMock(return_value=first)
await setup_integration(hass, mock_config_entry)
issue = issue_registry.async_get_issue(DOMAIN, "corrupted_gpio")
assert issue is not None
assert issue.translation_placeholders is not None
assert str(MAX_RELAY_GPIO + 1) in issue.translation_placeholders["details"]
second = dict(MOCK_POOL_DATA)
second["MBF_PAR_FILT_GPIO"] = MAX_RELAY_GPIO + 2
mock_neopool_client.async_read_all = AsyncMock(return_value=second)
freezer.tick(timedelta(seconds=60))
async_fire_time_changed(hass)
await hass.async_block_till_done()
issue = issue_registry.async_get_issue(DOMAIN, "corrupted_gpio")
assert issue is not None
assert issue.translation_placeholders is not None
assert str(MAX_RELAY_GPIO + 2) in issue.translation_placeholders["details"]