Files
core/tests/components/sofar/test_tuning.py
T

190 lines
6.4 KiB
Python

"""Tests for the Sofar link tuning."""
from datetime import timedelta
from freezegun.api import FrozenDateTimeFactory
from modbus_connection import (
ModbusConnectionError,
ModbusTimeoutError,
ServerDeviceBusyError,
)
from modbus_connection.mock import MockModbusConnection
import pytest
from sofar_modbus.model import UpdateReport
from homeassistant.components.sofar.const import SCAN_INTERVAL
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry, async_fire_time_changed
from tests.components.diagnostics import get_diagnostics_for_config_entry
from tests.typing import ClientSessionGenerator
GRID_REGISTER = 0x0484
CLEAN_POLLS = 5
"""Polls the tuner wants before it acts on what it measured."""
EARNED_TIMEOUT = 0.5
"""The shortest ask the tuner makes, which a mocked link always earns."""
async def _poll(
hass: HomeAssistant, freezer: FrozenDateTimeFactory, count: int
) -> None:
"""Run the readings coordinator ``count`` times."""
for _ in range(count):
freezer.tick(timedelta(seconds=SCAN_INTERVAL))
async_fire_time_changed(hass)
await hass.async_block_till_done()
@pytest.mark.usefixtures("init_integration")
async def test_tuner_lowers_the_link_timeout(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_connection: MockModbusConnection,
) -> None:
"""Test a link that answers cleanly is asked for a shorter timeout."""
unit = mock_connection.for_unit(1)
assert unit.required_timeout is None
await _poll(hass, freezer, CLEAN_POLLS)
assert unit.required_timeout == EARNED_TIMEOUT
@pytest.mark.usefixtures("init_integration")
async def test_tuner_withdraws_the_ask_after_a_timeout(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_connection: MockModbusConnection,
) -> None:
"""Test a timed-out poll hands the link its own timeout back."""
unit = mock_connection.for_unit(1)
await _poll(hass, freezer, CLEAN_POLLS)
assert unit.required_timeout == EARNED_TIMEOUT
unit.fail_read(GRID_REGISTER, ModbusTimeoutError("stuck"))
await _poll(hass, freezer, 1)
assert unit.required_timeout is None
@pytest.mark.usefixtures("init_integration")
async def test_tuner_withdraws_the_ask_when_the_poll_raises(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_connection: MockModbusConnection,
) -> None:
"""Test a poll that times out before anything answers is still heard."""
unit = mock_connection.for_unit(1)
await _poll(hass, freezer, CLEAN_POLLS)
assert unit.required_timeout == EARNED_TIMEOUT
unit.fail_requests(ModbusTimeoutError("link gone slow"))
await _poll(hass, freezer, 1)
assert unit.required_timeout is None
async def test_tuner_hears_a_timeout_only_the_retry_hit(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_connection: MockModbusConnection,
init_integration: MockConfigEntry,
) -> None:
"""Test a component that times out only on the retry still withdraws it."""
unit = mock_connection.for_unit(1)
await _poll(hass, freezer, CLEAN_POLLS)
assert unit.required_timeout == EARNED_TIMEOUT
async def busy_grid() -> UpdateReport:
"""A first attempt that failed without timing out."""
return UpdateReport({"state"}, {"grid": ServerDeviceBusyError("busy")})
init_integration.runtime_data.readings._poll = busy_grid
unit.fail_read(GRID_REGISTER, ModbusTimeoutError("stuck on retry"))
await _poll(hass, freezer, 1)
assert unit.required_timeout is None
async def test_tuner_hears_a_timeout_the_retry_recovered(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_connection: MockModbusConnection,
init_integration: MockConfigEntry,
) -> None:
"""Test a timeout that answered on the second attempt still withdraws it."""
unit = mock_connection.for_unit(1)
await _poll(hass, freezer, CLEAN_POLLS)
assert unit.required_timeout == EARNED_TIMEOUT
async def timed_out_grid() -> UpdateReport:
"""A first attempt that timed out; the retry finds the unit healthy."""
return UpdateReport({"state"}, {"grid": ModbusTimeoutError("slow")})
init_integration.runtime_data.readings._poll = timed_out_grid
await _poll(hass, freezer, 1)
assert unit.required_timeout is None
async def test_tuner_hears_a_timeout_the_retry_reported_otherwise(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_connection: MockModbusConnection,
init_integration: MockConfigEntry,
) -> None:
"""Test a retry's own error does not bury the timeout that preceded it."""
unit = mock_connection.for_unit(1)
await _poll(hass, freezer, CLEAN_POLLS)
assert unit.required_timeout == EARNED_TIMEOUT
async def timed_out_grid() -> UpdateReport:
"""A first attempt that timed out."""
return UpdateReport({"state"}, {"grid": ModbusTimeoutError("slow")})
init_integration.runtime_data.readings._poll = timed_out_grid
unit.fail_read(GRID_REGISTER, ServerDeviceBusyError("busy on retry"))
await _poll(hass, freezer, 1)
assert unit.required_timeout is None
async def test_tuner_hears_a_timeout_when_the_retry_loses_the_link(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_connection: MockModbusConnection,
init_integration: MockConfigEntry,
) -> None:
"""Test a link dying during the retry does not bury the earlier timeout."""
unit = mock_connection.for_unit(1)
await _poll(hass, freezer, CLEAN_POLLS)
assert unit.required_timeout == EARNED_TIMEOUT
async def timed_out_grid() -> UpdateReport:
"""A first attempt that timed out."""
return UpdateReport({"state"}, {"grid": ModbusTimeoutError("slow")})
init_integration.runtime_data.readings._poll = timed_out_grid
unit.fail_read(GRID_REGISTER, ModbusConnectionError("link gone"))
await _poll(hass, freezer, 1)
assert unit.required_timeout is None
async def test_diagnostics_report_what_the_tuner_asked(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
freezer: FrozenDateTimeFactory,
mock_connection: MockModbusConnection,
init_integration: MockConfigEntry,
) -> None:
"""Test the tuning a link settled on reaches the diagnostics dump."""
await _poll(hass, freezer, CLEAN_POLLS)
diag = await get_diagnostics_for_config_entry(hass, hass_client, init_integration)
assert diag["link"]["tuning"]["timeout"] == EARNED_TIMEOUT