From 20485cbf881fc378a0e361767f8985f6b8391a1d Mon Sep 17 00:00:00 2001 From: "Barry vd. Heuvel" Date: Fri, 11 Sep 2026 16:22:32 +0200 Subject: [PATCH] Add Smartthings soundmode using networkaudio (#168698) --- .../components/smartthings/media_player.py | 36 ++++++++++++ .../components/smartthings/strings.json | 8 +++ .../snapshots/test_media_player.ambr | 16 +++++- .../smartthings/test_media_player.py | 57 +++++++++++++++++++ 4 files changed, 115 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/smartthings/media_player.py b/homeassistant/components/smartthings/media_player.py index 28d5221c6b42..8a4e70f9ebf7 100644 --- a/homeassistant/components/smartthings/media_player.py +++ b/homeassistant/components/smartthings/media_player.py @@ -70,6 +70,17 @@ REPEAT_MODE_TO_HA = { HA_REPEAT_MODE_TO_SMARTTHINGS = {v: k for k, v in REPEAT_MODE_TO_HA.items()} +NETWORK_AUDIO_VENDOR_IDS = ["VD-NetworkAudio-002S"] + +SOUND_MODE_TO_HA = { + "standard": "standard", + "surround": "surround", + "game": "game", + "adaptive sound": "adaptive_sound", +} + +HA_TO_SOUND_MODE = {v: k for k, v in SOUND_MODE_TO_HA.items()} + async def async_setup_entry( hass: HomeAssistant, @@ -104,6 +115,7 @@ class SmartThingsMediaPlayer(SmartThingsEntity, MediaPlayerEntity): Capability.AUDIO_MUTE, Capability.AUDIO_TRACK_DATA, Capability.AUDIO_VOLUME, + Capability.EXECUTE, Capability.MEDIA_INPUT_SOURCE, Capability.MEDIA_PLAYBACK, Capability.MEDIA_PLAYBACK_REPEAT, @@ -120,6 +132,9 @@ class SmartThingsMediaPlayer(SmartThingsEntity, MediaPlayerEntity): ) self._source_to_smartthings_id: dict[str, str] = {} + if self._supports_samsung_network_audio_sound_mode(): + self._attr_sound_mode_list = list(SOUND_MODE_TO_HA.values()) + @override def _update_attr(self) -> None: """Update the attributes.""" @@ -157,6 +172,11 @@ class SmartThingsMediaPlayer(SmartThingsEntity, MediaPlayerEntity): ) return None + def _supports_samsung_network_audio_sound_mode(self) -> bool: + """Return True if the device is a Samsung network audio soundbar.""" + ocf = self.device.device.ocf + return ocf is not None and ocf.vendor_id in NETWORK_AUDIO_VENDOR_IDS + def _determine_features(self) -> MediaPlayerEntityFeature: flags = ( MediaPlayerEntityFeature.VOLUME_SET @@ -189,6 +209,8 @@ class SmartThingsMediaPlayer(SmartThingsEntity, MediaPlayerEntity): flags |= MediaPlayerEntityFeature.SHUFFLE_SET if self.supports_capability(Capability.MEDIA_PLAYBACK_REPEAT): flags |= MediaPlayerEntityFeature.REPEAT_SET + if self._supports_samsung_network_audio_sound_mode(): + flags |= MediaPlayerEntityFeature.SELECT_SOUND_MODE return flags @override @@ -316,6 +338,20 @@ class SmartThingsMediaPlayer(SmartThingsEntity, MediaPlayerEntity): argument=HA_REPEAT_MODE_TO_SMARTTHINGS[repeat], ) + @override + async def async_select_sound_mode(self, sound_mode: str) -> None: + """Select sound mode.""" + await self.execute_device_command( + Capability.EXECUTE, + Command.EXECUTE, + argument=[ + "/sec/networkaudio/soundmode", + {"x.com.samsung.networkaudio.soundmode": HA_TO_SOUND_MODE[sound_mode]}, + ], + ) + self._attr_sound_mode = sound_mode + self.async_write_ha_state() + @property @override def media_title(self) -> str | None: diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index d2250780190c..648ad48ad1ed 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -193,6 +193,14 @@ "media_player": { "media_player": { "state_attributes": { + "sound_mode": { + "state": { + "adaptive_sound": "Adaptive sound", + "game": "Game", + "standard": "Standard", + "surround": "Surround" + } + }, "source": { "state": { "am": "AM", diff --git a/tests/components/smartthings/snapshots/test_media_player.ambr b/tests/components/smartthings/snapshots/test_media_player.ambr index 00abfab3498d..a64e3132a383 100644 --- a/tests/components/smartthings/snapshots/test_media_player.ambr +++ b/tests/components/smartthings/snapshots/test_media_player.ambr @@ -247,6 +247,12 @@ ]), 'area_id': None, 'capabilities': dict({ + : list([ + 'standard', + 'surround', + 'game', + 'adaptive_sound', + ]), }), 'config_entry_id': , 'config_subentry_id': , @@ -272,7 +278,7 @@ 'platform': 'smartthings', 'previous_unique_id': None, 'suggested_object_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': 'media_player', 'unique_id': '0d94e5db-8501-2355-eb4f-214163702cac_main', 'unit_of_measurement': None, @@ -286,8 +292,14 @@ : False, : '', : '', + : list([ + 'standard', + 'surround', + 'game', + 'adaptive_sound', + ]), : 'hdmi1', - : , + : , : 0.17, }), 'context': , diff --git a/tests/components/smartthings/test_media_player.py b/tests/components/smartthings/test_media_player.py index d2d7613a88a4..287d51a891e5 100644 --- a/tests/components/smartthings/test_media_player.py +++ b/tests/components/smartthings/test_media_player.py @@ -13,7 +13,10 @@ from homeassistant.components.media_player import ( ATTR_MEDIA_SHUFFLE, ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, + ATTR_SOUND_MODE, + ATTR_SOUND_MODE_LIST, DOMAIN as MEDIA_PLAYER_DOMAIN, + SERVICE_SELECT_SOUND_MODE, SERVICE_SELECT_SOURCE, MediaPlayerEntityFeature, RepeatMode, @@ -513,6 +516,60 @@ async def test_media_repeat_mode( ) +@pytest.mark.parametrize("device_fixture", ["vd_network_audio_002s"]) +async def test_select_sound_mode( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test media player select sound mode command.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_SELECT_SOUND_MODE, + { + ATTR_ENTITY_ID: "media_player.theater_soundbar_living", + ATTR_SOUND_MODE: "adaptive_sound", + }, + blocking=True, + ) + devices.execute_device_command.assert_called_once_with( + "0d94e5db-8501-2355-eb4f-214163702cac", + Capability.EXECUTE, + Command.EXECUTE, + MAIN, + argument=[ + "/sec/networkaudio/soundmode", + {"x.com.samsung.networkaudio.soundmode": "adaptive sound"}, + ], + ) + assert ( + hass.states.get("media_player.theater_soundbar_living").attributes[ + ATTR_SOUND_MODE + ] + == "adaptive_sound" + ) + + +@pytest.mark.parametrize("device_fixture", ["im_speaker_ai_0001"]) +async def test_no_sound_mode_on_non_soundbar( + hass: HomeAssistant, + devices: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test non-soundbar network audio devices don't expose sound mode.""" + await setup_integration(hass, mock_config_entry) + + state = hass.states.get("media_player.galaxy_home_mini") + assert state is not None + assert ATTR_SOUND_MODE_LIST not in state.attributes + assert not ( + state.attributes[ATTR_SUPPORTED_FEATURES] + & MediaPlayerEntityFeature.SELECT_SOUND_MODE + ) + + @pytest.mark.parametrize("device_fixture", ["hw_q80r_soundbar"]) async def test_state_update( hass: HomeAssistant,