From dc5b9b6cbfedffb8f84be3438e3fc936570e4813 Mon Sep 17 00:00:00 2001 From: Alex Fishlock Date: Sun, 6 Sep 2026 07:41:28 +0100 Subject: [PATCH] Remove the lyngdorf 1.11 compatibility scaffolding (#180855) --- .../components/lyngdorf/diagnostics.py | 5 +-- .../components/lyngdorf/media_player.py | 12 +++--- homeassistant/components/lyngdorf/number.py | 18 ++------- homeassistant/components/lyngdorf/select.py | 7 +--- tests/components/lyngdorf/conftest.py | 30 ++++++--------- tests/components/lyngdorf/test_diagnostics.py | 19 +--------- .../components/lyngdorf/test_media_player.py | 32 ---------------- tests/components/lyngdorf/test_number.py | 37 +++++-------------- 8 files changed, 33 insertions(+), 127 deletions(-) diff --git a/homeassistant/components/lyngdorf/diagnostics.py b/homeassistant/components/lyngdorf/diagnostics.py index 45ac73a947bb..1f080212c233 100644 --- a/homeassistant/components/lyngdorf/diagnostics.py +++ b/homeassistant/components/lyngdorf/diagnostics.py @@ -106,11 +106,8 @@ async def async_get_config_entry_diagnostics( for name, control in trims.items() } - # Not lipsync.range: the control reads None until the device reports a - # value, while the range is known from the model as soon as it connects. - lipsync_range = receiver.lipsync_range ranges: dict[str, Any] = { - "lipsync_range": asdict(lipsync_range) if lipsync_range is not None else None + "lipsync_range": asdict(lipsync.range) if lipsync is not None else None } ranges |= { f"{name}_range": asdict(control.range) if control is not None else None diff --git a/homeassistant/components/lyngdorf/media_player.py b/homeassistant/components/lyngdorf/media_player.py index 450c9150aab2..4412afe9a834 100644 --- a/homeassistant/components/lyngdorf/media_player.py +++ b/homeassistant/components/lyngdorf/media_player.py @@ -452,7 +452,7 @@ class LyngdorfMainDevice(LyngdorfDevice): def volume_level(self) -> float | None: """Volume level of the media player (0..1).""" volume = self._receiver.volume - if volume is None or volume.value is None: + if volume.value is None: return None return _to_ha_volume(volume.value, volume.range) @@ -481,20 +481,18 @@ class LyngdorfMainDevice(LyngdorfDevice): @override async def async_volume_up(self) -> None: """Volume up the media player.""" - if (volume := self._receiver.volume) is not None: - await volume.up() + await self._receiver.volume.up() @override async def async_volume_down(self) -> None: """Volume down the media player.""" - if (volume := self._receiver.volume) is not None: - await volume.down() + await self._receiver.volume.down() @override async def async_set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" - if (control := self._receiver.volume) is not None: - await control.set(_to_lyngdorf_volume(volume, control.range)) + control = self._receiver.volume + await control.set(_to_lyngdorf_volume(volume, control.range)) @override async def async_mute_volume(self, mute: bool) -> None: diff --git a/homeassistant/components/lyngdorf/number.py b/homeassistant/components/lyngdorf/number.py index e7edb15a69e3..473bd1a610d0 100644 --- a/homeassistant/components/lyngdorf/number.py +++ b/homeassistant/components/lyngdorf/number.py @@ -26,9 +26,6 @@ PARALLEL_UPDATES = 1 class LyngdorfNumberEntityDescription(NumberEntityDescription): """Describe a Lyngdorf number entity.""" - # Whether the model has this control at all. Must not depend on the device - # having reported a value, or the entity is dropped at startup. - range_fn: Callable[[LyngdorfReceiver], NumericRange | None] control_fn: Callable[[LyngdorfReceiver], NumericControl | None] set_value_fn: Callable[[NumericControl, float], Awaitable[None]] @@ -40,7 +37,6 @@ NUMBER_ENTITIES: tuple[LyngdorfNumberEntityDescription, ...] = ( device_class=NumberDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.MILLISECONDS, entity_category=EntityCategory.CONFIG, - range_fn=lambda r: r.lipsync_range, control_fn=lambda r: r.lipsync, # The device takes lip sync as whole milliseconds. set_value_fn=lambda c, v: c.set(round(v)), @@ -50,7 +46,6 @@ NUMBER_ENTITIES: tuple[LyngdorfNumberEntityDescription, ...] = ( translation_key="trim_bass", native_unit_of_measurement=UnitOfSoundPressure.DECIBEL, entity_category=EntityCategory.CONFIG, - range_fn=lambda r: c.range if (c := r.trims.get(Trim.BASS)) else None, control_fn=lambda r: r.trims.get(Trim.BASS), set_value_fn=lambda c, v: c.set(v), ), @@ -59,7 +54,6 @@ NUMBER_ENTITIES: tuple[LyngdorfNumberEntityDescription, ...] = ( translation_key="trim_treble", native_unit_of_measurement=UnitOfSoundPressure.DECIBEL, entity_category=EntityCategory.CONFIG, - range_fn=lambda r: c.range if (c := r.trims.get(Trim.TREBLE)) else None, control_fn=lambda r: r.trims.get(Trim.TREBLE), set_value_fn=lambda c, v: c.set(v), ), @@ -69,7 +63,6 @@ NUMBER_ENTITIES: tuple[LyngdorfNumberEntityDescription, ...] = ( entity_registry_enabled_default=False, native_unit_of_measurement=UnitOfSoundPressure.DECIBEL, entity_category=EntityCategory.CONFIG, - range_fn=lambda r: c.range if (c := r.trims.get(Trim.CENTER)) else None, control_fn=lambda r: r.trims.get(Trim.CENTER), set_value_fn=lambda c, v: c.set(v), ), @@ -79,7 +72,6 @@ NUMBER_ENTITIES: tuple[LyngdorfNumberEntityDescription, ...] = ( entity_registry_enabled_default=False, native_unit_of_measurement=UnitOfSoundPressure.DECIBEL, entity_category=EntityCategory.CONFIG, - range_fn=lambda r: c.range if (c := r.trims.get(Trim.HEIGHT)) else None, control_fn=lambda r: r.trims.get(Trim.HEIGHT), set_value_fn=lambda c, v: c.set(v), ), @@ -89,7 +81,6 @@ NUMBER_ENTITIES: tuple[LyngdorfNumberEntityDescription, ...] = ( entity_registry_enabled_default=False, native_unit_of_measurement=UnitOfSoundPressure.DECIBEL, entity_category=EntityCategory.CONFIG, - range_fn=lambda r: c.range if (c := r.trims.get(Trim.LFE)) else None, control_fn=lambda r: r.trims.get(Trim.LFE), set_value_fn=lambda c, v: c.set(v), ), @@ -99,7 +90,6 @@ NUMBER_ENTITIES: tuple[LyngdorfNumberEntityDescription, ...] = ( entity_registry_enabled_default=False, native_unit_of_measurement=UnitOfSoundPressure.DECIBEL, entity_category=EntityCategory.CONFIG, - range_fn=lambda r: c.range if (c := r.trims.get(Trim.SURROUND)) else None, control_fn=lambda r: r.trims.get(Trim.SURROUND), set_value_fn=lambda c, v: c.set(v), ), @@ -118,7 +108,7 @@ async def async_setup_entry( async_add_entities( LyngdorfNumber(receiver, config_entry, runtime_data.device_info, description) for description in NUMBER_ENTITIES - if description.range_fn(receiver) is not None + if description.control_fn(receiver) is not None ) @@ -144,11 +134,11 @@ class LyngdorfNumber(LyngdorfEntity, NumberEntity): @property def _range(self) -> NumericRange: """Return the device's range for this setting.""" - device_range = self.entity_description.range_fn(self._receiver) + control = self.entity_description.control_fn(self._receiver) # Entities are only created for controls the model actually has. if TYPE_CHECKING: - assert device_range is not None - return device_range + assert control is not None + return control.range @override @property diff --git a/homeassistant/components/lyngdorf/select.py b/homeassistant/components/lyngdorf/select.py index b2ff28a82a73..528545014b85 100644 --- a/homeassistant/components/lyngdorf/select.py +++ b/homeassistant/components/lyngdorf/select.py @@ -23,8 +23,7 @@ class LyngdorfSelectEntityDescription(SelectEntityDescription): current_option_fn: Callable[[LyngdorfReceiver], str | None] options_fn: Callable[[LyngdorfReceiver], list[str]] - # None on the pinned library, a coroutine on 2.x: await whichever it is. - select_option_fn: Callable[[LyngdorfReceiver, str], Awaitable[None] | None] + select_option_fn: Callable[[LyngdorfReceiver, str], Awaitable[None]] SELECT_ENTITIES: tuple[LyngdorfSelectEntityDescription, ...] = ( @@ -95,6 +94,4 @@ class LyngdorfSelect(LyngdorfEntity, SelectEntity): @override async def async_select_option(self, option: str) -> None: """Set the selected option.""" - result = self.entity_description.select_option_fn(self._receiver, option) - if result is not None: - await result + await self.entity_description.select_option_fn(self._receiver, option) diff --git a/tests/components/lyngdorf/conftest.py b/tests/components/lyngdorf/conftest.py index 5dbf74f2d27c..0f8311a45aa8 100644 --- a/tests/components/lyngdorf/conftest.py +++ b/tests/components/lyngdorf/conftest.py @@ -3,7 +3,6 @@ from __future__ import annotations from collections.abc import Generator -from typing import Self from unittest.mock import AsyncMock, MagicMock, Mock, patch from lyngdorf import ( @@ -14,6 +13,7 @@ from lyngdorf import ( Player, Remote, RemoteKey, + SteppableControl, Trim, ZoneB, ) @@ -63,18 +63,12 @@ def mock_setup_entry() -> Generator[None]: yield -class _FloatControl(float): - """A float that is also a control, as the library's 1.x values are.""" - - def __new__(cls, value: float, value_range: NumericRange) -> Self: - """Return a float carrying the control interface alongside it.""" - control = super().__new__(cls, value) - control.value = value - control.range = value_range - control.up = AsyncMock() - control.down = AsyncMock() - control.set = AsyncMock() - return control +def _steppable(value: float | None, value_range: NumericRange) -> MagicMock: + """Return a mocked volume-style control.""" + control = MagicMock(spec=SteppableControl) + control.value = value + control.range = value_range + return control def _control(value: float | None, value_range: NumericRange) -> MagicMock: @@ -125,7 +119,6 @@ def mock_receiver(mock_create_receiver: MagicMock) -> MagicMock: receiver.set_voicing.return_value = None receiver.set_room_perfect_position.return_value = None receiver.lipsync = None - receiver.lipsync_range = NumericRange(0, 500, 1) for _t in ("bass", "treble"): setattr(receiver, f"trim_{_t}", None) setattr(receiver, f"trim_{_t}_range", NumericRange(-12.0, 12.0, 0.1)) @@ -137,7 +130,7 @@ def mock_receiver(mock_create_receiver: MagicMock) -> MagicMock: receiver.zone_b_volume_range = NumericRange(-99.9, 24.0, 0.1) receiver.power_on = False - receiver.volume = _FloatControl(-40.0, NumericRange(-99.9, 24.0, 0.1)) + receiver.volume = _steppable(-40.0, NumericRange(-99.9, 24.0, 0.1)) receiver.muted = False receiver.sources = [] receiver.sound_modes = [] @@ -166,8 +159,7 @@ def mock_receiver(mock_create_receiver: MagicMock) -> MagicMock: receiver.can_shuffle = False receiver.available_repeat_modes = frozenset() - receiver.lipsync = _FloatControl(50.0, NumericRange(0, 500, 1)) - receiver.lipsync_range = NumericRange(0, 500, 1) + receiver.lipsync = _control(50.0, NumericRange(0, 500, 1)) receiver.trims = { Trim.BASS: _control(3.0, NumericRange(-12.0, 12.0, 0.1)), Trim.TREBLE: _control(0.0, NumericRange(-12.0, 12.0, 0.1)), @@ -199,7 +191,7 @@ def mock_receiver(mock_create_receiver: MagicMock) -> MagicMock: receiver.zone_b = zone_b receiver.zone_b_streaming_source = "DLNA" - receiver.volume = _FloatControl(-40.0, NumericRange(-99.9, 24.0, 0.1)) + receiver.volume = _steppable(-40.0, NumericRange(-99.9, 24.0, 0.1)) receiver.muted = False receiver.sources = [] receiver.sound_modes = [] @@ -225,7 +217,7 @@ def mock_receiver(mock_create_receiver: MagicMock) -> MagicMock: zone_b.audio_input = "aux" zone_b.streaming_source = "DLNA" zone_b.sources = [] - zone_b.volume = _FloatControl(-40.0, NumericRange(-99.9, 24.0, 0.1)) + zone_b.volume = _steppable(-40.0, NumericRange(-99.9, 24.0, 0.1)) receiver.zone_b = zone_b mock_create_receiver.return_value = receiver diff --git a/tests/components/lyngdorf/test_diagnostics.py b/tests/components/lyngdorf/test_diagnostics.py index 9ae84b3919e2..dd624b59b349 100644 --- a/tests/components/lyngdorf/test_diagnostics.py +++ b/tests/components/lyngdorf/test_diagnostics.py @@ -1,6 +1,6 @@ """Tests for the Lyngdorf diagnostics.""" -from unittest.mock import MagicMock, patch +from unittest.mock import patch import pytest from syrupy.assertion import SnapshotAssertion @@ -34,23 +34,6 @@ async def test_diagnostics( ) == snapshot(exclude=props("entry_id", "created_at", "modified_at")) -async def test_lipsync_range_reported_before_the_device_reports_a_value( - hass: HomeAssistant, - hass_client: ClientSessionGenerator, - init_integration: MockConfigEntry, - mock_receiver: MagicMock, -) -> None: - """Test the lipsync range still reports before the first value arrives.""" - mock_receiver.lipsync = None - - diagnostics = await get_diagnostics_for_config_entry( - hass, hass_client, init_integration - ) - - assert diagnostics["state"]["lipsync"] is None - assert diagnostics["ranges"]["lipsync_range"] is not None - - async def test_diagnostics_includes_ssdp_description( hass: HomeAssistant, hass_client: ClientSessionGenerator, diff --git a/tests/components/lyngdorf/test_media_player.py b/tests/components/lyngdorf/test_media_player.py index 732526904d96..6c11c5e34b10 100644 --- a/tests/components/lyngdorf/test_media_player.py +++ b/tests/components/lyngdorf/test_media_player.py @@ -480,38 +480,6 @@ async def test_set_play_mode( attrgetter(method)(playing_receiver).assert_awaited_once_with(expected) -@pytest.mark.usefixtures("init_integration") -async def test_volume_before_the_device_reports_one( - hass: HomeAssistant, - mock_receiver: MagicMock, -) -> None: - """Test the volume control being absent until the device reports a level.""" - mock_receiver.power_on = True - mock_receiver.volume = None - # Changed alongside so the assertions below fail if building the state - # raised rather than merely omitting the volume. - mock_receiver.muted = True - notify_receiver_update(mock_receiver) - await hass.async_block_till_done() - - state = hass.states.get(MAIN_ZONE) - assert state.attributes[ATTR_MEDIA_VOLUME_MUTED] is True - assert state.attributes.get(ATTR_MEDIA_VOLUME_LEVEL) is None - - await hass.services.async_call( - MEDIA_PLAYER_DOMAIN, - SERVICE_VOLUME_UP, - {ATTR_ENTITY_ID: MAIN_ZONE}, - blocking=True, - ) - await hass.services.async_call( - MEDIA_PLAYER_DOMAIN, - SERVICE_VOLUME_SET, - {ATTR_ENTITY_ID: MAIN_ZONE, ATTR_MEDIA_VOLUME_LEVEL: 0.5}, - blocking=True, - ) - - @pytest.mark.usefixtures("init_integration") async def test_transport_features_follow_the_source( hass: HomeAssistant, diff --git a/tests/components/lyngdorf/test_number.py b/tests/components/lyngdorf/test_number.py index 7535385de776..ae36f3ba281a 100644 --- a/tests/components/lyngdorf/test_number.py +++ b/tests/components/lyngdorf/test_number.py @@ -113,7 +113,7 @@ async def test_number_none_values( mock_receiver: MagicMock, ) -> None: """Test a number shows unknown when the device reports nothing.""" - mock_receiver.lipsync = None + mock_receiver.lipsync.value = None mock_receiver.trims[Trim.BASS].value = None notify_receiver_update(mock_receiver) await hass.async_block_till_done() @@ -122,37 +122,15 @@ async def test_number_none_values( assert hass.states.get(TRIM_BASS_ENTITY_ID).state == STATE_UNKNOWN -@pytest.mark.usefixtures("entity_registry_enabled_by_default") -async def test_entity_created_before_the_device_reports_a_value( - hass: HomeAssistant, - mock_config_entry: MockConfigEntry, - mock_receiver: MagicMock, -) -> None: - """Test a control the model has still gets an entity before its first report.""" - mock_receiver.lipsync = None - mock_config_entry.add_to_hass(hass) - - with ( - patch( - "homeassistant.components.lyngdorf.lookup_model", - return_value=LyngdorfModel.MP_60, - ), - patch("homeassistant.components.lyngdorf.PLATFORMS", [Platform.NUMBER]), - ): - await hass.config_entries.async_setup(mock_config_entry.entry_id) - await hass.async_block_till_done() - - assert hass.states.get(LIPSYNC_ENTITY_ID).state == STATE_UNKNOWN - - @pytest.mark.usefixtures("entity_registry_enabled_by_default", "mock_receiver") async def test_entities_absent_for_controls_the_model_lacks( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_receiver: MagicMock, + entity_registry: er.EntityRegistry, ) -> None: """Test no entity is created where the model has no such control.""" - mock_receiver.lipsync_range = None + mock_receiver.lipsync = None del mock_receiver.trims[Trim.SURROUND] mock_config_entry.add_to_hass(hass) @@ -166,9 +144,12 @@ async def test_entities_absent_for_controls_the_model_lacks( await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - assert hass.states.get(LIPSYNC_ENTITY_ID) is None - assert hass.states.get(TRIM_SURROUND_ENTITY_ID) is None - assert hass.states.get(TRIM_BASS_ENTITY_ID) is not None + # The registry, not the state machine: an entity that was created and then + # failed to render has no state either, so states alone cannot tell the two + # apart. + assert entity_registry.async_get(LIPSYNC_ENTITY_ID) is None + assert entity_registry.async_get(TRIM_SURROUND_ENTITY_ID) is None + assert entity_registry.async_get(TRIM_BASS_ENTITY_ID) is not None @pytest.mark.usefixtures("init_integration")