Share Duco coordinator update test helper (#181781)

This commit is contained in:
Ronald van der Meer
2026-09-11 07:47:31 +02:00
committed by GitHub
parent 58b268bade
commit 498244c1f9
6 changed files with 64 additions and 106 deletions
+13 -1
View File
@@ -3,10 +3,22 @@
from collections.abc import Sequence
from unittest.mock import patch
from freezegun.api import FrozenDateTimeFactory
from homeassistant.components.duco.const import SCAN_INTERVAL
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
from tests.common import MockConfigEntry, async_fire_time_changed
async def async_fire_coordinator_update(
hass: HomeAssistant, freezer: FrozenDateTimeFactory
) -> None:
"""Trigger a scheduled coordinator update."""
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
async def setup_integration(
+13 -20
View File
@@ -14,7 +14,7 @@ from freezegun.api import FrozenDateTimeFactory
import pytest
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.duco.const import BOX_NODE_ID, SCAN_INTERVAL
from homeassistant.components.duco.const import BOX_NODE_ID
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import (
STATE_OFF,
@@ -26,9 +26,9 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_platform_integration
from . import async_fire_coordinator_update, setup_platform_integration
from tests.common import MockConfigEntry, async_fire_time_changed
from tests.common import MockConfigEntry
VENTILATION_PROBLEM_ENTITY_ID = "binary_sensor.living_ventilation"
@@ -38,13 +38,6 @@ DIAGNOSTIC_ERROR_TYPES = [
]
async def _async_refresh(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> None:
"""Trigger a coordinator refresh."""
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
async def test_diagnostic_binary_sensor_entity_registry_defaults(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
@@ -144,7 +137,7 @@ async def test_diagnostic_binary_sensors_added_after_initial_empty_response(
diagnostic_subsystems=(DiagComponent(component="Ventilation", status="Error"),)
)
await _async_refresh(hass, freezer)
await async_fire_coordinator_update(hass, freezer)
assert hass.states.is_state(VENTILATION_PROBLEM_ENTITY_ID, STATE_ON)
@@ -155,7 +148,7 @@ async def test_diagnostic_binary_sensors_added_after_initial_empty_response(
)
)
await _async_refresh(hass, freezer)
await async_fire_coordinator_update(hass, freezer)
assert hass.states.is_state("binary_sensor.living_filter", STATE_OFF)
@@ -178,7 +171,7 @@ async def test_diagnostic_binary_sensors_wait_for_box_node(
assert hass.states.get(VENTILATION_PROBLEM_ENTITY_ID) is None
mock_duco_client.async_get_nodes.return_value = mock_sensor_nodes
await _async_refresh(hass, freezer)
await async_fire_coordinator_update(hass, freezer)
assert hass.states.is_state(VENTILATION_PROBLEM_ENTITY_ID, STATE_OFF)
@@ -209,7 +202,7 @@ async def test_diagnostic_binary_sensor_becomes_unknown_without_known_status(
diagnostic_subsystems=diagnostic_subsystems
)
await _async_refresh(hass, freezer)
await async_fire_coordinator_update(hass, freezer)
assert hass.states.is_state(VENTILATION_PROBLEM_ENTITY_ID, STATE_UNKNOWN)
@@ -233,13 +226,13 @@ async def test_diagnostics_refresh_failure_is_isolated_and_recovers(
mock_duco_client.async_get_diagnostics_info.side_effect = exception_type("error")
await _async_refresh(hass, freezer)
await async_fire_coordinator_update(hass, freezer)
assert hass.states.is_state(VENTILATION_PROBLEM_ENTITY_ID, STATE_UNAVAILABLE)
assert hass.states.is_state("sensor.office_co2_carbon_dioxide", "405")
mock_duco_client.async_get_diagnostics_info.side_effect = None
await _async_refresh(hass, freezer)
await async_fire_coordinator_update(hass, freezer)
assert hass.states.is_state(VENTILATION_PROBLEM_ENTITY_ID, STATE_OFF)
@@ -266,7 +259,7 @@ async def test_initial_diagnostics_failure_is_isolated_and_recovers(
assert hass.states.is_state("sensor.office_co2_carbon_dioxide", "405")
mock_duco_client.async_get_diagnostics_info.side_effect = None
await _async_refresh(hass, freezer)
await async_fire_coordinator_update(hass, freezer)
assert hass.states.is_state(VENTILATION_PROBLEM_ENTITY_ID, STATE_OFF)
@@ -283,11 +276,11 @@ async def test_diagnostics_availability_transitions_logged(
mock_duco_client.async_get_diagnostics_info.side_effect = DucoError("error")
await setup_platform_integration(hass, mock_config_entry, [Platform.BINARY_SENSOR])
await _async_refresh(hass, freezer)
await async_fire_coordinator_update(hass, freezer)
mock_duco_client.async_get_diagnostics_info.side_effect = None
await _async_refresh(hass, freezer)
await _async_refresh(hass, freezer)
await async_fire_coordinator_update(hass, freezer)
await async_fire_coordinator_update(hass, freezer)
assert [
record.message
+3 -6
View File
@@ -8,7 +8,6 @@ from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.duco.const import SCAN_INTERVAL
from homeassistant.components.fan import (
ATTR_PERCENTAGE,
ATTR_PRESET_MODE,
@@ -21,9 +20,9 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from . import setup_platform_integration
from . import async_fire_coordinator_update, setup_platform_integration
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
from tests.common import MockConfigEntry, snapshot_platform
_FAN_ENTITY = "fan.living"
@@ -145,9 +144,7 @@ async def test_coordinator_update_marks_unavailable(
side_effect=DucoConnectionError("offline")
)
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
await async_fire_coordinator_update(hass, freezer)
state = hass.states.get(_FAN_ENTITY)
assert state is not None
+5 -11
View File
@@ -21,13 +21,13 @@ from duco_connectivity import (
from freezegun.api import FrozenDateTimeFactory
import pytest
from homeassistant.components.duco.const import BOX_NODE_ID, DOMAIN, SCAN_INTERVAL
from homeassistant.components.duco.const import BOX_NODE_ID, DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from . import setup_platform_integration
from . import async_fire_coordinator_update, setup_platform_integration
from .conftest import (
TEST_HOST,
TEST_MAC,
@@ -223,9 +223,7 @@ async def test_setup_entry_recovers_from_optional_temperature_capability_failure
assert mock_config_entry.state is ConfigEntryState.LOADED
assert hass.states.get("sensor.living_outdoor_air_temperature") is None
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
await async_fire_coordinator_update(hass, freezer)
state = hass.states.get("sensor.living_outdoor_air_temperature")
assert state is not None
@@ -289,9 +287,7 @@ async def test_empty_bypass_temperature_targets_are_retried(
assert hass.states.get("number.living_bypass_target_2") is None
mock_duco_client.async_get_bypass_supply_temperature_targets.assert_awaited_once_with()
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
await async_fire_coordinator_update(hass, freezer)
assert mock_duco_client.async_get_bypass_supply_temperature_targets.await_count == 2
assert hass.states.get("number.living_bypass_target_1") is not None
@@ -321,9 +317,7 @@ async def test_missing_bypass_temperature_targets_are_retried(
assert mock_config_entry.state is ConfigEntryState.LOADED
assert hass.states.get("number.living_bypass_target_1") is None
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
await async_fire_coordinator_update(hass, freezer)
state = hass.states.get("number.living_bypass_target_1")
assert state is not None
+6 -15
View File
@@ -13,7 +13,6 @@ from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.duco.const import SCAN_INTERVAL
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, SERVICE_SET_VALUE
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform
from homeassistant.core import HomeAssistant
@@ -21,9 +20,9 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
from . import setup_platform_integration
from . import async_fire_coordinator_update, setup_platform_integration
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
from tests.common import MockConfigEntry, snapshot_platform
_ZONE_1_ENTITY_ID = "number.living_bypass_target_1"
_ZONE_2_ENTITY_ID = "number.living_bypass_target_2"
@@ -298,18 +297,14 @@ async def test_bypass_supply_temperature_target_becomes_unavailable_when_missing
assert state.state == "20.0"
updated_target = replace(mock_bypass_supply_temperature_targets.pop(1), value=20.5)
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
await async_fire_coordinator_update(hass, freezer)
state = hass.states.get(_ZONE_1_ENTITY_ID)
assert state is not None
assert state.state == STATE_UNAVAILABLE
mock_bypass_supply_temperature_targets[1] = updated_target
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
await async_fire_coordinator_update(hass, freezer)
state = hass.states.get(_ZONE_1_ENTITY_ID)
assert state is not None
@@ -334,17 +329,13 @@ async def test_bypass_supply_temperature_target_recovers_from_refresh_error(
DucoError("Temporary bypass target failure"),
mock_bypass_supply_temperature_targets.copy(),
]
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
await async_fire_coordinator_update(hass, freezer)
state = hass.states.get(_ZONE_1_ENTITY_ID)
assert state is not None
assert state.state == STATE_UNAVAILABLE
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
await async_fire_coordinator_update(hass, freezer)
state = hass.states.get(_ZONE_1_ENTITY_ID)
assert state is not None
+24 -53
View File
@@ -19,14 +19,14 @@ from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.duco.const import BOX_NODE_ID, DOMAIN, SCAN_INTERVAL
from homeassistant.components.duco.const import BOX_NODE_ID, DOMAIN
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from . import setup_platform_integration
from . import async_fire_coordinator_update, setup_platform_integration
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
from tests.common import MockConfigEntry, snapshot_platform
FILTER_REMAINING_ENTITY_ID = "sensor.living_filter_remaining"
VENTILATION_TEMPERATURE_ENTITY_IDS = (
@@ -195,14 +195,15 @@ async def test_iaq_sensor_entities_disabled_by_default(
@pytest.mark.usefixtures("init_integration")
async def test_rssi_sensor_disabled_by_default(
async def test_diagnostic_sensor_entities_disabled_by_default(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
) -> None:
"""Test that the RSSI sensor is disabled by default."""
entry = entity_registry.async_get("sensor.living_signal_strength")
assert entry is not None
assert entry.disabled_by == er.RegistryEntryDisabler.INTEGRATION
"""Test that diagnostic sensor entities are disabled by default."""
for entity_id in ("sensor.living_signal_strength",):
entry = entity_registry.async_get(entity_id)
assert entry is not None
assert entry.disabled_by == er.RegistryEntryDisabler.INTEGRATION
@pytest.mark.usefixtures("init_integration")
@@ -223,9 +224,7 @@ async def test_coordinator_update_failure_marks_unavailable(
"""Test sensor entities become unavailable when the coordinator update fails."""
mock_duco_client.async_get_nodes.side_effect = exception_type(exception_message)
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
await async_fire_coordinator_update(hass, freezer)
state = hass.states.get("sensor.office_co2_carbon_dioxide")
assert state is not None
@@ -249,9 +248,7 @@ async def test_lan_info_failures_keep_node_entities_available(
"""Test node entities stay available when LAN info retrieval fails."""
mock_duco_client.async_get_lan_info = AsyncMock(side_effect=exception)
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
await async_fire_coordinator_update(hass, freezer)
state = hass.states.get("sensor.office_co2_carbon_dioxide")
assert state is not None
@@ -280,9 +277,7 @@ async def test_time_filter_remaining_missing_is_retried(
assert hass.states.get(FILTER_REMAINING_ENTITY_ID) is None
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
await async_fire_coordinator_update(hass, freezer)
assert mock_duco_client.async_get_time_filter_remaining.await_count == 2
state = hass.states.get(FILTER_REMAINING_ENTITY_ID)
@@ -307,9 +302,7 @@ async def test_empty_ventilation_temperatures_are_retried(
for entity_id in VENTILATION_TEMPERATURE_ENTITY_IDS:
assert hass.states.get(entity_id) is None
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
await async_fire_coordinator_update(hass, freezer)
assert mock_duco_client.async_get_ventilation_temperature_info.await_count == 2
state = hass.states.get("sensor.living_outdoor_air_temperature")
@@ -358,9 +351,7 @@ async def test_time_filter_remaining_transient_failure_recovers_sensor_creation(
assert hass.states.get(FILTER_REMAINING_ENTITY_ID) is None
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
await async_fire_coordinator_update(hass, freezer)
state = hass.states.get(FILTER_REMAINING_ENTITY_ID)
assert state is not None
@@ -414,9 +405,7 @@ async def test_new_node_added_dynamically(
new_node = dynamic_sensor_nodes[node_id]
mock_duco_client.async_get_nodes.return_value = [*mock_sensor_nodes, new_node]
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
await async_fire_coordinator_update(hass, freezer)
state = hass.states.get(expected_entity_id)
assert state is not None
@@ -448,9 +437,7 @@ async def test_deregistered_node_removes_device(
node for node in mock_sensor_nodes if node.node_id != 2
]
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
await async_fire_coordinator_update(hass, freezer)
# The device should be removed from the device registry.
device = device_registry.async_get_device_by_identifier(
@@ -485,9 +472,7 @@ async def test_box_node_not_removed_on_transient_incomplete_node_list(
node for node in mock_sensor_nodes if node.node_id != BOX_NODE_ID
]
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
await async_fire_coordinator_update(hass, freezer)
assert (
device_registry.async_get_device_by_identifier(
@@ -502,9 +487,7 @@ async def test_box_node_not_removed_on_transient_incomplete_node_list(
mock_duco_client.async_get_nodes.return_value = mock_sensor_nodes
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
await async_fire_coordinator_update(hass, freezer)
state = hass.states.get("fan.living")
assert state is not None
@@ -538,9 +521,7 @@ async def test_unknown_node_type_logs_warning_and_creates_no_entities(
)
mock_duco_client.async_get_nodes.return_value = [*mock_sensor_nodes, unknown_node]
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
await async_fire_coordinator_update(hass, freezer)
assert "99" in caplog.text
assert "unsupported" in caplog.text.lower()
@@ -589,9 +570,7 @@ async def test_previously_unknown_node_gets_entities_after_type_becomes_known(
*mock_sensor_nodes,
_make_node(NodeType.UNKNOWN),
]
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
await async_fire_coordinator_update(hass, freezer)
assert hass.states.get("sensor.future_sensor_humidity") is None
@@ -600,9 +579,7 @@ async def test_previously_unknown_node_gets_entities_after_type_becomes_known(
*mock_sensor_nodes,
_make_node("BSRH"),
]
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
await async_fire_coordinator_update(hass, freezer)
state = hass.states.get("sensor.future_sensor_humidity")
assert state is not None
@@ -642,16 +619,12 @@ async def test_unknown_node_logged_at_debug(
mock_duco_client.async_get_nodes.return_value = [*mock_sensor_nodes, unknown_node]
with caplog.at_level(logging.WARNING, logger="homeassistant.components.duco"):
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
await async_fire_coordinator_update(hass, freezer)
assert "has an unsupported device type" not in caplog.text
with caplog.at_level(logging.DEBUG, logger="homeassistant.components.duco"):
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
await async_fire_coordinator_update(hass, freezer)
assert "has an unsupported device type" in caplog.text
@@ -684,9 +657,7 @@ async def test_ventilation_state_unknown_returns_state_unknown(
]
mock_duco_client.async_get_nodes.return_value = updated_nodes
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
await async_fire_coordinator_update(hass, freezer)
state = hass.states.get("sensor.living_ventilation_state")
assert state is not None