Cleanup stale devices on incomfort integration startup (#136566)

This commit is contained in:
Jan Bouwhuis 2025-01-26 21:57:32 +01:00 committed by GitHub
parent 7133eec185
commit 3e0f6562c7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 97 additions and 5 deletions

View File

@ -7,12 +7,12 @@ from incomfortclient import InvalidGateway, InvalidHeaterList
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform from homeassistant.const import Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from .const import DOMAIN from .const import DOMAIN
from .coordinator import InComfortDataCoordinator, async_connect_gateway from .coordinator import InComfortData, InComfortDataCoordinator, async_connect_gateway
from .errors import InComfortTimeout, InComfortUnknownError, NoHeaters, NotFound from .errors import InComfortTimeout, InComfortUnknownError, NoHeaters, NotFound
PLATFORMS = ( PLATFORMS = (
@ -27,6 +27,43 @@ INTEGRATION_TITLE = "Intergas InComfort/Intouch Lan2RF gateway"
type InComfortConfigEntry = ConfigEntry[InComfortDataCoordinator] type InComfortConfigEntry = ConfigEntry[InComfortDataCoordinator]
@callback
def async_cleanup_stale_devices(
hass: HomeAssistant,
entry: InComfortConfigEntry,
data: InComfortData,
gateway_device: dr.DeviceEntry,
) -> None:
"""Cleanup stale heater devices and climates."""
heater_serial_numbers = {heater.serial_no for heater in data.heaters}
device_registry = dr.async_get(hass)
device_entries = device_registry.devices.get_devices_for_config_entry_id(
entry.entry_id
)
stale_heater_serial_numbers: list[str] = [
device_entry.serial_number
for device_entry in device_entries
if device_entry.id != gateway_device.id
and device_entry.serial_number is not None
and device_entry.serial_number not in heater_serial_numbers
]
if not stale_heater_serial_numbers:
return
cleanup_devices: list[str] = []
# Find stale heater and climate devices
for serial_number in stale_heater_serial_numbers:
cleanup_list = [f"{serial_number}_{index}" for index in range(1, 4)]
cleanup_list.append(serial_number)
cleanup_identifiers = [{(DOMAIN, cleanup_id)} for cleanup_id in cleanup_list]
cleanup_devices.extend(
device_entry.id
for device_entry in device_entries
if device_entry.identifiers in cleanup_identifiers
)
for device_id in cleanup_devices:
device_registry.async_remove_device(device_id)
async def async_setup_entry(hass: HomeAssistant, entry: InComfortConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: InComfortConfigEntry) -> bool:
"""Set up a config entry.""" """Set up a config entry."""
try: try:
@ -46,7 +83,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: InComfortConfigEntry) ->
# Register discovered gateway device # Register discovered gateway device
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)
device_registry.async_get_or_create( gateway_device = device_registry.async_get_or_create(
config_entry_id=entry.entry_id, config_entry_id=entry.entry_id,
identifiers={(DOMAIN, entry.entry_id)}, identifiers={(DOMAIN, entry.entry_id)},
connections={(dr.CONNECTION_NETWORK_MAC, entry.unique_id)} connections={(dr.CONNECTION_NETWORK_MAC, entry.unique_id)}
@ -55,6 +92,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: InComfortConfigEntry) ->
manufacturer="Intergas", manufacturer="Intergas",
name="RFGateway", name="RFGateway",
) )
async_cleanup_stale_devices(hass, entry, data, gateway_device)
coordinator = InComfortDataCoordinator(hass, data, entry.entry_id) coordinator = InComfortDataCoordinator(hass, data, entry.entry_id)
entry.runtime_data = coordinator entry.runtime_data = coordinator
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()

View File

@ -1,6 +1,7 @@
"""Tests for Intergas InComfort integration.""" """Tests for Intergas InComfort integration."""
from datetime import timedelta from datetime import timedelta
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
from aiohttp import ClientResponseError, RequestInfo from aiohttp import ClientResponseError, RequestInfo
@ -8,13 +9,17 @@ from freezegun.api import FrozenDateTimeFactory
from incomfortclient import InvalidGateway, InvalidHeaterList from incomfortclient import InvalidGateway, InvalidHeaterList
import pytest import pytest
from homeassistant.components.incomfort import DOMAIN
from homeassistant.components.incomfort.coordinator import UPDATE_INTERVAL from homeassistant.components.incomfort.coordinator import UPDATE_INTERVAL
from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import STATE_UNAVAILABLE from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.device_registry import DeviceRegistry
from tests.common import async_fire_time_changed from .conftest import MOCK_HEATER_STATUS
from tests.common import MockConfigEntry, async_fire_time_changed
@pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.usefixtures("entity_registry_enabled_by_default")
@ -22,13 +27,62 @@ async def test_setup_platforms(
hass: HomeAssistant, hass: HomeAssistant,
mock_incomfort: MagicMock, mock_incomfort: MagicMock,
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
mock_config_entry: ConfigEntry, mock_config_entry: MockConfigEntry,
) -> None: ) -> None:
"""Test the incomfort integration is set up correctly.""" """Test the incomfort integration is set up correctly."""
await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.config_entries.async_setup(mock_config_entry.entry_id)
assert mock_config_entry.state is ConfigEntryState.LOADED assert mock_config_entry.state is ConfigEntryState.LOADED
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
@pytest.mark.parametrize(
"mock_heater_status", [MOCK_HEATER_STATUS | {"serial_no": "c01d00c0ffee"}]
)
async def test_stale_devices_cleanup(
hass: HomeAssistant,
device_registry: DeviceRegistry,
mock_incomfort: MagicMock,
entity_registry: er.EntityRegistry,
mock_config_entry: MockConfigEntry,
mock_heater_status: dict[str, Any],
) -> None:
"""Test the incomfort integration is cleaning up stale devices."""
# Setup an old heater with serial_no c01d00c0ffee
await hass.config_entries.async_setup(mock_config_entry.entry_id)
assert mock_config_entry.state is ConfigEntryState.LOADED
await hass.config_entries.async_unload(mock_config_entry.entry_id)
old_entries = device_registry.devices.get_devices_for_config_entry_id(
mock_config_entry.entry_id
)
assert len(old_entries) == 3
old_heater = device_registry.async_get_device({(DOMAIN, "c01d00c0ffee")})
assert old_heater is not None
assert old_heater.serial_number == "c01d00c0ffee"
old_climate = device_registry.async_get_device({(DOMAIN, "c01d00c0ffee_1")})
assert old_heater is not None
old_climate = device_registry.async_get_device({(DOMAIN, "c01d00c0ffee_1")})
assert old_climate is not None
mock_heater_status["serial_no"] = "c0ffeec0ffee"
await hass.config_entries.async_setup(mock_config_entry.entry_id)
assert mock_config_entry.state is ConfigEntryState.LOADED
new_entries = device_registry.devices.get_devices_for_config_entry_id(
mock_config_entry.entry_id
)
assert len(new_entries) == 3
new_heater = device_registry.async_get_device({(DOMAIN, "c0ffeec0ffee")})
assert new_heater is not None
assert new_heater.serial_number == "c0ffeec0ffee"
new_climate = device_registry.async_get_device({(DOMAIN, "c0ffeec0ffee_1")})
assert new_climate is not None
old_heater = device_registry.async_get_device({(DOMAIN, "c01d00c0ffee")})
assert old_heater is None
old_climate = device_registry.async_get_device({(DOMAIN, "c01d00c0ffee_1")})
assert old_climate is None
@pytest.mark.usefixtures("entity_registry_enabled_by_default") @pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_coordinator_updates( async def test_coordinator_updates(
hass: HomeAssistant, hass: HomeAssistant,