1
0
mirror of https://github.com/home-assistant/core.git synced 2026-05-08 17:49:37 +01:00

Allow selecting input source on SmartThings TVs (#160034)

Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
This commit is contained in:
Felipe Santos
2026-05-07 14:53:38 -03:00
committed by GitHub
parent 046b48df43
commit ef83ccc423
5 changed files with 250 additions and 47 deletions
@@ -18,6 +18,26 @@ from . import FullDevice, SmartThingsConfigEntry
from .const import MAIN
from .entity import SmartThingsEntity
MEDIA_SOURCE_ID_TO_HA_KEY: dict[str, str] = {
"AM": "am",
"BT": "bluetooth",
"CD": "cd",
"D.IN": "digital_input",
"HDMI": "hdmi",
"HDMI1": "hdmi1",
"HDMI2": "hdmi2",
"HDMI3": "hdmi3",
"HDMI4": "hdmi4",
"HDMI5": "hdmi5",
"HDMI6": "hdmi6",
"USB": "usb",
"WIFI": "wifi",
"digitalTv": "digital_tv",
"dtv": "digital_tv",
"melon": "melon",
"youtube": "youtube",
}
MEDIA_PLAYER_CAPABILITIES = (
Capability.AUDIO_MUTE,
Capability.AUDIO_VOLUME,
@@ -72,6 +92,7 @@ class SmartThingsMediaPlayer(SmartThingsEntity, MediaPlayerEntity):
"""Define a SmartThings media player."""
_attr_name = None
_attr_translation_key = "media_player"
def __init__(self, client: SmartThings, device: FullDevice) -> None:
"""Initialize the media_player class."""
@@ -87,6 +108,7 @@ class SmartThingsMediaPlayer(SmartThingsEntity, MediaPlayerEntity):
Capability.MEDIA_PLAYBACK_REPEAT,
Capability.MEDIA_PLAYBACK_SHUFFLE,
Capability.SAMSUNG_VD_AUDIO_INPUT_SOURCE,
Capability.SAMSUNG_VD_MEDIA_INPUT_SOURCE,
Capability.SWITCH,
},
)
@@ -95,6 +117,43 @@ class SmartThingsMediaPlayer(SmartThingsEntity, MediaPlayerEntity):
device.device.components[MAIN].user_category
or device.device.components[MAIN].manufacturer_category,
)
self._source_to_smartthings_id: dict[str, str] = {}
def _update_attr(self) -> None:
"""Update the attributes."""
self._build_source_map()
def _build_source_map(self) -> None:
"""Build the source mapping from HA key to SmartThings ID."""
raw_sources = self._get_raw_source_list()
if not raw_sources:
self._source_to_smartthings_id = {}
return
self._source_to_smartthings_id = {
MEDIA_SOURCE_ID_TO_HA_KEY.get(source_id, source_id): source_id
for source_id in raw_sources
}
def _get_raw_source_list(self) -> list[str] | None:
"""Get the raw source list from the device."""
if self.supports_capability(Capability.SAMSUNG_VD_MEDIA_INPUT_SOURCE):
sources_map = self.get_attribute_value(
Capability.SAMSUNG_VD_MEDIA_INPUT_SOURCE,
Attribute.SUPPORTED_INPUT_SOURCES_MAP,
)
if not sources_map:
return None
return [source["id"] for source in sources_map]
if self.supports_capability(Capability.MEDIA_INPUT_SOURCE):
return self.get_attribute_value(
Capability.MEDIA_INPUT_SOURCE, Attribute.SUPPORTED_INPUT_SOURCES
)
if self.supports_capability(Capability.SAMSUNG_VD_AUDIO_INPUT_SOURCE):
return self.get_attribute_value(
Capability.SAMSUNG_VD_AUDIO_INPUT_SOURCE,
Attribute.SUPPORTED_INPUT_SOURCES,
)
return None
def _determine_features(self) -> MediaPlayerEntityFeature:
flags = (
@@ -120,7 +179,9 @@ class SmartThingsMediaPlayer(SmartThingsEntity, MediaPlayerEntity):
flags |= (
MediaPlayerEntityFeature.TURN_ON | MediaPlayerEntityFeature.TURN_OFF
)
if self.supports_capability(Capability.MEDIA_INPUT_SOURCE):
if self.supports_capability(
Capability.SAMSUNG_VD_MEDIA_INPUT_SOURCE
) or self.supports_capability(Capability.MEDIA_INPUT_SOURCE):
flags |= MediaPlayerEntityFeature.SELECT_SOURCE
if self.supports_capability(Capability.MEDIA_PLAYBACK_SHUFFLE):
flags |= MediaPlayerEntityFeature.SHUFFLE_SET
@@ -209,11 +270,19 @@ class SmartThingsMediaPlayer(SmartThingsEntity, MediaPlayerEntity):
async def async_select_source(self, source: str) -> None:
"""Select source."""
await self.execute_device_command(
Capability.MEDIA_INPUT_SOURCE,
Command.SET_INPUT_SOURCE,
argument=source,
)
smartthings_source = self._source_to_smartthings_id.get(source, source)
if self.supports_capability(Capability.SAMSUNG_VD_MEDIA_INPUT_SOURCE):
await self.execute_device_command(
Capability.SAMSUNG_VD_MEDIA_INPUT_SOURCE,
Command.SET_INPUT_SOURCE,
argument=smartthings_source,
)
else:
await self.execute_device_command(
Capability.MEDIA_INPUT_SOURCE,
Command.SET_INPUT_SOURCE,
argument=smartthings_source,
)
async def async_set_shuffle(self, shuffle: bool) -> None:
"""Set shuffle mode."""
@@ -309,29 +378,30 @@ class SmartThingsMediaPlayer(SmartThingsEntity, MediaPlayerEntity):
@property
def source(self) -> str | None:
"""Input source."""
if self.supports_capability(Capability.MEDIA_INPUT_SOURCE):
return self.get_attribute_value(
if self.supports_capability(Capability.SAMSUNG_VD_MEDIA_INPUT_SOURCE):
raw = self.get_attribute_value(
Capability.SAMSUNG_VD_MEDIA_INPUT_SOURCE, Attribute.INPUT_SOURCE
)
elif self.supports_capability(Capability.MEDIA_INPUT_SOURCE):
raw = self.get_attribute_value(
Capability.MEDIA_INPUT_SOURCE, Attribute.INPUT_SOURCE
)
if self.supports_capability(Capability.SAMSUNG_VD_AUDIO_INPUT_SOURCE):
return self.get_attribute_value(
elif self.supports_capability(Capability.SAMSUNG_VD_AUDIO_INPUT_SOURCE):
raw = self.get_attribute_value(
Capability.SAMSUNG_VD_AUDIO_INPUT_SOURCE, Attribute.INPUT_SOURCE
)
return None
else:
raw = None
if raw is None:
return None
return MEDIA_SOURCE_ID_TO_HA_KEY.get(raw, raw)
@property
def source_list(self) -> list[str] | None:
"""List of input sources."""
if self.supports_capability(Capability.MEDIA_INPUT_SOURCE):
return self.get_attribute_value(
Capability.MEDIA_INPUT_SOURCE, Attribute.SUPPORTED_INPUT_SOURCES
)
if self.supports_capability(Capability.SAMSUNG_VD_AUDIO_INPUT_SOURCE):
return self.get_attribute_value(
Capability.SAMSUNG_VD_AUDIO_INPUT_SOURCE,
Attribute.SUPPORTED_INPUT_SOURCES,
)
return None
if not self._source_to_smartthings_id:
return None
return list(self._source_to_smartthings_id)
@property
def shuffle(self) -> bool | None:
@@ -197,6 +197,42 @@
"name": "[%key:component::light::title%]"
}
},
"media_player": {
"media_player": {
"state_attributes": {
"source": {
"state": {
"am": "AM",
"analog1": "Analog 1",
"analog2": "Analog 2",
"analog3": "Analog 3",
"aux": "AUX",
"bluetooth": "Bluetooth",
"cd": "CD",
"coaxial": "Coaxial",
"digital": "Digital",
"digital_input": "Digital input",
"digital_tv": "Digital TV",
"fm": "FM",
"hdmi": "HDMI",
"hdmi1": "HDMI 1",
"hdmi2": "HDMI 2",
"hdmi3": "HDMI 3",
"hdmi4": "HDMI 4",
"hdmi5": "HDMI 5",
"hdmi6": "HDMI 6",
"melon": "Melon",
"network": "Network",
"optical": "Optical",
"phono": "Phono",
"usb": "USB",
"wifi": "Wi-Fi",
"youtube": "YouTube"
}
}
}
}
},
"number": {
"cool_select_plus_temperature": {
"name": "CoolSelect+ temperature"
@@ -36,11 +36,11 @@
"name": "PlayStation 4"
},
{
"id": "HDMI4",
"id": "HDMI2",
"name": "HT-CT370"
},
{
"id": "HDMI4",
"id": "HDMI3",
"name": "HT-CT370"
}
],
@@ -53,7 +53,7 @@
},
"mediaInputSource": {
"supportedInputSources": {
"value": ["digitalTv", "HDMI1", "HDMI4", "HDMI4"],
"value": ["digitalTv", "HDMI1", "HDMI2", "HDMI3"],
"timestamp": "2021-10-16T15:18:11.622Z"
},
"inputSource": {
@@ -32,7 +32,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <MediaPlayerEntityFeature: 284045>,
'translation_key': None,
'translation_key': 'media_player',
'unique_id': '01b28624-5907-c8bc-0325-8ad23f03a637_main',
'unit_of_measurement': None,
})
@@ -64,8 +64,8 @@
'source_list': list([
'wifi',
'bluetooth',
'HDMI1',
'HDMI2',
'hdmi1',
'hdmi2',
'digital',
]),
}),
@@ -94,7 +94,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <MediaPlayerEntityFeature: 23949>,
'translation_key': None,
'translation_key': 'media_player',
'unique_id': 'afcf3b91-0000-1111-2222-ddff2a0a6577_main',
'unit_of_measurement': None,
})
@@ -111,8 +111,8 @@
'source_list': list([
'wifi',
'bluetooth',
'HDMI1',
'HDMI2',
'hdmi1',
'hdmi2',
'digital',
]),
'supported_features': <MediaPlayerEntityFeature: 23949>,
@@ -159,7 +159,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <MediaPlayerEntityFeature: 318477>,
'translation_key': None,
'translation_key': 'media_player',
'unique_id': 'c9276e43-fe3c-88c3-1dcc-2eb79e292b8c_main',
'unit_of_measurement': None,
})
@@ -216,7 +216,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <MediaPlayerEntityFeature: 21517>,
'translation_key': None,
'translation_key': 'media_player',
'unique_id': 'c85fced9-c474-4a47-93c2-037cc7829536_main',
'unit_of_measurement': None,
})
@@ -273,7 +273,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <MediaPlayerEntityFeature: 21901>,
'translation_key': None,
'translation_key': 'media_player',
'unique_id': '0d94e5db-8501-2355-eb4f-214163702cac_main',
'unit_of_measurement': None,
})
@@ -286,7 +286,7 @@
'is_volume_muted': False,
'media_artist': '',
'media_title': '',
'source': 'HDMI1',
'source': 'hdmi1',
'supported_features': <MediaPlayerEntityFeature: 21901>,
'volume_level': 0.17,
}),
@@ -331,7 +331,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <MediaPlayerEntityFeature: 1420>,
'translation_key': None,
'translation_key': 'media_player',
'unique_id': 'a75cb1e1-03fd-3c77-ca9f-d4e56c4096c6_main',
'unit_of_measurement': None,
})
@@ -359,10 +359,10 @@
'area_id': None,
'capabilities': dict({
'source_list': list([
'digitalTv',
'HDMI1',
'HDMI4',
'HDMI4',
'digital_tv',
'hdmi1',
'hdmi2',
'hdmi3',
]),
}),
'config_entry_id': <ANY>,
@@ -390,7 +390,7 @@
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <MediaPlayerEntityFeature: 23997>,
'translation_key': None,
'translation_key': 'media_player',
'unique_id': '4588d2d9-a8cf-40f4-9a0b-ed5dfbaccda1_main',
'unit_of_measurement': None,
})
@@ -401,12 +401,12 @@
'device_class': 'tv',
'friendly_name': '[TV] Samsung 8 Series (49)',
'is_volume_muted': True,
'source': 'HDMI1',
'source': 'hdmi1',
'source_list': list([
'digitalTv',
'HDMI1',
'HDMI4',
'HDMI4',
'digital_tv',
'hdmi1',
'hdmi2',
'hdmi3',
]),
'supported_features': <MediaPlayerEntityFeature: 23997>,
'volume_level': 0.13,
@@ -15,11 +15,13 @@ from homeassistant.components.media_player import (
ATTR_MEDIA_VOLUME_MUTED,
DOMAIN as MEDIA_PLAYER_DOMAIN,
SERVICE_SELECT_SOURCE,
MediaPlayerEntityFeature,
RepeatMode,
)
from homeassistant.components.smartthings.const import MAIN
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_SUPPORTED_FEATURES,
SERVICE_MEDIA_NEXT_TRACK,
SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_PLAY,
@@ -325,7 +327,7 @@ async def test_select_source(
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test media player stop command."""
"""Test media player select source command."""
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
@@ -339,10 +341,105 @@ async def test_select_source(
Capability.MEDIA_INPUT_SOURCE,
Command.SET_INPUT_SOURCE,
MAIN,
"digital",
argument="digital",
)
@pytest.mark.parametrize("device_fixture", ["vd_stv_2017_k"])
async def test_vd_capability_select_source(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test media player select source command using Samsung VD capability."""
await setup_integration(hass, mock_config_entry)
state = hass.states.get("media_player.tv_samsung_8_series_49")
assert state is not None
assert MediaPlayerEntityFeature.SELECT_SOURCE in MediaPlayerEntityFeature(
state.attributes[ATTR_SUPPORTED_FEATURES]
)
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_SELECT_SOURCE,
{
ATTR_ENTITY_ID: "media_player.tv_samsung_8_series_49",
ATTR_INPUT_SOURCE: "hdmi1",
},
blocking=True,
)
devices.execute_device_command.assert_called_once_with(
"4588d2d9-a8cf-40f4-9a0b-ed5dfbaccda1",
Capability.SAMSUNG_VD_MEDIA_INPUT_SOURCE,
Command.SET_INPUT_SOURCE,
MAIN,
argument="HDMI1",
)
@pytest.mark.parametrize("device_fixture", ["vd_stv_2017_k"])
async def test_select_source_legacy_raw_id(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test select source falls back to raw ID when not found in source map.
When a legacy/raw source ID (e.g. 'HDMI1') is passed directly instead of the
slugified HA name ('hdmi1'), it should be forwarded as-is to SmartThings.
"""
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_SELECT_SOURCE,
{
ATTR_ENTITY_ID: "media_player.tv_samsung_8_series_49",
ATTR_INPUT_SOURCE: "HDMI1",
},
blocking=True,
)
devices.execute_device_command.assert_called_once_with(
"4588d2d9-a8cf-40f4-9a0b-ed5dfbaccda1",
Capability.SAMSUNG_VD_MEDIA_INPUT_SOURCE,
Command.SET_INPUT_SOURCE,
MAIN,
argument="HDMI1",
)
@pytest.mark.parametrize("device_fixture", ["vd_stv_2017_k"])
async def test_vd_capability_source_update(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test source state update using Samsung VD capability."""
await setup_integration(hass, mock_config_entry)
state = hass.states.get("media_player.tv_samsung_8_series_49")
assert state is not None
assert MediaPlayerEntityFeature.SELECT_SOURCE in MediaPlayerEntityFeature(
state.attributes[ATTR_SUPPORTED_FEATURES]
)
assert state.attributes[ATTR_INPUT_SOURCE] == "hdmi1"
# Update source to dtv
await trigger_update(
hass,
devices,
"4588d2d9-a8cf-40f4-9a0b-ed5dfbaccda1",
Capability.SAMSUNG_VD_MEDIA_INPUT_SOURCE,
Attribute.INPUT_SOURCE,
"dtv",
)
state = hass.states.get("media_player.tv_samsung_8_series_49")
assert state is not None
assert state.attributes[ATTR_INPUT_SOURCE] == "digital_tv"
@pytest.mark.parametrize("device_fixture", ["hw_q80r_soundbar"])
@pytest.mark.parametrize(
("shuffle", "argument"),