Add Smartthings soundmode using networkaudio (#168698)

This commit is contained in:
Barry vd. Heuvel
2026-09-11 16:22:32 +02:00
committed by GitHub
parent 96c22f9ef5
commit 20485cbf88
4 changed files with 115 additions and 2 deletions
@@ -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:
@@ -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",
@@ -247,6 +247,12 @@
]),
'area_id': None,
'capabilities': dict({
<MediaPlayerEntityCapabilityAttribute.SOUND_MODE_LIST: 'sound_mode_list'>: list([
'standard',
'surround',
'game',
'adaptive_sound',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
@@ -272,7 +278,7 @@
'platform': 'smartthings',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': <MediaPlayerEntityFeature: 21901>,
'supported_features': <MediaPlayerEntityFeature: 87437>,
'translation_key': 'media_player',
'unique_id': '0d94e5db-8501-2355-eb4f-214163702cac_main',
'unit_of_measurement': None,
@@ -286,8 +292,14 @@
<MediaPlayerEntityStateAttribute.MEDIA_VOLUME_MUTED: 'is_volume_muted'>: False,
<MediaPlayerEntityStateAttribute.MEDIA_ARTIST: 'media_artist'>: '',
<MediaPlayerEntityStateAttribute.MEDIA_TITLE: 'media_title'>: '',
<MediaPlayerEntityCapabilityAttribute.SOUND_MODE_LIST: 'sound_mode_list'>: list([
'standard',
'surround',
'game',
'adaptive_sound',
]),
<MediaPlayerEntityStateAttribute.INPUT_SOURCE: 'source'>: 'hdmi1',
<EntityStateAttribute.SUPPORTED_FEATURES: 'supported_features'>: <MediaPlayerEntityFeature: 21901>,
<EntityStateAttribute.SUPPORTED_FEATURES: 'supported_features'>: <MediaPlayerEntityFeature: 87437>,
<MediaPlayerEntityStateAttribute.MEDIA_VOLUME_LEVEL: 'volume_level'>: 0.17,
}),
'context': <ANY>,
@@ -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,