Home Assistant Unofficial Reference 2024.12.1
media_player.py
Go to the documentation of this file.
1 """MediaPlayer platform for SlimProto Player integration."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from typing import Any
7 
8 from aioslimproto.client import PlayerState, SlimClient
9 from aioslimproto.models import EventType, SlimEvent
10 from aioslimproto.server import SlimServer
11 
12 from homeassistant.components import media_source
14  BrowseMedia,
15  MediaPlayerDeviceClass,
16  MediaPlayerEntity,
17  MediaPlayerEntityFeature,
18  MediaPlayerState,
19  MediaType,
20  async_process_play_media_url,
21 )
22 from homeassistant.config_entries import ConfigEntry
23 from homeassistant.core import HomeAssistant, callback
24 from homeassistant.helpers.device_registry import DeviceInfo
25 from homeassistant.helpers.entity_platform import AddEntitiesCallback
26 from homeassistant.util.dt import utcnow
27 
28 from .const import DEFAULT_NAME, DOMAIN, PLAYER_EVENT
29 
30 STATE_MAPPING = {
31  PlayerState.STOPPED: MediaPlayerState.IDLE,
32  PlayerState.PLAYING: MediaPlayerState.PLAYING,
33  PlayerState.BUFFER_READY: MediaPlayerState.PLAYING,
34  PlayerState.BUFFERING: MediaPlayerState.PLAYING,
35  PlayerState.PAUSED: MediaPlayerState.PAUSED,
36 }
37 
38 
40  hass: HomeAssistant,
41  config_entry: ConfigEntry,
42  async_add_entities: AddEntitiesCallback,
43 ) -> None:
44  """Set up SlimProto MediaPlayer(s) from Config Entry."""
45  slimserver: SlimServer = hass.data[DOMAIN]
46  added_ids = set()
47 
48  async def async_add_player(player: SlimClient) -> None:
49  """Add MediaPlayerEntity from SlimClient."""
50  # we delay adding the player a small bit because the player name may be received
51  # just a bit after connect. This way we can create a device reg entry with the
52  # correct name the name will either be available within a few milliseconds after
53  # connect or not at all (its an optional data packet)
54  for _ in range(10):
55  if player.player_id not in player.name:
56  break
57  await asyncio.sleep(0.1)
58  async_add_entities([SlimProtoPlayer(slimserver, player)])
59 
60  async def on_slim_event(event: SlimEvent) -> None:
61  """Handle player added/connected event."""
62  if event.player_id in added_ids:
63  return
64  added_ids.add(event.player_id)
65  player = slimserver.get_player(event.player_id)
66  await async_add_player(player)
67 
68  # register listener for new players
69  config_entry.async_on_unload(
70  slimserver.subscribe(on_slim_event, EventType.PLAYER_CONNECTED)
71  )
72 
73  # add all current items in controller
74  await asyncio.gather(*(async_add_player(player) for player in slimserver.players))
75 
76 
78  """Representation of MediaPlayerEntity from SlimProto Player."""
79 
80  _attr_has_entity_name = True
81  _attr_should_poll = False
82  _attr_supported_features = (
83  MediaPlayerEntityFeature.PAUSE
84  | MediaPlayerEntityFeature.VOLUME_SET
85  | MediaPlayerEntityFeature.STOP
86  | MediaPlayerEntityFeature.TURN_ON
87  | MediaPlayerEntityFeature.TURN_OFF
88  | MediaPlayerEntityFeature.PLAY
89  | MediaPlayerEntityFeature.PLAY_MEDIA
90  | MediaPlayerEntityFeature.VOLUME_MUTE
91  | MediaPlayerEntityFeature.BROWSE_MEDIA
92  )
93  _attr_device_class = MediaPlayerDeviceClass.SPEAKER
94  _attr_name = None
95 
96  def __init__(self, slimserver: SlimServer, player: SlimClient) -> None:
97  """Initialize MediaPlayer entity."""
98  self.slimserverslimserver = slimserver
99  self.playerplayer = player
100  self._attr_unique_id_attr_unique_id = player.player_id
101  self._attr_device_info_attr_device_info = DeviceInfo(
102  identifiers={(DOMAIN, self.playerplayer.player_id)},
103  manufacturer=DEFAULT_NAME,
104  model=self.playerplayer.device_model or self.playerplayer.device_type,
105  name=self.playerplayer.name,
106  hw_version=self.playerplayer.firmware,
107  )
108  # PiCore + SqueezeESP32 player has web interface
109  if "-pCP" in self.playerplayer.firmware or self.playerplayer.device_model == "SqueezeESP32":
110  self._attr_device_info_attr_device_info["configuration_url"] = (
111  f"http://{self.player.device_address}"
112  )
113  self.update_attributesupdate_attributes()
114 
115  async def async_added_to_hass(self) -> None:
116  """Register callbacks."""
117  self.update_attributesupdate_attributes()
118  self.async_on_removeasync_on_remove(
119  self.slimserverslimserver.subscribe(
120  self._on_slim_event_on_slim_event,
121  (
122  EventType.PLAYER_UPDATED,
123  EventType.PLAYER_CONNECTED,
124  EventType.PLAYER_DISCONNECTED,
125  EventType.PLAYER_NAME_RECEIVED,
126  EventType.PLAYER_CLI_EVENT,
127  ),
128  player_filter=self.playerplayer.player_id,
129  )
130  )
131 
132  @property
133  def available(self) -> bool:
134  """Return availability of entity."""
135  return self.playerplayer.connected
136 
137  @property
138  def state(self) -> MediaPlayerState:
139  """Return current state."""
140  if not self.playerplayer.powered:
141  return MediaPlayerState.OFF
142  return STATE_MAPPING[self.playerplayer.state]
143 
144  @callback
145  def update_attributes(self) -> None:
146  """Handle player updates."""
147  self._attr_volume_level_attr_volume_level = self.playerplayer.volume_level / 100
148  self._attr_is_volume_muted_attr_is_volume_muted = self.playerplayer.muted
149  self._attr_media_position_attr_media_position = self.playerplayer.elapsed_seconds
150  self._attr_media_position_updated_at_attr_media_position_updated_at = utcnow()
151  if (current_media := self.playerplayer.current_media) and (
152  metadata := current_media.metadata
153  ):
154  self._attr_media_content_id_attr_media_content_id = metadata.get("item_id", current_media.url)
155  self._attr_media_artist_attr_media_artist = metadata.get("artist")
156  self._attr_media_album_name_attr_media_album_name = metadata.get("album")
157  self._attr_media_title_attr_media_title = metadata.get("title")
158  self._attr_media_image_url_attr_media_image_url = metadata.get("image_url")
159  else:
160  self._attr_media_content_id_attr_media_content_id = current_media.url if current_media else None
161  self._attr_media_artist_attr_media_artist = None
162  self._attr_media_album_name_attr_media_album_name = None
163  self._attr_media_title_attr_media_title = None
164  self._attr_media_image_url_attr_media_image_url = None
165  self._attr_media_content_type_attr_media_content_type = "music"
166 
167  async def async_media_play(self) -> None:
168  """Send play command to device."""
169  await self.playerplayer.play()
170 
171  async def async_media_pause(self) -> None:
172  """Send pause command to device."""
173  await self.playerplayer.pause()
174 
175  async def async_media_stop(self) -> None:
176  """Send stop command to device."""
177  await self.playerplayer.stop()
178 
179  async def async_set_volume_level(self, volume: float) -> None:
180  """Send new volume_level to device."""
181  volume = round(volume * 100)
182  await self.playerplayer.volume_set(volume)
183 
184  async def async_mute_volume(self, mute: bool) -> None:
185  """Mute the volume."""
186  await self.playerplayer.mute(mute)
187 
188  async def async_turn_on(self) -> None:
189  """Turn on device."""
190  await self.playerplayer.power(True)
191 
192  async def async_turn_off(self) -> None:
193  """Turn off device."""
194  await self.playerplayer.power(False)
195 
196  async def async_play_media(
197  self, media_type: MediaType | str, media_id: str, **kwargs: Any
198  ) -> None:
199  """Send the play_media command to the media player."""
200  to_send_media_type: str | None = media_type
201  # Handle media_source
202  if media_source.is_media_source_id(media_id):
203  sourced_media = await media_source.async_resolve_media(
204  self.hasshass, media_id, self.entity_identity_id
205  )
206  media_id = sourced_media.url
207  to_send_media_type = sourced_media.mime_type
208 
209  if to_send_media_type and not to_send_media_type.startswith("audio/"):
210  to_send_media_type = None
211  media_id = async_process_play_media_url(self.hasshass, media_id)
212 
213  await self.playerplayer.play_url(media_id, mime_type=to_send_media_type)
214 
216  self,
217  media_content_type: MediaType | str | None = None,
218  media_content_id: str | None = None,
219  ) -> BrowseMedia:
220  """Implement the websocket media browsing helper."""
221  return await media_source.async_browse_media(
222  self.hasshass,
223  media_content_id,
224  content_filter=lambda item: item.media_content_type.startswith("audio/"),
225  )
226 
227  async def _on_slim_event(self, event: SlimEvent) -> None:
228  """Call when we receive an event from SlimProto."""
229  if event.type == EventType.PLAYER_CONNECTED:
230  # player reconnected, update our player object
231  self.playerplayer = self.slimserverslimserver.get_player(event.player_id)
232  if event.type == EventType.PLAYER_CLI_EVENT:
233  # rpc event from player such as a button press,
234  # forward on the eventbus for others to handle
235  dev_id = self.registry_entryregistry_entry.device_id if self.registry_entryregistry_entry else None
236  evt_data = {
237  **event.data,
238  "entity_id": self.entity_identity_id,
239  "device_id": dev_id,
240  }
241  self.hasshass.bus.async_fire(PLAYER_EVENT, evt_data)
242  return
243  self.update_attributesupdate_attributes()
244  self.async_write_ha_stateasync_write_ha_state()
None async_play_media(self, MediaType|str media_type, str media_id, **Any kwargs)
None __init__(self, SlimServer slimserver, SlimClient player)
Definition: media_player.py:96
BrowseMedia async_browse_media(self, MediaType|str|None media_content_type=None, str|None media_content_id=None)
None async_on_remove(self, CALLBACK_TYPE func)
Definition: entity.py:1331
str async_process_play_media_url(HomeAssistant hass, str media_content_id, *bool allow_relative_url=False, bool for_supervisor_network=False)
Definition: browse_media.py:36
Callable[[], None] subscribe(HomeAssistant hass, str topic, MessageCallbackType msg_callback, int qos=DEFAULT_QOS, str encoding="utf-8")
Definition: client.py:247
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: media_player.py:43