diff --git a/homeassistant/components/smartthings/media_player.py b/homeassistant/components/smartthings/media_player.py index 07414452893..07746a64b15 100644 --- a/homeassistant/components/smartthings/media_player.py +++ b/homeassistant/components/smartthings/media_player.py @@ -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: diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index 78a34e0339e..cf4653c55cb 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -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" diff --git a/tests/components/smartthings/fixtures/device_status/vd_stv_2017_k.json b/tests/components/smartthings/fixtures/device_status/vd_stv_2017_k.json index 18496942e2f..4caa839e2dd 100644 --- a/tests/components/smartthings/fixtures/device_status/vd_stv_2017_k.json +++ b/tests/components/smartthings/fixtures/device_status/vd_stv_2017_k.json @@ -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": { diff --git a/tests/components/smartthings/snapshots/test_media_player.ambr b/tests/components/smartthings/snapshots/test_media_player.ambr index fe70141ee16..9ca876fba95 100644 --- a/tests/components/smartthings/snapshots/test_media_player.ambr +++ b/tests/components/smartthings/snapshots/test_media_player.ambr @@ -32,7 +32,7 @@ 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': , - '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': , - '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': , @@ -159,7 +159,7 @@ 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': , - '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': , - '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': , - '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': , 'volume_level': 0.17, }), @@ -331,7 +331,7 @@ 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': , - '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': , @@ -390,7 +390,7 @@ 'previous_unique_id': None, 'suggested_object_id': None, 'supported_features': , - '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': , 'volume_level': 0.13, diff --git a/tests/components/smartthings/test_media_player.py b/tests/components/smartthings/test_media_player.py index 0fb53e642d4..2a848df8250 100644 --- a/tests/components/smartthings/test_media_player.py +++ b/tests/components/smartthings/test_media_player.py @@ -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"),