Report an available source for LG soundbars (#178799)

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Maxim Kolokolnikov
2026-08-14 14:10:13 +02:00
committed by GitHub
co-authored by Joost Lekkerkerker
parent ae288ad94d
commit 1bb7e60aeb
4 changed files with 209 additions and 1 deletions
@@ -17,6 +17,22 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
EQUIVALENT_FUNCTIONS = (
("Optical/HDMI ARC", "E-ARC", "ARC", "LG Optical", "Optical", "Optical2"),
("HDMI", "HDMI2", "HDMI3"),
("USB", "USB2"),
("Bluetooth", "Portable"),
("Wi-Fi", "Chromecast", "Spotify"),
)
def _offered_equivalent(function: str, offered: list[int]) -> str | None:
"""Return an offered function from the same group as the given one."""
group = next((names for names in EQUIVALENT_FUNCTIONS if function in names), ())
return next(
(name for name in group if temescal.functions.index(name) in offered), None
)
async def async_setup_entry(
hass: HomeAssistant,
@@ -240,7 +256,10 @@ class LGDevice(MediaPlayerEntity):
"""Return the current input source."""
if self._function == -1 or self._function >= len(temescal.functions):
return None
return temescal.functions[self._function]
function = temescal.functions[self._function]
if self._function in self._functions:
return function
return _offered_equivalent(function, self._functions) or function
@property
@override
+23
View File
@@ -1 +1,24 @@
"""Tests for the lg_soundbar component."""
from collections.abc import Callable
from typing import Any
from unittest.mock import MagicMock
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
"""Set up the component."""
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
def find_update_callback(
mock: MagicMock,
) -> Callable[[dict[str, Any]], None]:
"""Return the callback registered with the temescal device."""
return mock.call_args.kwargs["callback"]
+42
View File
@@ -0,0 +1,42 @@
"""Common fixtures for the lg_soundbar tests."""
from collections.abc import Generator
from unittest.mock import MagicMock, patch
import pytest
from homeassistant.components.lg_soundbar.const import DEFAULT_PORT, DOMAIN
from homeassistant.const import CONF_HOST, CONF_PORT
from tests.common import MockConfigEntry
@pytest.fixture
def mock_config_entry() -> MockConfigEntry:
"""Return the default mocked config entry."""
return MockConfigEntry(
domain=DOMAIN,
title="LG Soundbar",
data={CONF_HOST: "127.0.0.1", CONF_PORT: DEFAULT_PORT},
unique_id="uuid",
)
@pytest.fixture
def mock_temescal() -> Generator[MagicMock]:
"""Mock the temescal library.
Only the device constructor is mocked so that the real ``functions`` and
``equalisers`` lookup tables remain available to the media player.
"""
with (
patch(
"homeassistant.components.lg_soundbar.media_player.temescal.temescal",
autospec=True,
) as mock_temescal,
patch(
"homeassistant.components.lg_soundbar.test_connect",
return_value={"name": "LG Soundbar", "uuid": "uuid"},
),
):
yield mock_temescal
@@ -0,0 +1,124 @@
"""Test the lg_soundbar media player."""
from unittest.mock import MagicMock
import temescal
from homeassistant.components.media_player import ATTR_INPUT_SOURCE_LIST
from homeassistant.core import HomeAssistant
from . import find_update_callback, setup_integration
from tests.common import MockConfigEntry
ENTITY_ID = "media_player.127_0_0_1"
AVAILABLE_FUNCTIONS = [
temescal.functions.index("Wi-Fi"),
temescal.functions.index("Bluetooth"),
temescal.functions.index("Optical/HDMI ARC"),
temescal.functions.index("HDMI"),
temescal.functions.index("USB2"),
]
def send_func_view_info(callback: MagicMock, current_function: str) -> None:
"""Report the given function as the current one via the callback."""
callback(
{
"msg": "FUNC_VIEW_INFO",
"data": {
"i_curr_func": temescal.functions.index(current_function),
"ai_func_list": AVAILABLE_FUNCTIONS,
},
}
)
async def test_source_reported_as_is_when_available(
hass: HomeAssistant,
mock_temescal: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test that a function offered by the device is reported unchanged."""
await setup_integration(hass, mock_config_entry)
send_func_view_info(find_update_callback(mock_temescal), "Bluetooth")
await hass.async_block_till_done()
assert hass.states.get(ENTITY_ID).attributes["source"] == "Bluetooth"
async def test_source_falls_back_to_available_equivalent(
hass: HomeAssistant,
mock_temescal: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test that a function missing from the list falls back to an equivalent one.
Soundbars report states such as E-ARC or HDMI3 that they never offer in
ai_func_list, which leaves the current source outside of source_list.
"""
await setup_integration(hass, mock_config_entry)
callback = find_update_callback(mock_temescal)
send_func_view_info(callback, "E-ARC")
await hass.async_block_till_done()
assert hass.states.get(ENTITY_ID).attributes["source"] == "Optical/HDMI ARC"
send_func_view_info(callback, "ARC")
await hass.async_block_till_done()
assert hass.states.get(ENTITY_ID).attributes["source"] == "Optical/HDMI ARC"
send_func_view_info(callback, "HDMI3")
await hass.async_block_till_done()
assert hass.states.get(ENTITY_ID).attributes["source"] == "HDMI"
send_func_view_info(callback, "USB")
await hass.async_block_till_done()
assert hass.states.get(ENTITY_ID).attributes["source"] == "USB2"
async def test_source_kept_when_no_equivalent_is_available(
hass: HomeAssistant,
mock_temescal: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test that a function without an available equivalent is reported as is."""
await setup_integration(hass, mock_config_entry)
send_func_view_info(find_update_callback(mock_temescal), "Aux")
await hass.async_block_till_done()
assert hass.states.get(ENTITY_ID).attributes["source"] == "Aux"
async def test_source_list_only_contains_offered_functions(
hass: HomeAssistant,
mock_temescal: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test that the source list is not extended by the fallback."""
await setup_integration(hass, mock_config_entry)
send_func_view_info(find_update_callback(mock_temescal), "E-ARC")
await hass.async_block_till_done()
assert hass.states.get(ENTITY_ID).attributes[ATTR_INPUT_SOURCE_LIST] == [
"Bluetooth",
"HDMI",
"Optical/HDMI ARC",
"USB2",
"Wi-Fi",
]
async def test_source_unknown_before_any_response(
hass: HomeAssistant,
mock_temescal: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test that no source is reported before the device answers."""
await setup_integration(hass, mock_config_entry)
assert "source" not in hass.states.get(ENTITY_ID).attributes