Add media_player support to SmartThings integration (#141296)

* Initial soundbar support

* Soundbar support

* Add SAMSUNG_VD_AUDIO_INPUT_SOURCE capability

* Adjust setting input source

* Add unit tests for media_player platform

* Adjust code after merge

* Adjust code after merge

* Adjust code style

* Adjust code style

* Fix

* Fix

---------

Co-authored-by: Piotr Machowski <PiotrMachowski@users.noreply.github.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
Piotr Machowski 2025-03-25 13:34:19 +01:00 committed by GitHub
parent 0ddf3c794b
commit f00fb1d9a3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 1550 additions and 0 deletions

View File

@ -89,6 +89,7 @@ PLATFORMS = [
Platform.FAN,
Platform.LIGHT,
Platform.LOCK,
Platform.MEDIA_PLAYER,
Platform.NUMBER,
Platform.SCENE,
Platform.SELECT,

View File

@ -0,0 +1,348 @@
"""Support for media players through the SmartThings cloud API."""
from __future__ import annotations
from typing import Any
from pysmartthings import Attribute, Capability, Category, Command, SmartThings
from homeassistant.components.media_player import (
MediaPlayerDeviceClass,
MediaPlayerEntity,
MediaPlayerEntityFeature,
MediaPlayerState,
RepeatMode,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import FullDevice, SmartThingsConfigEntry
from .const import MAIN
from .entity import SmartThingsEntity
MEDIA_PLAYER_CAPABILITIES = (
Capability.AUDIO_MUTE,
Capability.AUDIO_TRACK_DATA,
Capability.AUDIO_VOLUME,
Capability.MEDIA_PLAYBACK,
)
CONTROLLABLE_SOURCES = ["bluetooth", "wifi"]
DEVICE_CLASS_MAP: dict[Category | str, MediaPlayerDeviceClass] = {
Category.NETWORK_AUDIO: MediaPlayerDeviceClass.SPEAKER,
Category.SPEAKER: MediaPlayerDeviceClass.SPEAKER,
Category.TELEVISION: MediaPlayerDeviceClass.TV,
Category.RECEIVER: MediaPlayerDeviceClass.RECEIVER,
}
VALUE_TO_STATE = {
"buffering": MediaPlayerState.BUFFERING,
"paused": MediaPlayerState.PAUSED,
"playing": MediaPlayerState.PLAYING,
"stopped": MediaPlayerState.IDLE,
"fast forwarding": MediaPlayerState.BUFFERING,
"rewinding": MediaPlayerState.BUFFERING,
}
REPEAT_MODE_TO_HA = {
"all": RepeatMode.ALL,
"one": RepeatMode.ONE,
"off": RepeatMode.OFF,
}
HA_REPEAT_MODE_TO_SMARTTHINGS = {v: k for k, v in REPEAT_MODE_TO_HA.items()}
async def async_setup_entry(
hass: HomeAssistant,
entry: SmartThingsConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Add media players for a config entry."""
entry_data = entry.runtime_data
async_add_entities(
SmartThingsMediaPlayer(entry_data.client, device)
for device in entry_data.devices.values()
if all(
capability in device.status[MAIN]
for capability in MEDIA_PLAYER_CAPABILITIES
)
)
class SmartThingsMediaPlayer(SmartThingsEntity, MediaPlayerEntity):
"""Define a SmartThings media player."""
_attr_name = None
def __init__(self, client: SmartThings, device: FullDevice) -> None:
"""Initialize the media_player class."""
super().__init__(
client,
device,
{
Capability.AUDIO_MUTE,
Capability.AUDIO_TRACK_DATA,
Capability.AUDIO_VOLUME,
Capability.MEDIA_INPUT_SOURCE,
Capability.MEDIA_PLAYBACK,
Capability.MEDIA_PLAYBACK_REPEAT,
Capability.MEDIA_PLAYBACK_SHUFFLE,
Capability.SAMSUNG_VD_AUDIO_INPUT_SOURCE,
Capability.SWITCH,
},
)
self._attr_supported_features = self._determine_features()
self._attr_device_class = DEVICE_CLASS_MAP.get(
device.device.components[MAIN].user_category
or device.device.components[MAIN].manufacturer_category,
)
def _determine_features(self) -> MediaPlayerEntityFeature:
flags = MediaPlayerEntityFeature(0)
playback_commands = self.get_attribute_value(
Capability.MEDIA_PLAYBACK, Attribute.SUPPORTED_PLAYBACK_COMMANDS
)
if "play" in playback_commands:
flags |= MediaPlayerEntityFeature.PLAY
if "pause" in playback_commands:
flags |= MediaPlayerEntityFeature.PAUSE
if "stop" in playback_commands:
flags |= MediaPlayerEntityFeature.STOP
if "rewind" in playback_commands:
flags |= MediaPlayerEntityFeature.PREVIOUS_TRACK
if "fastForward" in playback_commands:
flags |= MediaPlayerEntityFeature.NEXT_TRACK
if self.supports_capability(Capability.AUDIO_VOLUME):
flags |= (
MediaPlayerEntityFeature.VOLUME_SET
| MediaPlayerEntityFeature.VOLUME_STEP
)
if self.supports_capability(Capability.AUDIO_MUTE):
flags |= MediaPlayerEntityFeature.VOLUME_MUTE
if self.supports_capability(Capability.SWITCH):
flags |= (
MediaPlayerEntityFeature.TURN_ON | MediaPlayerEntityFeature.TURN_OFF
)
if self.supports_capability(Capability.MEDIA_INPUT_SOURCE):
flags |= MediaPlayerEntityFeature.SELECT_SOURCE
if self.supports_capability(Capability.MEDIA_PLAYBACK_SHUFFLE):
flags |= MediaPlayerEntityFeature.SHUFFLE_SET
if self.supports_capability(Capability.MEDIA_PLAYBACK_REPEAT):
flags |= MediaPlayerEntityFeature.REPEAT_SET
return flags
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the media player off."""
await self.execute_device_command(
Capability.SWITCH,
Command.OFF,
)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the media player on."""
await self.execute_device_command(
Capability.SWITCH,
Command.ON,
)
async def async_mute_volume(self, mute: bool) -> None:
"""Mute volume."""
await self.execute_device_command(
Capability.AUDIO_MUTE,
Command.SET_MUTE,
argument="muted" if mute else "unmuted",
)
async def async_set_volume_level(self, volume: float) -> None:
"""Set volume level."""
await self.execute_device_command(
Capability.AUDIO_VOLUME,
Command.SET_VOLUME,
argument=int(volume * 100),
)
async def async_volume_up(self) -> None:
"""Increase volume."""
await self.execute_device_command(
Capability.AUDIO_VOLUME,
Command.VOLUME_UP,
)
async def async_volume_down(self) -> None:
"""Decrease volume."""
await self.execute_device_command(
Capability.AUDIO_VOLUME,
Command.VOLUME_DOWN,
)
async def async_media_play(self) -> None:
"""Play media."""
await self.execute_device_command(
Capability.MEDIA_PLAYBACK,
Command.PLAY,
)
async def async_media_pause(self) -> None:
"""Pause media."""
await self.execute_device_command(
Capability.MEDIA_PLAYBACK,
Command.PAUSE,
)
async def async_media_stop(self) -> None:
"""Stop media."""
await self.execute_device_command(
Capability.MEDIA_PLAYBACK,
Command.STOP,
)
async def async_media_previous_track(self) -> None:
"""Previous track."""
await self.execute_device_command(
Capability.MEDIA_PLAYBACK,
Command.REWIND,
)
async def async_media_next_track(self) -> None:
"""Next track."""
await self.execute_device_command(
Capability.MEDIA_PLAYBACK,
Command.FAST_FORWARD,
)
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,
)
async def async_set_shuffle(self, shuffle: bool) -> None:
"""Set shuffle mode."""
await self.execute_device_command(
Capability.MEDIA_PLAYBACK_SHUFFLE,
Command.SET_PLAYBACK_SHUFFLE,
argument="enabled" if shuffle else "disabled",
)
async def async_set_repeat(self, repeat: RepeatMode) -> None:
"""Set repeat mode."""
await self.execute_device_command(
Capability.MEDIA_PLAYBACK_REPEAT,
Command.SET_PLAYBACK_REPEAT_MODE,
argument=HA_REPEAT_MODE_TO_SMARTTHINGS[repeat],
)
@property
def media_title(self) -> str | None:
"""Title of current playing media."""
if (
track_data := self.get_attribute_value(
Capability.AUDIO_TRACK_DATA, Attribute.AUDIO_TRACK_DATA
)
) is None:
return None
return track_data.get("title", None)
@property
def media_artist(self) -> str | None:
"""Artist of current playing media."""
if (
track_data := self.get_attribute_value(
Capability.AUDIO_TRACK_DATA, Attribute.AUDIO_TRACK_DATA
)
) is None:
return None
return track_data.get("artist")
@property
def state(self) -> MediaPlayerState | None:
"""State of the media player."""
if self.supports_capability(Capability.SWITCH):
if self.get_attribute_value(Capability.SWITCH, Attribute.SWITCH) == "on":
if (
self.source is not None
and self.source in CONTROLLABLE_SOURCES
and self.get_attribute_value(
Capability.MEDIA_PLAYBACK, Attribute.PLAYBACK_STATUS
)
in VALUE_TO_STATE
):
return VALUE_TO_STATE[
self.get_attribute_value(
Capability.MEDIA_PLAYBACK, Attribute.PLAYBACK_STATUS
)
]
return MediaPlayerState.ON
return MediaPlayerState.OFF
return VALUE_TO_STATE[
self.get_attribute_value(
Capability.MEDIA_PLAYBACK, Attribute.PLAYBACK_STATUS
)
]
@property
def is_volume_muted(self) -> bool:
"""Returns if the volume is muted."""
return (
self.get_attribute_value(Capability.AUDIO_MUTE, Attribute.MUTE) == "muted"
)
@property
def volume_level(self) -> float:
"""Volume level."""
return self.get_attribute_value(Capability.AUDIO_VOLUME, Attribute.VOLUME) / 100
@property
def source(self) -> str | None:
"""Input source."""
if self.supports_capability(Capability.MEDIA_INPUT_SOURCE):
return 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(
Capability.SAMSUNG_VD_AUDIO_INPUT_SOURCE, Attribute.INPUT_SOURCE
)
return None
@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
@property
def shuffle(self) -> bool | None:
"""Returns if shuffle mode is set."""
if self.supports_capability(Capability.MEDIA_PLAYBACK_SHUFFLE):
return (
self.get_attribute_value(
Capability.MEDIA_PLAYBACK_SHUFFLE, Attribute.PLAYBACK_SHUFFLE
)
== "enabled"
)
return None
@property
def repeat(self) -> RepeatMode | None:
"""Returns if repeat mode is set."""
if self.supports_capability(Capability.MEDIA_PLAYBACK_REPEAT):
return REPEAT_MODE_TO_HA[
self.get_attribute_value(
Capability.MEDIA_PLAYBACK_REPEAT, Attribute.PLAYBACK_REPEAT_MODE
)
]
return None

View File

@ -140,6 +140,7 @@ def mock_smartthings() -> Generator[AsyncMock]:
"tplink_p110",
"ikea_kadrilj",
"aux_ac",
"hw_q80r_soundbar",
]
)
def device_fixture(

View File

@ -0,0 +1,173 @@
{
"components": {
"main": {
"mediaPlayback": {
"supportedPlaybackCommands": {
"value": ["play", "pause", "stop"],
"timestamp": "2025-03-23T01:10:02.207Z"
},
"playbackStatus": {
"value": "playing",
"timestamp": "2025-03-23T01:19:44.622Z"
}
},
"samsungvd.groupInfo": {
"role": {
"value": "none",
"timestamp": "2025-03-23T01:17:10.965Z"
},
"channel": {
"value": "all",
"timestamp": "2025-03-23T01:17:10.965Z"
},
"masterName": {
"value": "",
"timestamp": "2025-03-23T01:17:10.965Z"
},
"status": {
"value": "single",
"timestamp": "2025-03-23T01:17:10.965Z"
}
},
"audioVolume": {
"volume": {
"value": 1,
"unit": "%",
"timestamp": "2025-03-23T01:17:13.754Z"
}
},
"ocf": {
"st": {
"value": "NONE",
"timestamp": "2024-12-18T21:07:25.406Z"
},
"mndt": {
"value": "2018-01-01",
"timestamp": "2024-12-18T21:07:25.406Z"
},
"mnfv": {
"value": "HW-Q80RWWB-1012.6",
"timestamp": "2024-12-18T21:07:25.406Z"
},
"mnhw": {
"value": "0-0",
"timestamp": "2024-12-18T21:07:25.406Z"
},
"di": {
"value": "afcf3b91-48fe-4c3b-ab44-ddff2a0a6577",
"timestamp": "2024-12-18T21:07:25.406Z"
},
"mnsl": {
"value": "http://www.samsung.com/sec/audio-video/",
"timestamp": "2024-12-18T21:07:25.406Z"
},
"dmv": {
"value": "res.1.1.0,sh.1.1.0",
"timestamp": "2024-12-18T21:07:25.406Z"
},
"n": {
"value": "[AV] Samsung Soundbar Q80R",
"timestamp": "2024-12-18T21:07:25.406Z"
},
"mnmo": {
"value": "Q80R",
"timestamp": "2024-12-18T21:07:25.406Z"
},
"vid": {
"value": "VD-NetworkAudio-001S",
"timestamp": "2024-12-18T21:07:25.406Z"
},
"mnmn": {
"value": "Samsung Electronics",
"timestamp": "2024-12-18T21:07:25.406Z"
},
"mnml": {
"value": "http://www.samsung.com",
"timestamp": "2024-12-18T21:07:25.406Z"
},
"mnpv": {
"value": "Tizen 4.0",
"timestamp": "2024-12-18T21:07:25.406Z"
},
"mnos": {
"value": "4.1.10",
"timestamp": "2024-12-18T21:07:25.406Z"
},
"pi": {
"value": "afcf3b91-48fe-4c3b-ab44-ddff2a0a6577",
"timestamp": "2024-12-18T21:07:25.406Z"
},
"icv": {
"value": "core.1.1.0",
"timestamp": "2024-12-18T21:07:25.406Z"
}
},
"mediaInputSource": {
"supportedInputSources": {
"value": ["wifi", "bluetooth", "HDMI1", "HDMI2", "digital"],
"timestamp": "2025-03-23T01:18:01.663Z"
},
"inputSource": {
"value": "wifi",
"timestamp": "2025-03-23T01:18:01.663Z"
}
},
"refresh": {},
"audioNotification": {},
"audioMute": {
"mute": {
"value": "unmuted",
"timestamp": "2025-03-23T01:17:11.024Z"
}
},
"execute": {
"data": {
"value": {
"payload": {
"rt": ["x.com.samsung.networkaudio.soundmode"],
"if": ["oic.if.a", "oic.if.baseline"],
"x.com.samsung.networkaudio.soundmode": "standard"
}
},
"data": {
"href": "/sec/networkaudio/soundmode"
},
"timestamp": "2023-07-16T23:16:55.582Z"
}
},
"samsungvd.audioInputSource": {
"supportedInputSources": {
"value": ["wifi", "bluetooth", "HDMI1", "HDMI2", "digital"],
"timestamp": "2025-03-23T01:18:01.663Z"
},
"inputSource": {
"value": "wificp",
"timestamp": "2025-03-23T01:18:01.663Z"
}
},
"switch": {
"switch": {
"value": "on",
"timestamp": "2025-03-23T01:19:44.837Z"
}
},
"audioTrackData": {
"totalTime": {
"value": null,
"timestamp": "2020-07-30T16:09:09.109Z"
},
"audioTrackData": {
"value": {
"title": "Never Gonna Give You Up",
"artist": "Rick Astley"
},
"timestamp": "2025-03-23T01:19:15.067Z"
},
"elapsedTime": {
"value": null,
"timestamp": "2020-07-30T16:09:09.109Z"
}
}
}
}
}

View File

@ -0,0 +1,106 @@
{
"items": [
{
"deviceId": "afcf3b91-0000-1111-2222-ddff2a0a6577",
"name": "[AV] Samsung Soundbar Q80R",
"label": "Soundbar",
"manufacturerName": "Samsung Electronics",
"presentationId": "VD-NetworkAudio-001S",
"deviceManufacturerCode": "Samsung Electronics",
"locationId": "c7f8e400-0000-1111-2222-76463f4eb484",
"ownerId": "bd0d9288-0000-1111-2222-68310a42a709",
"roomId": "be09ff51-0000-1111-2222-e48e2dab37fd",
"deviceTypeName": "Samsung OCF Network Audio Player",
"components": [
{
"id": "main",
"label": "Soundbar",
"capabilities": [
{
"id": "ocf",
"version": 1
},
{
"id": "execute",
"version": 1
},
{
"id": "refresh",
"version": 1
},
{
"id": "switch",
"version": 1
},
{
"id": "audioVolume",
"version": 1
},
{
"id": "audioMute",
"version": 1
},
{
"id": "audioTrackData",
"version": 1
},
{
"id": "mediaInputSource",
"version": 1
},
{
"id": "samsungvd.audioInputSource",
"version": 1
},
{
"id": "mediaPlayback",
"version": 1
},
{
"id": "audioNotification",
"version": 1
},
{
"id": "samsungvd.groupInfo",
"version": 1
}
],
"categories": [
{
"name": "NetworkAudio",
"categoryType": "manufacturer"
}
]
}
],
"createTime": "2020-10-19T01:35:08Z",
"profile": {
"id": "c1036d88-000-1111-2222-a361463fd53f"
},
"ocf": {
"ocfDeviceType": "oic.d.networkaudio",
"name": "[AV] Samsung Soundbar Q80R",
"specVersion": "core.1.1.0",
"verticalDomainSpecVersion": "res.1.1.0,sh.1.1.0",
"manufacturerName": "Samsung Electronics",
"modelNumber": "Q80R",
"platformVersion": "Tizen 4.0",
"platformOS": "4.1.10",
"hwVersion": "0-0",
"firmwareVersion": "HW-Q80RWWB-1012.6",
"vendorId": "VD-NetworkAudio-001S",
"vendorResourceClientServerVersion": "1.2",
"locale": "KO",
"lastSignupTime": "2021-01-16T07:05:02.184545Z",
"transferCandidate": false,
"additionalAuthCodeRequired": false
},
"type": "OCF",
"restrictionTier": 0,
"allowed": null,
"executionContext": "CLOUD",
"relationships": []
}
],
"_links": {}
}

View File

@ -1157,6 +1157,39 @@
'via_device_id': None,
})
# ---
# name: test_devices[hw_q80r_soundbar]
DeviceRegistryEntrySnapshot({
'area_id': None,
'config_entries': <ANY>,
'config_entries_subentries': <ANY>,
'configuration_url': 'https://account.smartthings.com',
'connections': set({
}),
'disabled_by': None,
'entry_type': None,
'hw_version': '0-0',
'id': <ANY>,
'identifiers': set({
tuple(
'smartthings',
'afcf3b91-0000-1111-2222-ddff2a0a6577',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Samsung Electronics',
'model': 'Q80R',
'model_id': None,
'name': 'Soundbar',
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': None,
'sw_version': 'HW-Q80RWWB-1012.6',
'via_device_id': None,
})
# ---
# name: test_devices[ikea_kadrilj]
DeviceRegistryEntrySnapshot({
'area_id': None,

View File

@ -0,0 +1,233 @@
# serializer version: 1
# name: test_all_entities[hw_q80r_soundbar][media_player.soundbar-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'source_list': list([
'wifi',
'bluetooth',
'HDMI1',
'HDMI2',
'digital',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'media_player',
'entity_category': None,
'entity_id': 'media_player.soundbar',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <MediaPlayerDeviceClass.SPEAKER: 'speaker'>,
'original_icon': None,
'original_name': None,
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': <MediaPlayerEntityFeature: 23949>,
'translation_key': None,
'unique_id': 'afcf3b91-0000-1111-2222-ddff2a0a6577',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[hw_q80r_soundbar][media_player.soundbar-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'speaker',
'friendly_name': 'Soundbar',
'is_volume_muted': False,
'media_artist': 'Rick Astley',
'media_title': 'Never Gonna Give You Up',
'source': 'wifi',
'source_list': list([
'wifi',
'bluetooth',
'HDMI1',
'HDMI2',
'digital',
]),
'supported_features': <MediaPlayerEntityFeature: 23949>,
'volume_level': 0.01,
}),
'context': <ANY>,
'entity_id': 'media_player.soundbar',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'playing',
})
# ---
# name: test_all_entities[im_speaker_ai_0001][media_player.galaxy_home_mini-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'media_player',
'entity_category': None,
'entity_id': 'media_player.galaxy_home_mini',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <MediaPlayerDeviceClass.SPEAKER: 'speaker'>,
'original_icon': None,
'original_name': None,
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': <MediaPlayerEntityFeature: 318477>,
'translation_key': None,
'unique_id': 'c9276e43-fe3c-88c3-1dcc-2eb79e292b8c',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[im_speaker_ai_0001][media_player.galaxy_home_mini-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'speaker',
'friendly_name': 'Galaxy Home Mini',
'is_volume_muted': False,
'repeat': <RepeatMode.OFF: 'off'>,
'shuffle': False,
'supported_features': <MediaPlayerEntityFeature: 318477>,
'volume_level': 0.52,
}),
'context': <ANY>,
'entity_id': 'media_player.galaxy_home_mini',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'idle',
})
# ---
# name: test_all_entities[sonos_player][media_player.elliots_rum-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'media_player',
'entity_category': None,
'entity_id': 'media_player.elliots_rum',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <MediaPlayerDeviceClass.SPEAKER: 'speaker'>,
'original_icon': None,
'original_name': None,
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': <MediaPlayerEntityFeature: 21517>,
'translation_key': None,
'unique_id': 'c85fced9-c474-4a47-93c2-037cc7829536',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[sonos_player][media_player.elliots_rum-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'speaker',
'friendly_name': 'Elliots Rum',
'is_volume_muted': False,
'media_artist': 'David Guetta',
'media_title': 'Forever Young',
'supported_features': <MediaPlayerEntityFeature: 21517>,
'volume_level': 0.15,
}),
'context': <ANY>,
'entity_id': 'media_player.elliots_rum',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'playing',
})
# ---
# name: test_all_entities[vd_network_audio_002s][media_player.soundbar_living-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'media_player',
'entity_category': None,
'entity_id': 'media_player.soundbar_living',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <MediaPlayerDeviceClass.SPEAKER: 'speaker'>,
'original_icon': None,
'original_name': None,
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': <MediaPlayerEntityFeature: 21901>,
'translation_key': None,
'unique_id': '0d94e5db-8501-2355-eb4f-214163702cac',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[vd_network_audio_002s][media_player.soundbar_living-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'speaker',
'friendly_name': 'Soundbar Living',
'is_volume_muted': False,
'media_artist': '',
'media_title': '',
'source': 'HDMI1',
'supported_features': <MediaPlayerEntityFeature: 21901>,
'volume_level': 0.17,
}),
'context': <ANY>,
'entity_id': 'media_player.soundbar_living',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---

View File

@ -7193,6 +7193,182 @@
'state': '19.0',
})
# ---
# name: test_all_entities[hw_q80r_soundbar][sensor.soundbar_media_input_source-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'wifi',
'bluetooth',
'hdmi1',
'hdmi2',
'digital',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.soundbar_media_input_source',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': None,
'original_name': 'Media input source',
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'media_input_source',
'unique_id': 'afcf3b91-0000-1111-2222-ddff2a0a6577.inputSource',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[hw_q80r_soundbar][sensor.soundbar_media_input_source-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'enum',
'friendly_name': 'Soundbar Media input source',
'options': list([
'wifi',
'bluetooth',
'hdmi1',
'hdmi2',
'digital',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.soundbar_media_input_source',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'wifi',
})
# ---
# name: test_all_entities[hw_q80r_soundbar][sensor.soundbar_media_playback_status-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': dict({
'options': list([
'paused',
'playing',
'stopped',
'fast_forwarding',
'rewinding',
'buffering',
]),
}),
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.soundbar_media_playback_status',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
'original_icon': None,
'original_name': 'Media playback status',
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'media_playback_status',
'unique_id': 'afcf3b91-0000-1111-2222-ddff2a0a6577.playbackStatus',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[hw_q80r_soundbar][sensor.soundbar_media_playback_status-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'enum',
'friendly_name': 'Soundbar Media playback status',
'options': list([
'paused',
'playing',
'stopped',
'fast_forwarding',
'rewinding',
'buffering',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.soundbar_media_playback_status',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'playing',
})
# ---
# name: test_all_entities[hw_q80r_soundbar][sensor.soundbar_volume-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.soundbar_volume',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Volume',
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': 'audio_volume',
'unique_id': 'afcf3b91-0000-1111-2222-ddff2a0a6577.volume',
'unit_of_measurement': '%',
})
# ---
# name: test_all_entities[hw_q80r_soundbar][sensor.soundbar_volume-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Soundbar Volume',
'unit_of_measurement': '%',
}),
'context': <ANY>,
'entity_id': 'sensor.soundbar_volume',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '1',
})
# ---
# name: test_all_entities[ikea_kadrilj][sensor.kitchen_ikea_kadrilj_window_blind_battery-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@ -610,6 +610,53 @@
'state': 'off',
})
# ---
# name: test_all_entities[hw_q80r_soundbar][switch.soundbar-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': None,
'entity_id': 'switch.soundbar',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': None,
'platform': 'smartthings',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'afcf3b91-0000-1111-2222-ddff2a0a6577',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[hw_q80r_soundbar][switch.soundbar-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Soundbar',
}),
'context': <ANY>,
'entity_id': 'switch.soundbar',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'on',
})
# ---
# name: test_all_entities[sensibo_airconditioner_1][switch.office-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@ -0,0 +1,432 @@
"""Test for the SmartThings media player platform."""
from unittest.mock import AsyncMock
from pysmartthings import Attribute, Capability, Command, Status
import pytest
from syrupy import SnapshotAssertion
from homeassistant.components.media_player import (
ATTR_INPUT_SOURCE,
ATTR_MEDIA_REPEAT,
ATTR_MEDIA_SHUFFLE,
ATTR_MEDIA_VOLUME_LEVEL,
ATTR_MEDIA_VOLUME_MUTED,
DOMAIN as MEDIA_PLAYER_DOMAIN,
SERVICE_SELECT_SOURCE,
RepeatMode,
)
from homeassistant.components.smartthings.const import MAIN
from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_MEDIA_NEXT_TRACK,
SERVICE_MEDIA_PAUSE,
SERVICE_MEDIA_PLAY,
SERVICE_MEDIA_PREVIOUS_TRACK,
SERVICE_MEDIA_STOP,
SERVICE_REPEAT_SET,
SERVICE_SHUFFLE_SET,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
SERVICE_VOLUME_DOWN,
SERVICE_VOLUME_MUTE,
SERVICE_VOLUME_SET,
SERVICE_VOLUME_UP,
STATE_OFF,
STATE_PLAYING,
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_integration, snapshot_smartthings_entities, trigger_update
from tests.common import MockConfigEntry
async def test_all_entities(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test all entities."""
await setup_integration(hass, mock_config_entry)
snapshot_smartthings_entities(
hass, entity_registry, snapshot, Platform.MEDIA_PLAYER
)
@pytest.mark.parametrize("device_fixture", ["hw_q80r_soundbar"])
@pytest.mark.parametrize(
("action", "command"),
[
(SERVICE_TURN_ON, Command.ON),
(SERVICE_TURN_OFF, Command.OFF),
],
)
async def test_turn_on_off(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
action: str,
command: Command,
) -> None:
"""Test media player turn on and off command."""
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
action,
{ATTR_ENTITY_ID: "media_player.soundbar"},
blocking=True,
)
devices.execute_device_command.assert_called_once_with(
"afcf3b91-0000-1111-2222-ddff2a0a6577", Capability.SWITCH, command, MAIN
)
@pytest.mark.parametrize("device_fixture", ["hw_q80r_soundbar"])
@pytest.mark.parametrize(
("muted", "argument"),
[
(True, "muted"),
(False, "unmuted"),
],
)
async def test_mute_unmute(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
muted: bool,
argument: str,
) -> None:
"""Test media player mute and unmute command."""
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_VOLUME_MUTE,
{ATTR_ENTITY_ID: "media_player.soundbar", ATTR_MEDIA_VOLUME_MUTED: muted},
blocking=True,
)
devices.execute_device_command.assert_called_once_with(
"afcf3b91-0000-1111-2222-ddff2a0a6577",
Capability.AUDIO_MUTE,
Command.SET_MUTE,
MAIN,
argument=argument,
)
@pytest.mark.parametrize("device_fixture", ["hw_q80r_soundbar"])
async def test_set_volume_level(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test media player set volume level command."""
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_VOLUME_SET,
{ATTR_ENTITY_ID: "media_player.soundbar", ATTR_MEDIA_VOLUME_LEVEL: 0.31},
blocking=True,
)
devices.execute_device_command.assert_called_once_with(
"afcf3b91-0000-1111-2222-ddff2a0a6577",
Capability.AUDIO_VOLUME,
Command.SET_VOLUME,
MAIN,
argument=31,
)
@pytest.mark.parametrize("device_fixture", ["hw_q80r_soundbar"])
async def test_volume_up(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test media player increase volume level command."""
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_VOLUME_UP,
{ATTR_ENTITY_ID: "media_player.soundbar"},
blocking=True,
)
devices.execute_device_command.assert_called_once_with(
"afcf3b91-0000-1111-2222-ddff2a0a6577",
Capability.AUDIO_VOLUME,
Command.VOLUME_UP,
MAIN,
)
@pytest.mark.parametrize("device_fixture", ["hw_q80r_soundbar"])
async def test_volume_down(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test media player decrease volume level command."""
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_VOLUME_DOWN,
{ATTR_ENTITY_ID: "media_player.soundbar"},
blocking=True,
)
devices.execute_device_command.assert_called_once_with(
"afcf3b91-0000-1111-2222-ddff2a0a6577",
Capability.AUDIO_VOLUME,
Command.VOLUME_DOWN,
MAIN,
)
@pytest.mark.parametrize("device_fixture", ["hw_q80r_soundbar"])
async def test_media_play(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test media player play command."""
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_MEDIA_PLAY,
{ATTR_ENTITY_ID: "media_player.soundbar"},
blocking=True,
)
devices.execute_device_command.assert_called_once_with(
"afcf3b91-0000-1111-2222-ddff2a0a6577",
Capability.MEDIA_PLAYBACK,
Command.PLAY,
MAIN,
)
@pytest.mark.parametrize("device_fixture", ["hw_q80r_soundbar"])
async def test_media_pause(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test media player pause command."""
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_MEDIA_PAUSE,
{ATTR_ENTITY_ID: "media_player.soundbar"},
blocking=True,
)
devices.execute_device_command.assert_called_once_with(
"afcf3b91-0000-1111-2222-ddff2a0a6577",
Capability.MEDIA_PLAYBACK,
Command.PAUSE,
MAIN,
)
@pytest.mark.parametrize("device_fixture", ["hw_q80r_soundbar"])
async def test_media_stop(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test media player stop command."""
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_MEDIA_STOP,
{ATTR_ENTITY_ID: "media_player.soundbar"},
blocking=True,
)
devices.execute_device_command.assert_called_once_with(
"afcf3b91-0000-1111-2222-ddff2a0a6577",
Capability.MEDIA_PLAYBACK,
Command.STOP,
MAIN,
)
@pytest.mark.parametrize("device_fixture", ["hw_q80r_soundbar"])
async def test_media_previous_track(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test media player previous track command."""
devices.get_device_status.return_value[MAIN][Capability.MEDIA_PLAYBACK] = {
Attribute.SUPPORTED_PLAYBACK_COMMANDS: Status(["rewind"])
}
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_MEDIA_PREVIOUS_TRACK,
{ATTR_ENTITY_ID: "media_player.soundbar"},
blocking=True,
)
devices.execute_device_command.assert_called_once_with(
"afcf3b91-0000-1111-2222-ddff2a0a6577",
Capability.MEDIA_PLAYBACK,
Command.REWIND,
MAIN,
)
@pytest.mark.parametrize("device_fixture", ["hw_q80r_soundbar"])
async def test_media_next_track(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test media player next track command."""
devices.get_device_status.return_value[MAIN][Capability.MEDIA_PLAYBACK] = {
Attribute.SUPPORTED_PLAYBACK_COMMANDS: Status(["fastForward"])
}
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_MEDIA_NEXT_TRACK,
{ATTR_ENTITY_ID: "media_player.soundbar"},
blocking=True,
)
devices.execute_device_command.assert_called_once_with(
"afcf3b91-0000-1111-2222-ddff2a0a6577",
Capability.MEDIA_PLAYBACK,
Command.FAST_FORWARD,
MAIN,
)
@pytest.mark.parametrize("device_fixture", ["hw_q80r_soundbar"])
async def test_select_source(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test media player stop command."""
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_SELECT_SOURCE,
{ATTR_ENTITY_ID: "media_player.soundbar", ATTR_INPUT_SOURCE: "digital"},
blocking=True,
)
devices.execute_device_command.assert_called_once_with(
"afcf3b91-0000-1111-2222-ddff2a0a6577",
Capability.MEDIA_INPUT_SOURCE,
Command.SET_INPUT_SOURCE,
MAIN,
"digital",
)
@pytest.mark.parametrize("device_fixture", ["hw_q80r_soundbar"])
@pytest.mark.parametrize(
("shuffle", "argument"),
[
(True, "enabled"),
(False, "disabled"),
],
)
async def test_media_shuffle_on_off(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
shuffle: bool,
argument: bool,
) -> None:
"""Test media player media shuffle command."""
devices.get_device_status.return_value[MAIN][Capability.MEDIA_PLAYBACK_SHUFFLE] = {
Attribute.PLAYBACK_SHUFFLE: Status(True)
}
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_SHUFFLE_SET,
{ATTR_ENTITY_ID: "media_player.soundbar", ATTR_MEDIA_SHUFFLE: shuffle},
blocking=True,
)
devices.execute_device_command.assert_called_once_with(
"afcf3b91-0000-1111-2222-ddff2a0a6577",
Capability.MEDIA_PLAYBACK_SHUFFLE,
Command.SET_PLAYBACK_SHUFFLE,
MAIN,
argument=argument,
)
@pytest.mark.parametrize("device_fixture", ["hw_q80r_soundbar"])
@pytest.mark.parametrize(
("repeat", "argument"),
[
(RepeatMode.OFF, "off"),
(RepeatMode.ONE, "one"),
(RepeatMode.ALL, "all"),
],
)
async def test_media_repeat_mode(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
repeat: RepeatMode,
argument: bool,
) -> None:
"""Test media player repeat mode command."""
devices.get_device_status.return_value[MAIN][Capability.MEDIA_PLAYBACK_REPEAT] = {
Attribute.REPEAT_MODE: Status("one")
}
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_REPEAT_SET,
{ATTR_ENTITY_ID: "media_player.soundbar", ATTR_MEDIA_REPEAT: repeat},
blocking=True,
)
devices.execute_device_command.assert_called_once_with(
"afcf3b91-0000-1111-2222-ddff2a0a6577",
Capability.MEDIA_PLAYBACK_REPEAT,
Command.SET_PLAYBACK_REPEAT_MODE,
MAIN,
argument=argument,
)
@pytest.mark.parametrize("device_fixture", ["hw_q80r_soundbar"])
async def test_state_update(
hass: HomeAssistant,
devices: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test state update."""
await setup_integration(hass, mock_config_entry)
assert hass.states.get("media_player.soundbar").state == STATE_PLAYING
await trigger_update(
hass,
devices,
"afcf3b91-0000-1111-2222-ddff2a0a6577",
Capability.SWITCH,
Attribute.SWITCH,
"off",
)
assert hass.states.get("media_player.soundbar").state == STATE_OFF