Migrate Lyngdorf core to the lyngdorf 2.0 API (#180529)

This commit is contained in:
Alex Fishlock
2026-08-29 12:33:01 +02:00
committed by GitHub
parent 6a5d0472eb
commit 9575dd00a7
10 changed files with 73 additions and 58 deletions
+8 -11
View File
@@ -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
@@ -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,
+3 -9
View File
@@ -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."""
+2 -2
View File
@@ -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
+18 -15
View File
@@ -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
@@ -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
+25 -7
View File
@@ -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)
@@ -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)
+1 -1
View File
@@ -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]),
+1 -1
View File
@@ -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]),