diff --git a/homeassistant/components/lyngdorf/diagnostics.py b/homeassistant/components/lyngdorf/diagnostics.py index 701bdfa9c6ef..0acb33de4875 100644 --- a/homeassistant/components/lyngdorf/diagnostics.py +++ b/homeassistant/components/lyngdorf/diagnostics.py @@ -3,6 +3,8 @@ from dataclasses import asdict from typing import Any +from lyngdorf import Trim + from homeassistant.components.diagnostics import async_redact_data from homeassistant.components.ssdp import async_get_discovery_info_by_st from homeassistant.const import CONF_HOST @@ -12,10 +14,16 @@ from homeassistant.helpers.service_info.ssdp import ATTR_UPNP_SERIAL from .const import CONF_SERIAL_NUMBER, SSDP_ST from .models import LyngdorfConfigEntry -_TRIM_NAMES = tuple( - f"trim_{trim}" for trim in ("bass", "treble", "centre", "height", "lfe", "surround") -) -_RANGE_NAMES = ("lipsync_range", *(f"{name}_range" for name in _TRIM_NAMES)) +# Reported under the integration's own names, which do not all match the +# library's spelling of the band. +_TRIMS = { + "trim_bass": Trim.BASS, + "trim_treble": Trim.TREBLE, + "trim_centre": Trim.CENTER, + "trim_height": Trim.HEIGHT, + "trim_lfe": Trim.LFE, + "trim_surround": Trim.SURROUND, +} # The serial doubles as the device MAC and as the config entry unique_id, so it # needs redacting wherever it surfaces, including inside the UPnP description. @@ -56,43 +64,58 @@ async def async_get_config_entry_diagnostics( ) -> dict[str, Any]: """Return diagnostics for a config entry.""" receiver = config_entry.runtime_data.receiver + volume = receiver.volume + lipsync = receiver.lipsync + zone_b = receiver.zone_b state: dict[str, Any] = { "connected": receiver.connected, "model": receiver.model.name, "power_on": receiver.power_on, - "volume": receiver.volume, + "volume": volume.value if volume is not None else None, "max_volume": receiver.max_volume, - "mute_enabled": receiver.mute_enabled, + "mute_enabled": receiver.muted, "source": receiver.source, - "available_sources": receiver.available_sources, + "available_sources": receiver.sources, "sound_mode": receiver.sound_mode, - "available_sound_modes": receiver.available_sound_modes, + "available_sound_modes": receiver.sound_modes, "audio_input": receiver.audio_input, - "available_audio_inputs": receiver.available_audio_inputs, + "available_audio_inputs": receiver.audio_inputs, "video_input": receiver.video_input, - "available_video_inputs": receiver.available_video_inputs, + "available_video_inputs": receiver.video_inputs, "audio_information": receiver.audio_information, "video_information": receiver.video_information, "streaming_source": receiver.streaming_source, - "available_stream_types": receiver.available_stream_types, + "available_stream_types": receiver.stream_types, "room_perfect_position": receiver.room_perfect_position, - "available_room_perfect_positions": receiver.available_room_perfect_positions, + "available_room_perfect_positions": receiver.room_perfect_positions, "voicing": receiver.voicing, - "available_voicings": receiver.available_voicings, - "lipsync": receiver.lipsync, - "zone_b_power_on": receiver.zone_b_power_on, - "zone_b_volume": receiver.zone_b_volume, - "zone_b_mute_enabled": receiver.zone_b_mute_enabled, - "zone_b_source": receiver.zone_b_source, - "zone_b_audio_input": receiver.zone_b_audio_input, - "zone_b_streaming_source": receiver.zone_b_streaming_source, + "available_voicings": receiver.voicings, + "lipsync": lipsync.value if lipsync is not None else None, + "zone_b_power_on": zone_b.power_on if zone_b is not None else None, + "zone_b_volume": zone_b.volume.value if zone_b is not None else None, + "zone_b_mute_enabled": zone_b.muted if zone_b is not None else None, + "zone_b_source": zone_b.source if zone_b is not None else None, + "zone_b_audio_input": zone_b.audio_input if zone_b is not None else None, + "zone_b_streaming_source": zone_b.streaming_source + if zone_b is not None + else None, + } + trims = {name: receiver.trims.get(trim) for name, trim in _TRIMS.items()} + state |= { + name: control.value if control is not None else None + for name, control in trims.items() } - state |= {name: getattr(receiver, name) for name in _TRIM_NAMES} - ranges = { - name: asdict(value) if (value := getattr(receiver, name)) is not None else None - for name in _RANGE_NAMES + # 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 + } + ranges |= { + f"{name}_range": asdict(control.range) if control is not None else None + for name, control in trims.items() } return async_redact_data( diff --git a/tests/components/lyngdorf/conftest.py b/tests/components/lyngdorf/conftest.py index f00cf05616f0..7b842c8412dd 100644 --- a/tests/components/lyngdorf/conftest.py +++ b/tests/components/lyngdorf/conftest.py @@ -13,6 +13,7 @@ from lyngdorf import ( NumericRange, RemoteKey, Trim, + ZoneB, ) import pytest @@ -150,7 +151,7 @@ def mock_receiver(mock_create_receiver: MagicMock) -> MagicMock: receiver.can_shuffle = False receiver.available_repeat_modes = frozenset() - receiver.lipsync = _FloatControl(50, NumericRange(0, 500, 1)) + receiver.lipsync = _FloatControl(50.0, NumericRange(0, 500, 1)) receiver.lipsync_range = NumericRange(0, 500, 1) receiver.trims = { Trim.BASS: _control(3.0, NumericRange(-12.0, 12.0, 0.1)), @@ -179,6 +180,25 @@ def mock_receiver(mock_create_receiver: MagicMock) -> MagicMock: receiver.zone_b_audio_input = "aux" receiver.zone_b_streaming_source = "DLNA" + receiver.volume = _FloatControl(-40.0, NumericRange(-99.9, 24.0, 0.1)) + receiver.muted = False + receiver.sources = [] + receiver.sound_modes = [] + receiver.audio_inputs = ["optical", "aux"] + receiver.video_inputs = ["hdmi"] + receiver.stream_types = ["AirPlay", "DLNA"] + receiver.room_perfect_positions = ["Global", "Focus 1"] + receiver.voicings = ["Neutral", "Music", "Movie"] + + zone_b = MagicMock(spec=ZoneB) + zone_b.power_on = False + zone_b.muted = False + zone_b.source = None + zone_b.audio_input = "aux" + zone_b.streaming_source = "DLNA" + zone_b.volume = _FloatControl(-40.0, NumericRange(-99.9, 24.0, 0.1)) + receiver.zone_b = zone_b + mock_create_receiver.return_value = receiver return receiver diff --git a/tests/components/lyngdorf/snapshots/test_number.ambr b/tests/components/lyngdorf/snapshots/test_number.ambr index cb9740837e01..5e7ac2526bad 100644 --- a/tests/components/lyngdorf/snapshots/test_number.ambr +++ b/tests/components/lyngdorf/snapshots/test_number.ambr @@ -57,7 +57,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '50', + 'state': '50.0', }) # --- # name: test_entities[number.mock_lyngdorf_trim_bass-entry] diff --git a/tests/components/lyngdorf/test_diagnostics.py b/tests/components/lyngdorf/test_diagnostics.py index dd624b59b349..9ae84b3919e2 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 patch +from unittest.mock import MagicMock, patch import pytest from syrupy.assertion import SnapshotAssertion @@ -34,6 +34,23 @@ 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,