Type the lookin UDP manager with a HassKey (#181098)

This commit is contained in:
David Wu
2026-09-02 20:00:42 +02:00
committed by GitHub
parent f04652b324
commit 5c5073dd3a
2 changed files with 44 additions and 9 deletions
+9 -9
View File
@@ -1,5 +1,4 @@
"""The lookin integration."""
# pylint: disable=home-assistant-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
import asyncio
from collections.abc import Callable, Coroutine
@@ -23,6 +22,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util.hass_dict import HassKey
from .const import (
DOMAIN,
@@ -37,8 +37,6 @@ from .models import LookinConfigEntry, LookinData
LOGGER = logging.getLogger(__name__)
UDP_MANAGER = "udp_manager"
def _async_climate_updater(
lookin_protocol: LookInHttpProtocol,
@@ -90,9 +88,13 @@ class LookinUDPManager:
self._subscriptions = None
# One UDP listener serves every lookin device, so the manager is shared between
# config entries rather than owned by any one of them.
UDP_MANAGER: HassKey[LookinUDPManager] = HassKey(DOMAIN)
async def async_setup_entry(hass: HomeAssistant, entry: LookinConfigEntry) -> bool:
"""Set up lookin from a config entry."""
domain_data = hass.data.setdefault(DOMAIN, {})
host = entry.data[CONF_HOST]
lookin_protocol = LookInHttpProtocol(
api_uri=f"http://{host}", session=async_get_clientsession(hass)
@@ -159,10 +161,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: LookinConfigEntry) -> bo
meteo.update_from_value(event.value)
meteo_coordinator.async_set_updated_data(meteo)
if UDP_MANAGER not in domain_data:
manager = domain_data[UDP_MANAGER] = LookinUDPManager()
else:
manager = domain_data[UDP_MANAGER]
if (manager := hass.data.get(UDP_MANAGER)) is None:
manager = hass.data[UDP_MANAGER] = LookinUDPManager()
lookin_udp_subs = await manager.async_get_subscriptions()
@@ -200,7 +200,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: LookinConfigEntry) -> b
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if not hass.config_entries.async_loaded_entries(DOMAIN):
manager: LookinUDPManager = hass.data[DOMAIN][UDP_MANAGER]
manager = hass.data[UDP_MANAGER]
await manager.async_stop()
return unload_ok
+35
View File
@@ -2,6 +2,7 @@
from unittest.mock import AsyncMock, MagicMock, patch
from homeassistant.components.lookin import UDP_MANAGER
from homeassistant.components.lookin.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_HOST
@@ -81,3 +82,37 @@ async def test_controlled_device_links_to_lookin_device(
)
assert controlled_device is not None
assert controlled_device.via_device_id == lookin_device.id
async def test_udp_manager_outlives_the_config_entry(hass: HomeAssistant) -> None:
"""Test the shared UDP manager is reused rather than rebuilt per entry."""
device = _mocked_device()
remote = _mocked_remote()
protocol = _mocked_protocol(device, remote)
subscriptions = MagicMock()
subscriptions.subscribe_event = MagicMock(return_value=MagicMock())
entry = MockConfigEntry(
domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=DEVICE_ID
)
entry.add_to_hass(hass)
with (
patch(f"{MODULE}.LookInHttpProtocol", return_value=protocol),
patch(f"{MODULE}.LookinUDPSubscriptions", return_value=subscriptions),
patch(f"{MODULE}.start_lookin_udp", AsyncMock(return_value=MagicMock())),
):
assert await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
manager = hass.data[UDP_MANAGER]
assert manager is not None
await hass.config_entries.async_reload(entry.entry_id)
await hass.async_block_till_done()
assert entry.state is ConfigEntryState.LOADED
# The manager is keyed globally, not on the entry, so a reload reuses it
# instead of leaving a second listener behind.
assert hass.data[UDP_MANAGER] is manager