mirror of
https://github.com/home-assistant/core.git
synced 2026-09-25 07:51:46 -05:00
Keep LG soundbar modes the library cannot name (#180369)
This commit is contained in:
@@ -26,6 +26,37 @@ EQUIVALENT_FUNCTIONS = (
|
||||
)
|
||||
|
||||
|
||||
def _display_name(names: list[str], index: int) -> str:
|
||||
"""Return the display name for an index.
|
||||
|
||||
The temescal name tables stopped being maintained when the library was
|
||||
archived in 2023, so newer models report indices past the end of them.
|
||||
Naming those keeps them selectable instead of silently dropping them.
|
||||
"""
|
||||
if index < len(names):
|
||||
return names[index]
|
||||
|
||||
return f"Unknown ({index})"
|
||||
|
||||
|
||||
def _offered_names(names: list[str], offered: list[int]) -> list[str]:
|
||||
"""Return the display names of the offered indices."""
|
||||
return sorted(_display_name(names, index) for index in offered)
|
||||
|
||||
|
||||
def _index_for_name(names: list[str], offered: list[int], name: str) -> int:
|
||||
"""Return the index a display name refers to.
|
||||
|
||||
Indices the library cannot name are only known through the offered list, so
|
||||
they are resolved from there; anything else resolves against the library.
|
||||
"""
|
||||
for index in offered:
|
||||
if _display_name(names, index) == name:
|
||||
return index
|
||||
|
||||
return names.index(name)
|
||||
|
||||
|
||||
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), ())
|
||||
@@ -236,27 +267,23 @@ class LGDevice(MediaPlayerEntity):
|
||||
@override
|
||||
def sound_mode(self):
|
||||
"""Return the current sound mode."""
|
||||
if self._equaliser == -1 or self._equaliser >= len(temescal.equalisers):
|
||||
if self._equaliser == -1:
|
||||
return None
|
||||
return temescal.equalisers[self._equaliser]
|
||||
return _display_name(temescal.equalisers, self._equaliser)
|
||||
|
||||
@property
|
||||
@override
|
||||
def sound_mode_list(self):
|
||||
"""Return the available sound modes."""
|
||||
return sorted(
|
||||
temescal.equalisers[equaliser]
|
||||
for equaliser in self._equalisers
|
||||
if equaliser < len(temescal.equalisers)
|
||||
)
|
||||
return _offered_names(temescal.equalisers, self._equalisers)
|
||||
|
||||
@property
|
||||
@override
|
||||
def source(self):
|
||||
"""Return the current input source."""
|
||||
if self._function == -1 or self._function >= len(temescal.functions):
|
||||
if self._function == -1:
|
||||
return None
|
||||
function = temescal.functions[self._function]
|
||||
function = _display_name(temescal.functions, self._function)
|
||||
if self._function in self._functions:
|
||||
return function
|
||||
return _offered_equivalent(function, self._functions) or function
|
||||
@@ -265,11 +292,7 @@ class LGDevice(MediaPlayerEntity):
|
||||
@override
|
||||
def source_list(self):
|
||||
"""List of available input sources."""
|
||||
return sorted(
|
||||
temescal.functions[function]
|
||||
for function in self._functions
|
||||
if function < len(temescal.functions)
|
||||
)
|
||||
return _offered_names(temescal.functions, self._functions)
|
||||
|
||||
@override
|
||||
def set_volume_level(self, volume: float) -> None:
|
||||
@@ -285,12 +308,16 @@ class LGDevice(MediaPlayerEntity):
|
||||
@override
|
||||
def select_source(self, source: str) -> None:
|
||||
"""Select input source."""
|
||||
self._device.set_func(temescal.functions.index(source))
|
||||
self._device.set_func(
|
||||
_index_for_name(temescal.functions, self._functions, source)
|
||||
)
|
||||
|
||||
@override
|
||||
def select_sound_mode(self, sound_mode: str) -> None:
|
||||
"""Set Sound Mode for Receiver.."""
|
||||
self._device.set_eq(temescal.equalisers.index(sound_mode))
|
||||
self._device.set_eq(
|
||||
_index_for_name(temescal.equalisers, self._equalisers, sound_mode)
|
||||
)
|
||||
|
||||
@override
|
||||
def turn_on(self) -> None:
|
||||
|
||||
@@ -4,7 +4,16 @@ from unittest.mock import MagicMock
|
||||
|
||||
import temescal
|
||||
|
||||
from homeassistant.components.media_player import ATTR_INPUT_SOURCE_LIST
|
||||
from homeassistant.components.media_player import (
|
||||
ATTR_INPUT_SOURCE,
|
||||
ATTR_INPUT_SOURCE_LIST,
|
||||
ATTR_SOUND_MODE,
|
||||
ATTR_SOUND_MODE_LIST,
|
||||
DOMAIN as MEDIA_PLAYER_DOMAIN,
|
||||
SERVICE_SELECT_SOUND_MODE,
|
||||
SERVICE_SELECT_SOURCE,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import find_update_callback, setup_integration
|
||||
@@ -122,3 +131,119 @@ async def test_source_unknown_before_any_response(
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert "source" not in hass.states.get(ENTITY_ID).attributes
|
||||
|
||||
|
||||
# An SG10TY reports these alongside indices temescal knows about. They sit past
|
||||
# the end of the library's table, which has been read-only since 2023.
|
||||
UNMAPPED_EQUALISERS = [23, 24, 25, 26]
|
||||
|
||||
AVAILABLE_EQUALISERS = [
|
||||
temescal.equalisers.index("Standard"),
|
||||
temescal.equalisers.index("Cinema"),
|
||||
*UNMAPPED_EQUALISERS,
|
||||
]
|
||||
|
||||
|
||||
def send_eq_view_info(callback: MagicMock, current_equaliser: int) -> None:
|
||||
"""Report the given equaliser as the current one via the callback."""
|
||||
callback(
|
||||
{
|
||||
"msg": "EQ_VIEW_INFO",
|
||||
"data": {
|
||||
"i_curr_eq": current_equaliser,
|
||||
"ai_eq_list": AVAILABLE_EQUALISERS,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def test_sound_modes_beyond_the_library_table_are_offered(
|
||||
hass: HomeAssistant,
|
||||
mock_temescal: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test that equalisers the library cannot name are still offered."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
send_eq_view_info(find_update_callback(mock_temescal), 23)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (state := hass.states.get(ENTITY_ID))
|
||||
assert state.attributes[ATTR_SOUND_MODE_LIST] == [
|
||||
"Cinema",
|
||||
"Standard",
|
||||
"Unknown (23)",
|
||||
"Unknown (24)",
|
||||
"Unknown (25)",
|
||||
"Unknown (26)",
|
||||
]
|
||||
assert state.attributes[ATTR_SOUND_MODE] == "Unknown (23)"
|
||||
|
||||
|
||||
async def test_selecting_a_sound_mode_beyond_the_library_table(
|
||||
hass: HomeAssistant,
|
||||
mock_temescal: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test that an equaliser the library cannot name can be selected."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
send_eq_view_info(find_update_callback(mock_temescal), 0)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.services.async_call(
|
||||
MEDIA_PLAYER_DOMAIN,
|
||||
SERVICE_SELECT_SOUND_MODE,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_SOUND_MODE: "Unknown (25)"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_temescal.return_value.set_eq.assert_called_once_with(25)
|
||||
|
||||
|
||||
async def test_selecting_a_named_sound_mode(
|
||||
hass: HomeAssistant,
|
||||
mock_temescal: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test that an equaliser the library can name is still selectable."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
send_eq_view_info(find_update_callback(mock_temescal), 0)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
await hass.services.async_call(
|
||||
MEDIA_PLAYER_DOMAIN,
|
||||
SERVICE_SELECT_SOUND_MODE,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_SOUND_MODE: "Cinema"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_temescal.return_value.set_eq.assert_called_once_with(
|
||||
temescal.equalisers.index("Cinema")
|
||||
)
|
||||
|
||||
|
||||
async def test_selecting_a_source_the_device_does_not_offer(
|
||||
hass: HomeAssistant,
|
||||
mock_temescal: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test that a known function outside of ai_func_list stays selectable."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
send_func_view_info(find_update_callback(mock_temescal), "Bluetooth")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert "E-ARC" not in hass.states.get(ENTITY_ID).attributes[ATTR_INPUT_SOURCE_LIST]
|
||||
|
||||
await hass.services.async_call(
|
||||
MEDIA_PLAYER_DOMAIN,
|
||||
SERVICE_SELECT_SOURCE,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_INPUT_SOURCE: "E-ARC"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_temescal.return_value.set_func.assert_called_once_with(
|
||||
temescal.functions.index("E-ARC")
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user