diff --git a/homeassistant/components/lyngdorf/__init__.py b/homeassistant/components/lyngdorf/__init__.py index 7237091935e1..a34ae51c7020 100644 --- a/homeassistant/components/lyngdorf/__init__.py +++ b/homeassistant/components/lyngdorf/__init__.py @@ -2,7 +2,7 @@ import logging -from lyngdorf.device import async_create_receiver, lookup_receiver_model +from lyngdorf import LyngdorfReceiver, create_receiver, lookup_model from homeassistant.const import CONF_HOST, CONF_MODEL, EVENT_HOMEASSISTANT_STOP from homeassistant.core import Event, HomeAssistant, callback @@ -37,14 +37,14 @@ async def async_setup_entry( hass: HomeAssistant, config_entry: LyngdorfConfigEntry ) -> bool: """Set up Lyngdorf from a config entry.""" - lyngdorf_model = lookup_receiver_model(config_entry.data[CONF_MODEL]) + lyngdorf_model = lookup_model(config_entry.data[CONF_MODEL]) assert lyngdorf_model is not None try: - receiver = await async_create_receiver( + receiver: LyngdorfReceiver = await create_receiver( config_entry.data[CONF_HOST], lyngdorf_model ) - await receiver.async_connect() + await receiver.connect() except TimeoutError as err: raise ConfigEntryNotReady( translation_domain=DOMAIN, @@ -72,7 +72,7 @@ async def async_setup_entry( ) zone_b_device_info: DeviceInfo | None = None - if lyngdorf_model.has_zone_b_feature(): + if receiver.zone_b is not None: # Register the main device up front so Zone B can resolve its via_device_id. device_registry = dr.async_get(hass) device_registry.async_get_or_create( @@ -113,16 +113,13 @@ async def async_setup_entry( else: _LOGGER.info("Lyngdorf %s is unavailable", host) - receiver.register_notification_callback(_log_availability_change) - config_entry.async_on_unload( - lambda: receiver.un_register_notification_callback(_log_availability_change) - ) + config_entry.async_on_unload(receiver.on_change(_log_availability_change)) await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) async def _async_disconnect(event: Event) -> None: """Disconnect from receiver.""" - await receiver.async_disconnect() + await receiver.disconnect() config_entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_disconnect) @@ -139,5 +136,5 @@ async def async_unload_entry( config_entry, PLATFORMS ) if unload_ok: - await config_entry.runtime_data.receiver.async_disconnect() + await config_entry.runtime_data.receiver.disconnect() return unload_ok diff --git a/homeassistant/components/lyngdorf/config_flow.py b/homeassistant/components/lyngdorf/config_flow.py index 94366cc67b62..18c39e8406ee 100644 --- a/homeassistant/components/lyngdorf/config_flow.py +++ b/homeassistant/components/lyngdorf/config_flow.py @@ -4,11 +4,12 @@ import logging from typing import Any, override from urllib.parse import urlparse -from lyngdorf.const import LyngdorfModel -from lyngdorf.device import ( - async_find_receiver_model, - async_get_device_serial, - lookup_receiver_model, +from lyngdorf import ( + LyngdorfModel, + discover_model, + discover_ssdp_location, + fetch_device_serial, + lookup_model, ) import voluptuous as vol @@ -89,7 +90,7 @@ class LyngdorfFlowHandler(ConfigFlow, domain=DOMAIN): async def _async_probe(self, host: str) -> tuple[LyngdorfModel, str]: """Return the model and serial of the device at a host.""" try: - model = await async_find_receiver_model(host) + model = await discover_model(host) except TimeoutError as err: raise TimeoutConnect from err except OSError as err: @@ -98,7 +99,8 @@ class LyngdorfFlowHandler(ConfigFlow, domain=DOMAIN): raise UnsupportedModel try: - serial = await async_get_device_serial(host) + location = await discover_ssdp_location(host) + serial = await fetch_device_serial(location) if location else None except TimeoutError as err: raise TimeoutConnect from err except OSError as err: @@ -165,7 +167,7 @@ class LyngdorfFlowHandler(ConfigFlow, domain=DOMAIN): assert self._host try: - model = await async_find_receiver_model(self._host) + model = await discover_model(self._host) except TimeoutError, OSError: return self.async_abort(reason="cannot_connect") if not model: @@ -226,7 +228,7 @@ class LyngdorfFlowHandler(ConfigFlow, domain=DOMAIN): raise AbortFlow("cannot_connect") device_model_name = discovery_info.upnp.get(ATTR_UPNP_MODEL_NAME) or "" - if not (model := lookup_receiver_model(device_model_name)): + if not (model := lookup_model(device_model_name)): _LOGGER.warning( "SSDP discovered device with unrecognized model name %r at %s", device_model_name, diff --git a/homeassistant/components/lyngdorf/entity.py b/homeassistant/components/lyngdorf/entity.py index 4c95d27e86d1..7b30f495119f 100644 --- a/homeassistant/components/lyngdorf/entity.py +++ b/homeassistant/components/lyngdorf/entity.py @@ -2,7 +2,7 @@ from typing import override -from lyngdorf.device import Receiver +from lyngdorf import LyngdorfReceiver from homeassistant.core import callback from homeassistant.helpers.device_registry import DeviceInfo @@ -16,7 +16,7 @@ class LyngdorfEntity(Entity): _attr_available = True _attr_should_poll = False - def __init__(self, receiver: Receiver, device_info: DeviceInfo) -> None: + def __init__(self, receiver: LyngdorfReceiver, device_info: DeviceInfo) -> None: """Initialize the entity.""" self._receiver = receiver self._attr_device_info = device_info @@ -25,15 +25,9 @@ class LyngdorfEntity(Entity): async def async_added_to_hass(self) -> None: """Register notification callback when added to hass.""" await super().async_added_to_hass() - self._receiver.register_notification_callback(self._handle_receiver_update) + self.async_on_remove(self._receiver.on_change(self._handle_receiver_update)) self._update_availability() - @override - async def async_will_remove_from_hass(self) -> None: - """Unregister notification callback when removed from hass.""" - await super().async_will_remove_from_hass() - self._receiver.un_register_notification_callback(self._handle_receiver_update) - @callback def _handle_receiver_update(self) -> None: """Handle receiver updates.""" diff --git a/homeassistant/components/lyngdorf/models.py b/homeassistant/components/lyngdorf/models.py index 879cca55c27e..18a70d0e5fd7 100644 --- a/homeassistant/components/lyngdorf/models.py +++ b/homeassistant/components/lyngdorf/models.py @@ -2,7 +2,7 @@ from dataclasses import dataclass -from lyngdorf.device import Receiver +from lyngdorf import LyngdorfReceiver from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.device_registry import DeviceInfo @@ -12,7 +12,7 @@ from homeassistant.helpers.device_registry import DeviceInfo class LyngdorfRuntimeData: """Runtime data for Lyngdorf integration.""" - receiver: Receiver + receiver: LyngdorfReceiver device_info: DeviceInfo zone_b_device_info: DeviceInfo | None diff --git a/tests/components/lyngdorf/conftest.py b/tests/components/lyngdorf/conftest.py index f44ac0535592..bae4238c3d8f 100644 --- a/tests/components/lyngdorf/conftest.py +++ b/tests/components/lyngdorf/conftest.py @@ -5,8 +5,7 @@ from __future__ import annotations from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, Mock, patch -from lyngdorf.const import LyngdorfModel -from lyngdorf.device import Receiver +from lyngdorf import LyngdorfModel, LyngdorfReceiver from lyngdorf.models.base import NumericRange from lyngdorf.remote import RemoteKey import pytest @@ -58,10 +57,8 @@ def mock_setup_entry() -> Generator[None]: @pytest.fixture def mock_receiver() -> Generator[MagicMock]: """Return a mocked Lyngdorf receiver.""" - with patch( - "homeassistant.components.lyngdorf.async_create_receiver" - ) as create_mock: - receiver = MagicMock(spec=Receiver) + with patch("homeassistant.components.lyngdorf.create_receiver") as create_mock: + receiver = MagicMock(spec=LyngdorfReceiver) receiver.name = "Mock Lyngdorf" receiver.connected = True receiver.has_remote_keys = True @@ -148,19 +145,25 @@ def mock_receiver() -> Generator[MagicMock]: @pytest.fixture def mock_get_device_serial() -> Generator[AsyncMock]: - """Return a mocked async_get_device_serial function.""" - with patch( - "homeassistant.components.lyngdorf.config_flow.async_get_device_serial", - new=AsyncMock(return_value="0050c27c76b2"), - ) as serial_mock: + """Return a mocked fetch_device_serial function.""" + with ( + patch( + "homeassistant.components.lyngdorf.config_flow.discover_ssdp_location", + new=AsyncMock(return_value="http://127.0.0.1:8080/desc.xml"), + ), + patch( + "homeassistant.components.lyngdorf.config_flow.fetch_device_serial", + new=AsyncMock(return_value="0050c27c76b2"), + ) as serial_mock, + ): yield serial_mock @pytest.fixture def mock_find_receiver_model() -> Generator[AsyncMock]: - """Return a mocked async_find_receiver_model function.""" + """Return a mocked discover_model function.""" with patch( - "homeassistant.components.lyngdorf.config_flow.async_find_receiver_model", + "homeassistant.components.lyngdorf.config_flow.discover_model", new=AsyncMock(return_value=LyngdorfModel.MP_60), ) as find_mock: yield find_mock @@ -168,7 +171,7 @@ def mock_find_receiver_model() -> Generator[AsyncMock]: def notify_receiver_update(receiver: MagicMock) -> None: """Fire every notification callback the entities registered.""" - for call in receiver.register_notification_callback.call_args_list: + for call in receiver.on_change.call_args_list: call.args[0]() @@ -195,7 +198,7 @@ async def init_integration( mock_config_entry.add_to_hass(hass) with ( - patch("homeassistant.components.lyngdorf.lookup_receiver_model") as lookup, + patch("homeassistant.components.lyngdorf.lookup_model") as lookup, patch("homeassistant.components.lyngdorf.PLATFORMS", platforms), ): lookup.return_value = LyngdorfModel.MP_60 diff --git a/tests/components/lyngdorf/test_config_flow.py b/tests/components/lyngdorf/test_config_flow.py index 7dabfb3a8ea4..96f8520ded25 100644 --- a/tests/components/lyngdorf/test_config_flow.py +++ b/tests/components/lyngdorf/test_config_flow.py @@ -4,7 +4,7 @@ from __future__ import annotations from unittest.mock import AsyncMock -from lyngdorf.const import LyngdorfModel +from lyngdorf import LyngdorfModel import pytest from homeassistant.components.lyngdorf.const import CONF_SERIAL_NUMBER, DOMAIN diff --git a/tests/components/lyngdorf/test_init.py b/tests/components/lyngdorf/test_init.py index 180aad6420a6..0e80319acb37 100644 --- a/tests/components/lyngdorf/test_init.py +++ b/tests/components/lyngdorf/test_init.py @@ -2,7 +2,7 @@ from unittest.mock import MagicMock, patch -from lyngdorf.const import LyngdorfModel +from lyngdorf import LyngdorfModel import pytest from homeassistant.components.lyngdorf.const import CONF_SERIAL_NUMBER, DOMAIN @@ -31,10 +31,10 @@ async def test_setup_entry_connection_failures( ) -> None: """Test setup retries when connecting to the receiver fails.""" mock_config_entry.add_to_hass(hass) - mock_receiver.async_connect.side_effect = exc + mock_receiver.connect.side_effect = exc with patch( - "homeassistant.components.lyngdorf.lookup_receiver_model", + "homeassistant.components.lyngdorf.lookup_model", return_value=LyngdorfModel.MP_60, ): await hass.config_entries.async_setup(mock_config_entry.entry_id) @@ -54,7 +54,7 @@ async def test_receiver_disconnects_on_hass_stop( hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() - mock_receiver.async_disconnect.assert_awaited_once() + mock_receiver.disconnect.assert_awaited_once() async def test_unload_entry( @@ -69,6 +69,23 @@ async def test_unload_entry( assert init_integration.state is ConfigEntryState.NOT_LOADED +async def test_unload_releases_receiver_subscriptions( + hass: HomeAssistant, + init_integration: MockConfigEntry, + mock_receiver: MagicMock, +) -> None: + """Test every receiver subscription is released when the entry unloads.""" + unsubscribe = mock_receiver.on_change.return_value + registered = mock_receiver.on_change.call_count + assert registered > 0 + assert unsubscribe.call_count == 0 + + assert await hass.config_entries.async_unload(init_integration.entry_id) + await hass.async_block_till_done() + + assert unsubscribe.call_count == registered + + async def test_zone_b_via_device_id( init_integration: MockConfigEntry, device_registry: dr.DeviceRegistry, @@ -115,7 +132,7 @@ async def test_mac_connection_registered_when_serial_is_mac( entry.add_to_hass(hass) with patch( - "homeassistant.components.lyngdorf.lookup_receiver_model", + "homeassistant.components.lyngdorf.lookup_model", return_value=LyngdorfModel.MP_60, ): await hass.config_entries.async_setup(entry.entry_id) @@ -131,17 +148,18 @@ async def test_mac_connection_registered_when_serial_is_mac( assert mac_connections == expected_mac_connections -@pytest.mark.usefixtures("mock_receiver") async def test_no_zone_b_device_for_model_without_zone_b( hass: HomeAssistant, mock_config_entry: MockConfigEntry, + mock_receiver: MagicMock, device_registry: dr.DeviceRegistry, ) -> None: """Test no Zone B device is created for a model without Zone B.""" mock_config_entry.add_to_hass(hass) + mock_receiver.zone_b = None with patch( - "homeassistant.components.lyngdorf.lookup_receiver_model", + "homeassistant.components.lyngdorf.lookup_model", return_value=LyngdorfModel.TDAI_3400, ): await hass.config_entries.async_setup(mock_config_entry.entry_id) diff --git a/tests/components/lyngdorf/test_media_player.py b/tests/components/lyngdorf/test_media_player.py index f1d4811eb028..4a7ee8b634ed 100644 --- a/tests/components/lyngdorf/test_media_player.py +++ b/tests/components/lyngdorf/test_media_player.py @@ -117,17 +117,18 @@ async def test_entities( await snapshot_platform(hass, entity_registry, snapshot, init_integration.entry_id) -@pytest.mark.usefixtures("mock_receiver") async def test_no_zone_b_entity_for_model_without_zone_b( hass: HomeAssistant, mock_config_entry: MockConfigEntry, + mock_receiver: MagicMock, entity_registry: er.EntityRegistry, ) -> None: """Test no Zone B media player entity is created for a model without Zone B.""" mock_config_entry.add_to_hass(hass) + mock_receiver.zone_b = None with patch( - "homeassistant.components.lyngdorf.lookup_receiver_model", + "homeassistant.components.lyngdorf.lookup_model", return_value=LyngdorfModel.TDAI_3400, ): await hass.config_entries.async_setup(mock_config_entry.entry_id) diff --git a/tests/components/lyngdorf/test_number.py b/tests/components/lyngdorf/test_number.py index 303c3f6a1f98..fe361e3b3088 100644 --- a/tests/components/lyngdorf/test_number.py +++ b/tests/components/lyngdorf/test_number.py @@ -144,7 +144,7 @@ async def test_entities_absent_for_controls_the_model_lacks( with ( patch( - "homeassistant.components.lyngdorf.lookup_receiver_model", + "homeassistant.components.lyngdorf.lookup_model", return_value=LyngdorfModel.MP_60, ), patch("homeassistant.components.lyngdorf.PLATFORMS", [Platform.NUMBER]), diff --git a/tests/components/lyngdorf/test_remote.py b/tests/components/lyngdorf/test_remote.py index d38ac70c5424..a16db247b970 100644 --- a/tests/components/lyngdorf/test_remote.py +++ b/tests/components/lyngdorf/test_remote.py @@ -151,7 +151,7 @@ async def test_no_entity_for_model_without_remote_keys( with ( patch( - "homeassistant.components.lyngdorf.lookup_receiver_model", + "homeassistant.components.lyngdorf.lookup_model", return_value=LyngdorfModel.TDAI_3400, ), patch("homeassistant.components.lyngdorf.PLATFORMS", [Platform.REMOTE]),