1 """Support for LinkPlay media players."""
3 from __future__
import annotations
8 from linkplay.bridge
import LinkPlayBridge
9 from linkplay.consts
import EqualizerMode, LoopMode, PlayingMode, PlayingStatus
10 from linkplay.controller
import LinkPlayController, LinkPlayMultiroom
11 from linkplay.exceptions
import LinkPlayRequestException
12 import voluptuous
as vol
17 MediaPlayerDeviceClass,
19 MediaPlayerEntityFeature,
23 async_process_play_media_url,
29 config_validation
as cv,
31 entity_registry
as er,
36 from .
import LinkPlayConfigEntry, LinkPlayData
37 from .const
import CONTROLLER_KEY, DOMAIN
38 from .entity
import LinkPlayBaseEntity, exception_wrap
40 _LOGGER = logging.getLogger(__name__)
41 STATE_MAP: dict[PlayingStatus, MediaPlayerState] = {
42 PlayingStatus.STOPPED: MediaPlayerState.IDLE,
43 PlayingStatus.PAUSED: MediaPlayerState.PAUSED,
44 PlayingStatus.PLAYING: MediaPlayerState.PLAYING,
45 PlayingStatus.LOADING: MediaPlayerState.BUFFERING,
48 SOURCE_MAP: dict[PlayingMode, str] = {
49 PlayingMode.NETWORK:
"Wifi",
50 PlayingMode.LINE_IN:
"Line In",
51 PlayingMode.BLUETOOTH:
"Bluetooth",
52 PlayingMode.OPTICAL:
"Optical",
53 PlayingMode.LINE_IN_2:
"Line In 2",
54 PlayingMode.USB_DAC:
"USB DAC",
55 PlayingMode.COAXIAL:
"Coaxial",
56 PlayingMode.XLR:
"XLR",
57 PlayingMode.HDMI:
"HDMI",
58 PlayingMode.OPTICAL_2:
"Optical 2",
59 PlayingMode.EXTERN_BLUETOOTH:
"External Bluetooth",
60 PlayingMode.PHONO:
"Phono",
61 PlayingMode.ARC:
"ARC",
62 PlayingMode.COAXIAL_2:
"Coaxial 2",
63 PlayingMode.TF_CARD_1:
"SD Card 1",
64 PlayingMode.TF_CARD_2:
"SD Card 2",
66 PlayingMode.DAB:
"DAB Radio",
67 PlayingMode.FM:
"FM Radio",
68 PlayingMode.RCA:
"RCA",
69 PlayingMode.UDISK:
"USB",
70 PlayingMode.SPOTIFY:
"Spotify",
71 PlayingMode.TIDAL:
"Tidal",
72 PlayingMode.FOLLOWER:
"Follower",
75 SOURCE_MAP_INV: dict[str, PlayingMode] = {v: k
for k, v
in SOURCE_MAP.items()}
77 REPEAT_MAP: dict[LoopMode, RepeatMode] = {
78 LoopMode.CONTINOUS_PLAY_ONE_SONG: RepeatMode.ONE,
79 LoopMode.PLAY_IN_ORDER: RepeatMode.OFF,
80 LoopMode.CONTINUOUS_PLAYBACK: RepeatMode.ALL,
81 LoopMode.RANDOM_PLAYBACK: RepeatMode.ALL,
82 LoopMode.LIST_CYCLE: RepeatMode.ALL,
83 LoopMode.SHUFF_DISABLED_REPEAT_DISABLED: RepeatMode.OFF,
84 LoopMode.SHUFF_ENABLED_REPEAT_ENABLED_LOOP_ONCE: RepeatMode.ALL,
87 REPEAT_MAP_INV: dict[RepeatMode, LoopMode] = {v: k
for k, v
in REPEAT_MAP.items()}
89 EQUALIZER_MAP: dict[EqualizerMode, str] = {
90 EqualizerMode.NONE:
"None",
91 EqualizerMode.CLASSIC:
"Classic",
92 EqualizerMode.POP:
"Pop",
93 EqualizerMode.JAZZ:
"Jazz",
94 EqualizerMode.VOCAL:
"Vocal",
97 EQUALIZER_MAP_INV: dict[str, EqualizerMode] = {v: k
for k, v
in EQUALIZER_MAP.items()}
99 DEFAULT_FEATURES: MediaPlayerEntityFeature = (
100 MediaPlayerEntityFeature.PLAY
101 | MediaPlayerEntityFeature.PLAY_MEDIA
102 | MediaPlayerEntityFeature.BROWSE_MEDIA
103 | MediaPlayerEntityFeature.PAUSE
104 | MediaPlayerEntityFeature.STOP
105 | MediaPlayerEntityFeature.VOLUME_MUTE
106 | MediaPlayerEntityFeature.VOLUME_SET
107 | MediaPlayerEntityFeature.SELECT_SOURCE
108 | MediaPlayerEntityFeature.SELECT_SOUND_MODE
109 | MediaPlayerEntityFeature.GROUPING
112 SEEKABLE_FEATURES: MediaPlayerEntityFeature = (
113 MediaPlayerEntityFeature.PREVIOUS_TRACK
114 | MediaPlayerEntityFeature.NEXT_TRACK
115 | MediaPlayerEntityFeature.REPEAT_SET
116 | MediaPlayerEntityFeature.SEEK
119 SERVICE_PLAY_PRESET =
"play_preset"
120 ATTR_PRESET_NUMBER =
"preset_number"
122 SERVICE_PLAY_PRESET_SCHEMA = cv.make_entity_service_schema(
124 vol.Required(ATTR_PRESET_NUMBER): cv.positive_int,
131 entry: LinkPlayConfigEntry,
132 async_add_entities: AddEntitiesCallback,
134 """Set up a media player from a config entry."""
137 platform = entity_platform.async_get_current_platform()
138 platform.async_register_entity_service(
139 SERVICE_PLAY_PRESET, SERVICE_PLAY_PRESET_SCHEMA,
"async_play_preset"
147 """Representation of a LinkPlay media player."""
149 _attr_sound_mode_list =
list(EQUALIZER_MAP.values())
150 _attr_device_class = MediaPlayerDeviceClass.RECEIVER
151 _attr_media_content_type = MediaType.MUSIC
154 def __init__(self, bridge: LinkPlayBridge) ->
None:
155 """Initialize the LinkPlay media player."""
161 SOURCE_MAP[playing_mode]
for playing_mode
in bridge.device.playmode_support
166 """Update the state of the media player."""
168 await self.
_bridge_bridge.player.update_status()
170 except LinkPlayRequestException:
175 """Select input source."""
176 await self.
_bridge_bridge.player.set_play_mode(SOURCE_MAP_INV[source])
180 """Select sound mode."""
181 await self.
_bridge_bridge.player.set_equalizer_mode(EQUALIZER_MAP_INV[sound_mode])
185 """Mute the volume."""
187 await self.
_bridge_bridge.player.mute()
189 await self.
_bridge_bridge.player.unmute()
193 """Set volume level, range 0..1."""
194 await self.
_bridge_bridge.player.set_volume(
int(volume * 100))
198 """Send pause command."""
199 await self.
_bridge_bridge.player.pause()
203 """Send play command."""
204 await self.
_bridge_bridge.player.resume()
208 """Send stop command."""
209 await self.
_bridge_bridge.player.stop()
213 """Send next command."""
214 await self.
_bridge_bridge.player.next()
218 """Send previous command."""
219 await self.
_bridge_bridge.player.previous()
223 """Set repeat mode."""
224 await self.
_bridge_bridge.player.set_loop_mode(REPEAT_MAP_INV[repeat])
228 media_content_type: MediaType | str |
None =
None,
229 media_content_id: str |
None =
None,
231 """Return a BrowseMedia instance.
233 The BrowseMedia instance will be used by the
234 "media_player/browse_media" websocket command.
236 return await media_source.async_browse_media(
240 content_filter=
lambda item: item.media_content_type.startswith(
"audio/"),
245 self, media_type: MediaType | str, media_id: str, **kwargs: Any
247 """Play a piece of media."""
248 if media_source.is_media_source_id(media_id):
249 play_item = await media_source.async_resolve_media(
252 media_id = play_item.url
255 await self.
_bridge_bridge.player.play(url)
259 """Play preset number."""
261 await self.
_bridge_bridge.player.play_preset(preset_number)
262 except ValueError
as err:
267 """Seek to a position."""
268 await self.
_bridge_bridge.player.seek(round(position))
272 """Join `group_members` as a player group with the current player."""
274 controller: LinkPlayController = self.
hasshass.data[DOMAIN][CONTROLLER_KEY]
275 multiroom = self.
_bridge_bridge.multiroom
276 if multiroom
is None:
277 multiroom = LinkPlayMultiroom(self.
_bridge_bridge)
279 for group_member
in group_members:
282 await multiroom.add_follower(bridge)
284 await controller.discover_multirooms()
287 """Get linkplay bridge from entity_id."""
289 entity_registry = er.async_get(self.
hasshass)
292 entity_entry = entity_registry.async_get(entity_id)
296 or entity_entry.domain != Platform.MEDIA_PLAYER
297 or entity_entry.platform != DOMAIN
298 or entity_entry.config_entry_id
is None
301 translation_domain=DOMAIN,
302 translation_key=
"invalid_grouping_entity",
303 translation_placeholders={
"entity_id": entity_id},
306 config_entry = self.
hasshass.config_entries.async_get_entry(
307 entity_entry.config_entry_id
312 data: LinkPlayData = config_entry.runtime_data
317 """List of players which are grouped together."""
318 multiroom = self.
_bridge_bridge.multiroom
319 if multiroom
is not None:
320 return [multiroom.leader.device.uuid] + [
321 follower.device.uuid
for follower
in multiroom.followers
328 """Remove this player from any group."""
329 controller: LinkPlayController = self.
hasshass.data[DOMAIN][CONTROLLER_KEY]
331 multiroom = self.
_bridge_bridge.multiroom
332 if multiroom
is not None:
333 await multiroom.remove_follower(self.
_bridge_bridge)
335 await controller.discover_multirooms()
338 """Update the properties of the media player."""
348 if self.
_bridge_bridge.player.status == PlayingStatus.PLAYING:
349 if self.
_bridge_bridge.player.total_length != 0:
361 elif self.
_bridge_bridge.player.status == PlayingStatus.STOPPED: