mirror of
https://github.com/home-assistant/core.git
synced 2026-09-24 23:41:48 -05:00
Remove the lyngdorf 1.11 compatibility scaffolding (#180855)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user