1 """Support to interact with a Music Player Daemon."""
3 from __future__
import annotations
6 from contextlib
import asynccontextmanager, suppress
7 from datetime
import timedelta
11 from socket
import gaierror
12 from typing
import Any
15 from mpd.asyncio
import MPDClient
16 import voluptuous
as vol
20 PLATFORM_SCHEMA
as MEDIA_PLAYER_PLATFORM_SCHEMA,
23 MediaPlayerEntityFeature,
27 async_process_play_media_url,
41 from .const
import DOMAIN, LOGGER
49 MediaPlayerEntityFeature.PAUSE
50 | MediaPlayerEntityFeature.PREVIOUS_TRACK
51 | MediaPlayerEntityFeature.NEXT_TRACK
52 | MediaPlayerEntityFeature.PLAY_MEDIA
53 | MediaPlayerEntityFeature.PLAY
54 | MediaPlayerEntityFeature.CLEAR_PLAYLIST
55 | MediaPlayerEntityFeature.REPEAT_SET
56 | MediaPlayerEntityFeature.SHUFFLE_SET
57 | MediaPlayerEntityFeature.SEEK
58 | MediaPlayerEntityFeature.STOP
59 | MediaPlayerEntityFeature.TURN_OFF
60 | MediaPlayerEntityFeature.TURN_ON
61 | MediaPlayerEntityFeature.BROWSE_MEDIA
64 PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend(
66 vol.Required(CONF_HOST): cv.string,
67 vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
68 vol.Optional(CONF_PASSWORD): cv.string,
69 vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
77 async_add_entities: AddEntitiesCallback,
78 discovery_info: DiscoveryInfoType |
None =
None,
80 """Set up the MPD platform."""
82 result = await hass.config_entries.flow.async_init(
84 context={
"source": SOURCE_IMPORT},
88 result[
"type"]
is FlowResultType.CREATE_ENTRY
89 or result[
"reason"] ==
"already_configured"
94 f
"deprecated_yaml_{DOMAIN}",
95 breaks_in_ha_version=
"2025.1.0",
98 severity=IssueSeverity.WARNING,
99 translation_key=
"deprecated_yaml",
100 translation_placeholders={
102 "integration_title":
"Music Player Daemon",
109 f
"deprecated_yaml_import_issue_{result['reason']}",
110 breaks_in_ha_version=
"2025.1.0",
113 severity=IssueSeverity.WARNING,
114 translation_key=f
"deprecated_yaml_import_issue_{result['reason']}",
115 translation_placeholders={
117 "integration_title":
"Music Player Daemon",
123 hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
125 """Set up media player from config_entry."""
130 entry.data[CONF_HOST],
131 entry.data[CONF_PORT],
132 entry.data.get(CONF_PASSWORD),
141 """Representation of a MPD server."""
143 _attr_media_content_type = MediaType.MUSIC
144 _attr_has_entity_name =
True
148 self, server: str, port: int, password: str |
None, unique_id: str
150 """Initialize the MPD device."""
155 identifiers={(DOMAIN, unique_id)},
156 entry_type=DeviceEntryType.SERVICE,
160 self.
_status_status: dict[str, Any] = {}
170 self.
_client_client.timeout = 30
171 self.
_client_client.idletimeout = 10
180 """Handle MPD connect and disconnect."""
187 async
with asyncio.timeout(self.
_client_client.timeout + 5):
189 except TimeoutError
as error:
192 raise TimeoutError(
"Connection attempt timed out")
from error
193 if self.
passwordpassword
is not None:
205 log_level = logging.DEBUG
207 log_level = logging.WARNING
209 log_level,
"Error connecting to '%s': %s", self.
serverserver, error
218 with suppress(mpd.ConnectionError):
219 self.
_client_client.disconnect()
222 """Get the latest data from MPD and update the state."""
229 if (position := self.
_status_status.
get(
"elapsed"))
is None:
232 if isinstance(position, str)
and ":" in position:
233 position = position.split(
":")[0]
240 except (mpd.ConnectionError, ValueError)
as error:
241 LOGGER.debug(
"Error updating status: %s", error)
244 def state(self) -> MediaPlayerState:
245 """Return the media state."""
247 return MediaPlayerState.OFF
248 if self.
_status_status.
get(
"state") ==
"play":
249 return MediaPlayerState.PLAYING
250 if self.
_status_status.
get(
"state") ==
"pause":
251 return MediaPlayerState.PAUSED
252 if self.
_status_status.
get(
"state") ==
"stop":
253 return MediaPlayerState.OFF
255 return MediaPlayerState.OFF
259 """Return the content ID of current playing media."""
264 """Return the duration of current playing media in seconds."""
266 return currentsong_time
268 time_from_status = self.
_status_status.
get(
"time")
269 if isinstance(time_from_status, str)
and ":" in time_from_status:
270 return time_from_status.split(
":")[1]
276 """Return the title of current playing media."""
281 if name
is None and title
is None:
282 if file_name
is None:
284 return os.path.basename(file_name)
290 return f
"{name}: {title}"
294 """Return the artist of current playing media (Music track only)."""
296 if isinstance(artists, list):
297 return ", ".join(artists)
302 """Return the album of current playing media (Music track only)."""
307 """Hash value for media image."""
311 """Fetch media image of current playing track."""
316 with suppress(mpd.ConnectionError):
321 image = bytes(response[
"binary"])
328 """Update the hash value for the media image."""
343 bytes(response[
"binary"])
357 with suppress(mpd.ConnectionError):
358 commands =
list(await self.
_client_client.commands())
359 can_albumart =
"albumart" in commands
360 can_readpicture =
"readpicture" in commands
367 with suppress(mpd.ConnectionError):
368 response = await self.
_client_client.readpicture(file)
369 except mpd.CommandError
as error:
370 if error.errno
is not mpd.FailureResponseCode.NO_EXIST:
372 "Retrieving artwork through `readpicture` command failed: %s",
377 if can_albumart
and not response:
379 with suppress(mpd.ConnectionError):
380 response = await self.
_client_client.albumart(file)
381 except mpd.CommandError
as error:
382 if error.errno
is not mpd.FailureResponseCode.NO_EXIST:
384 "Retrieving artwork through `albumart` command failed: %s",
396 """Return the volume level."""
397 if "volume" in self.
_status_status:
398 return int(self.
_status_status[
"volume"]) / 100
403 """Flag media player features that are supported."""
407 supported = SUPPORT_MPD
408 if "volume" in self.
_status_status:
410 MediaPlayerEntityFeature.VOLUME_SET
411 | MediaPlayerEntityFeature.VOLUME_STEP
412 | MediaPlayerEntityFeature.VOLUME_MUTE
415 supported |= MediaPlayerEntityFeature.SELECT_SOURCE
421 """Name of the current input source."""
425 """Choose a different available playlist and play it."""
428 @Throttle(PLAYLIST_UPDATE_INTERVAL)
430 """Update available MPD playlists."""
433 with suppress(mpd.ConnectionError):
434 for playlist_data
in await self.
_client_client.listplaylists():
436 except mpd.CommandError
as error:
438 LOGGER.warning(
"Playlists could not be updated: %s:", error)
441 """Set volume of media player."""
443 if "volume" in self.
_status_status:
444 await self.
_client_client.setvol(
int(volume * 100))
447 """Service to send the MPD the command for volume up."""
449 if "volume" in self.
_status_status:
450 current_volume =
int(self.
_status_status[
"volume"])
452 if current_volume <= 100:
453 self.
_client_client.setvol(current_volume + 5)
456 """Service to send the MPD the command for volume down."""
458 if "volume" in self.
_status_status:
459 current_volume =
int(self.
_status_status[
"volume"])
461 if current_volume >= 0:
462 await self.
_client_client.setvol(current_volume - 5)
465 """Service to send the MPD the command for play/pause."""
467 if self.
_status_status.
get(
"state") ==
"pause":
468 await self.
_client_client.pause(0)
470 await self.
_client_client.play()
473 """Service to send the MPD the command for play/pause."""
475 await self.
_client_client.pause(1)
478 """Service to send the MPD the command for stop."""
480 await self.
_client_client.stop()
483 """Service to send the MPD the command for next track."""
485 await self.
_client_client.next()
488 """Service to send the MPD the command for previous track."""
490 await self.
_client_client.previous()
493 """Mute. Emulated with set_volume_level."""
494 if "volume" in self.
_status_status:
503 self, media_type: MediaType | str, media_id: str, **kwargs: Any
505 """Send the media player the command for playing a playlist."""
507 if media_source.is_media_source_id(media_id):
508 media_type = MediaType.MUSIC
509 play_item = await media_source.async_resolve_media(
514 if media_type == MediaType.PLAYLIST:
515 LOGGER.debug(
"Playing playlist: %s", media_id)
520 LOGGER.warning(
"Unknown playlist name %s", media_id)
521 await self.
_client_client.clear()
522 await self.
_client_client.load(media_id)
523 await self.
_client_client.play()
525 await self.
_client_client.clear()
528 await self.
_client_client.play()
532 """Return current repeat mode."""
535 return RepeatMode.ONE
536 return RepeatMode.ALL
537 return RepeatMode.OFF
540 """Set repeat mode."""
542 if repeat == RepeatMode.OFF:
544 await self.
_client_client.single(0)
547 if repeat == RepeatMode.ONE:
548 await self.
_client_client.single(1)
550 await self.
_client_client.single(0)
554 """Boolean if shuffle is enabled."""
558 """Enable/disable shuffle mode."""
560 await self.
_client_client.random(
int(shuffle))
563 """Service to send the MPD the command to stop playing."""
565 await self.
_client_client.stop()
568 """Service to send the MPD the command to start playing."""
570 await self.
_client_client.play()
574 """Clear players playlist."""
576 await self.
_client_client.clear()
579 """Send seek command."""
581 await self.
_client_client.seekcur(position)
585 media_content_type: MediaType | str |
None =
None,
586 media_content_id: str |
None =
None,
588 """Implement the websocket media browsing helper."""
590 return await media_source.async_browse_media(
593 content_filter=
lambda item: item.media_content_type.startswith(
bool add(self, _T matcher)
web.Response get(self, web.Request request, str config_key)
None async_create_issue(HomeAssistant hass, str entry_id)