Home Assistant Unofficial Reference 2024.12.1
media_player.py
Go to the documentation of this file.
1 """Support for interfacing to iTunes API."""
2 
3 from __future__ import annotations
4 
5 from typing import Any
6 
7 import requests
8 import voluptuous as vol
9 
11  PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA,
12  MediaPlayerEntity,
13  MediaPlayerEntityFeature,
14  MediaPlayerState,
15  MediaType,
16 )
17 from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_SSL
18 from homeassistant.core import HomeAssistant
20 from homeassistant.helpers.entity_platform import AddEntitiesCallback
21 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
22 
23 DEFAULT_NAME = "iTunes"
24 DEFAULT_PORT = 8181
25 DEFAULT_SSL = False
26 DEFAULT_TIMEOUT = 10
27 DOMAIN = "itunes"
28 
29 
30 PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend(
31  {
32  vol.Required(CONF_HOST): cv.string,
33  vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
34  vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
35  vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
36  }
37 )
38 
39 
40 class Itunes:
41  """The iTunes API client."""
42 
43  def __init__(self, host, port, use_ssl):
44  """Initialize the iTunes device."""
45  self.hosthost = host
46  self.portport = port
47  self.use_ssluse_ssl = use_ssl
48 
49  @property
50  def _base_url(self):
51  """Return the base URL for endpoints."""
52  if self.use_ssluse_ssl:
53  uri_scheme = "https://"
54  else:
55  uri_scheme = "http://"
56 
57  if self.portport:
58  return f"{uri_scheme}{self.host}:{self.port}"
59 
60  return f"{uri_scheme}{self.host}"
61 
62  def _request(self, method, path, params=None):
63  """Make the actual request and return the parsed response."""
64  url = f"{self._base_url}{path}"
65 
66  try:
67  if method == "GET":
68  response = requests.get(url, timeout=DEFAULT_TIMEOUT)
69  elif method in ("POST", "PUT"):
70  response = requests.put(url, params, timeout=DEFAULT_TIMEOUT)
71  elif method == "DELETE":
72  response = requests.delete(url, timeout=DEFAULT_TIMEOUT)
73 
74  return response.json()
75  except requests.exceptions.HTTPError:
76  return {"player_state": "error"}
77  except requests.exceptions.RequestException:
78  return {"player_state": "offline"}
79 
80  def _command(self, named_command):
81  """Make a request for a controlling command."""
82  return self._request_request("PUT", f"/{named_command}")
83 
84  def now_playing(self):
85  """Return the current state."""
86  return self._request_request("GET", "/now_playing")
87 
88  def set_volume(self, level):
89  """Set the volume and returns the current state, level 0-100."""
90  return self._request_request("PUT", "/volume", {"level": level})
91 
92  def set_muted(self, muted):
93  """Mute and returns the current state, muted True or False."""
94  return self._request_request("PUT", "/mute", {"muted": muted})
95 
96  def set_shuffle(self, shuffle):
97  """Set the shuffle mode, shuffle True or False."""
98  return self._request_request(
99  "PUT", "/shuffle", {"mode": ("songs" if shuffle else "off")}
100  )
101 
102  def play(self):
103  """Set playback to play and returns the current state."""
104  return self._command_command("play")
105 
106  def pause(self):
107  """Set playback to paused and returns the current state."""
108  return self._command_command("pause")
109 
110  def next(self):
111  """Skip to the next track and returns the current state."""
112  return self._command_command("next")
113 
114  def previous(self):
115  """Skip back and returns the current state."""
116  return self._command_command("previous")
117 
118  def stop(self):
119  """Stop playback and return the current state."""
120  return self._command_command("stop")
121 
122  def play_playlist(self, playlist_id_or_name):
123  """Set a playlist to be current and returns the current state."""
124  response = self._request_request("GET", "/playlists")
125  playlists = response.get("playlists", [])
126 
127  found_playlists = [
128  playlist
129  for playlist in playlists
130  if (playlist_id_or_name in [playlist["name"], playlist["id"]])
131  ]
132 
133  if found_playlists:
134  playlist = found_playlists[0]
135  path = f"/playlists/{playlist['id']}/play"
136  return self._request_request("PUT", path)
137 
138  raise ValueError(f"Playlist {playlist_id_or_name} not found")
139 
140  def artwork_url(self):
141  """Return a URL of the current track's album art."""
142  return f"{self._base_url}/artwork"
143 
144  def airplay_devices(self):
145  """Return a list of AirPlay devices."""
146  return self._request_request("GET", "/airplay_devices")
147 
148  def airplay_device(self, device_id):
149  """Return an AirPlay device."""
150  return self._request_request("GET", f"/airplay_devices/{device_id}")
151 
152  def toggle_airplay_device(self, device_id, toggle):
153  """Toggle airplay device on or off, id, toggle True or False."""
154  command = "on" if toggle else "off"
155  path = f"/airplay_devices/{device_id}/{command}"
156  return self._request_request("PUT", path)
157 
158  def set_volume_airplay_device(self, device_id, level):
159  """Set volume, returns current state of device, id,level 0-100."""
160  path = f"/airplay_devices/{device_id}/volume"
161  return self._request_request("PUT", path, {"level": level})
162 
163 
165  hass: HomeAssistant,
166  config: ConfigType,
167  add_entities: AddEntitiesCallback,
168  discovery_info: DiscoveryInfoType | None = None,
169 ) -> None:
170  """Set up the iTunes platform."""
171  add_entities(
172  [
173  ItunesDevice(
174  config.get(CONF_NAME),
175  config.get(CONF_HOST),
176  config.get(CONF_PORT),
177  config[CONF_SSL],
178  add_entities,
179  )
180  ]
181  )
182 
183 
185  """Representation of an iTunes API instance."""
186 
187  _attr_media_content_type = MediaType.MUSIC
188  _attr_supported_features = (
189  MediaPlayerEntityFeature.PAUSE
190  | MediaPlayerEntityFeature.VOLUME_SET
191  | MediaPlayerEntityFeature.VOLUME_MUTE
192  | MediaPlayerEntityFeature.PREVIOUS_TRACK
193  | MediaPlayerEntityFeature.NEXT_TRACK
194  | MediaPlayerEntityFeature.SEEK
195  | MediaPlayerEntityFeature.PLAY_MEDIA
196  | MediaPlayerEntityFeature.PLAY
197  | MediaPlayerEntityFeature.TURN_OFF
198  | MediaPlayerEntityFeature.SHUFFLE_SET
199  )
200 
201  def __init__(self, name, host, port, use_ssl, add_entities):
202  """Initialize the iTunes device."""
203  self._name_name = name
204  self._host_host = host
205  self._port_port = port
206  self._use_ssl_use_ssl = use_ssl
207  self._add_entities_add_entities = add_entities
208 
209  self.clientclient = Itunes(self._host_host, self._port_port, self._use_ssl_use_ssl)
210 
211  self.current_volumecurrent_volume = None
212  self.mutedmuted = None
213  self.shuffledshuffled = None
214  self.current_titlecurrent_title = None
215  self.current_albumcurrent_album = None
216  self.current_artistcurrent_artist = None
217  self.current_playlistcurrent_playlist = None
218  self.content_idcontent_id = None
219 
220  self.player_stateplayer_state = None
221 
222  self.airplay_devicesairplay_devices = {}
223 
224  self.updateupdate()
225 
226  def update_state(self, state_hash):
227  """Update all the state properties with the passed in dictionary."""
228  self.player_stateplayer_state = state_hash.get("player_state", None)
229 
230  self.current_volumecurrent_volume = state_hash.get("volume", 0)
231  self.mutedmuted = state_hash.get("muted", None)
232  self.current_titlecurrent_title = state_hash.get("name", None)
233  self.current_albumcurrent_album = state_hash.get("album", None)
234  self.current_artistcurrent_artist = state_hash.get("artist", None)
235  self.current_playlistcurrent_playlist = state_hash.get("playlist", None)
236  self.content_idcontent_id = state_hash.get("id", None)
237 
238  _shuffle = state_hash.get("shuffle", None)
239  self.shuffledshuffled = _shuffle == "songs"
240 
241  @property
242  def name(self):
243  """Return the name of the device."""
244  return self._name_name
245 
246  @property
247  def state(self):
248  """Return the state of the device."""
249  if self.player_stateplayer_state == "offline" or self.player_stateplayer_state is None:
250  return "offline"
251 
252  if self.player_stateplayer_state == "error":
253  return "error"
254 
255  if self.player_stateplayer_state == "stopped":
256  return MediaPlayerState.IDLE
257 
258  if self.player_stateplayer_state == "paused":
259  return MediaPlayerState.PAUSED
260 
261  return MediaPlayerState.PLAYING
262 
263  def update(self) -> None:
264  """Retrieve latest state."""
265  now_playing = self.clientclient.now_playing()
266  self.update_stateupdate_state(now_playing)
267 
268  found_devices = self.clientclient.airplay_devices()
269  found_devices = found_devices.get("airplay_devices", [])
270 
271  new_devices = []
272 
273  for device_data in found_devices:
274  device_id = device_data.get("id")
275 
276  if self.airplay_devicesairplay_devices.get(device_id):
277  # update it
278  airplay_device = self.airplay_devicesairplay_devices.get(device_id)
279  airplay_device.update_state(device_data)
280  else:
281  # add it
282  airplay_device = AirPlayDevice(device_id, self.clientclient)
283  airplay_device.update_state(device_data)
284  self.airplay_devicesairplay_devices[device_id] = airplay_device
285  new_devices.append(airplay_device)
286 
287  if new_devices:
288  self._add_entities_add_entities(new_devices)
289 
290  @property
291  def is_volume_muted(self):
292  """Boolean if volume is currently muted."""
293  return self.mutedmuted
294 
295  @property
296  def volume_level(self):
297  """Volume level of the media player (0..1)."""
298  return self.current_volumecurrent_volume / 100.0
299 
300  @property
301  def media_content_id(self):
302  """Content ID of current playing media."""
303  return self.content_idcontent_id
304 
305  @property
306  def media_image_url(self):
307  """Image url of current playing media."""
308  if (
309  self.player_stateplayer_state
310  in {
311  MediaPlayerState.PLAYING,
312  MediaPlayerState.IDLE,
313  MediaPlayerState.PAUSED,
314  }
315  and self.current_titlecurrent_title is not None
316  ):
317  return f"{self.client.artwork_url()}?id={self.content_id}"
318 
319  return (
320  "https://cloud.githubusercontent.com/assets/260/9829355"
321  "/33fab972-58cf-11e5-8ea2-2ca74bdaae40.png"
322  )
323 
324  @property
325  def media_title(self):
326  """Title of current playing media."""
327  return self.current_titlecurrent_title
328 
329  @property
330  def media_artist(self):
331  """Artist of current playing media (Music track only)."""
332  return self.current_artistcurrent_artist
333 
334  @property
335  def media_album_name(self):
336  """Album of current playing media (Music track only)."""
337  return self.current_albumcurrent_album
338 
339  @property
340  def media_playlist(self):
341  """Title of the currently playing playlist."""
342  return self.current_playlistcurrent_playlist
343 
344  @property
345  def shuffle(self):
346  """Boolean if shuffle is enabled."""
347  return self.shuffledshuffled
348 
349  def set_volume_level(self, volume: float) -> None:
350  """Set volume level, range 0..1."""
351  response = self.clientclient.set_volume(int(volume * 100))
352  self.update_stateupdate_state(response)
353 
354  def mute_volume(self, mute: bool) -> None:
355  """Mute (true) or unmute (false) media player."""
356  response = self.clientclient.set_muted(mute)
357  self.update_stateupdate_state(response)
358 
359  def set_shuffle(self, shuffle: bool) -> None:
360  """Shuffle (true) or no shuffle (false) media player."""
361  response = self.clientclient.set_shuffle(shuffle)
362  self.update_stateupdate_state(response)
363 
364  def media_play(self) -> None:
365  """Send media_play command to media player."""
366  response = self.clientclient.play()
367  self.update_stateupdate_state(response)
368 
369  def media_pause(self) -> None:
370  """Send media_pause command to media player."""
371  response = self.clientclient.pause()
372  self.update_stateupdate_state(response)
373 
374  def media_next_track(self) -> None:
375  """Send media_next command to media player."""
376  response = self.clientclient.next()
377  self.update_stateupdate_state(response)
378 
379  def media_previous_track(self) -> None:
380  """Send media_previous command media player."""
381  response = self.clientclient.previous()
382  self.update_stateupdate_state(response)
383 
385  self, media_type: MediaType | str, media_id: str, **kwargs: Any
386  ) -> None:
387  """Send the play_media command to the media player."""
388  if media_type == MediaType.PLAYLIST:
389  response = self.clientclient.play_playlist(media_id)
390  self.update_stateupdate_state(response)
391 
392  def turn_off(self) -> None:
393  """Turn the media player off."""
394  response = self.clientclient.stop()
395  self.update_stateupdate_state(response)
396 
397 
399  """Representation an AirPlay device via an iTunes API instance."""
400 
401  _attr_media_content_type = MediaType.MUSIC
402  _attr_supported_features = (
403  MediaPlayerEntityFeature.VOLUME_SET
404  | MediaPlayerEntityFeature.TURN_ON
405  | MediaPlayerEntityFeature.TURN_OFF
406  )
407 
408  def __init__(self, device_id, client):
409  """Initialize the AirPlay device."""
410  self._id_id = device_id
411  self.clientclient = client
412  self.device_namedevice_name = "AirPlay"
413  self.kindkind = None
414  self.activeactive = False
415  self.selectedselected = False
416  self.volumevolume = 0
417  self.supports_audiosupports_audio = False
418  self.supports_videosupports_video = False
419  self.player_stateplayer_state = None
420 
421  def update_state(self, state_hash):
422  """Update all the state properties with the passed in dictionary."""
423  if "player_state" in state_hash:
424  self.player_stateplayer_state = state_hash.get("player_state", None)
425 
426  if "name" in state_hash:
427  name = state_hash.get("name", "")
428  self.device_namedevice_name = f"{name} AirTunes Speaker".strip()
429 
430  if "kind" in state_hash:
431  self.kindkind = state_hash.get("kind", None)
432 
433  if "active" in state_hash:
434  self.activeactive = state_hash.get("active", None)
435 
436  if "selected" in state_hash:
437  self.selectedselected = state_hash.get("selected", None)
438 
439  if "sound_volume" in state_hash:
440  self.volumevolume = state_hash.get("sound_volume", 0)
441 
442  if "supports_audio" in state_hash:
443  self.supports_audiosupports_audio = state_hash.get("supports_audio", None)
444 
445  if "supports_video" in state_hash:
446  self.supports_videosupports_video = state_hash.get("supports_video", None)
447 
448  @property
449  def name(self):
450  """Return the name of the device."""
451  return self.device_namedevice_name
452 
453  @property
454  def icon(self):
455  """Return the icon to use in the frontend, if any."""
456  if self.selectedselected is True:
457  return "mdi:volume-high"
458 
459  return "mdi:volume-off"
460 
461  @property
462  def state(self) -> MediaPlayerState:
463  """Return the state of the device."""
464  if self.selectedselected is True:
465  return MediaPlayerState.ON
466 
467  return MediaPlayerState.OFF
468 
469  def update(self) -> None:
470  """Retrieve latest state."""
471 
472  @property
473  def volume_level(self):
474  """Return the volume."""
475  return float(self.volumevolume) / 100.0
476 
477  def set_volume_level(self, volume: float) -> None:
478  """Set volume level, range 0..1."""
479  volume = int(volume * 100)
480  response = self.clientclient.set_volume_airplay_device(self._id_id, volume)
481  self.update_stateupdate_state(response)
482 
483  def turn_on(self) -> None:
484  """Select AirPlay."""
485  self.update_stateupdate_state({"selected": True})
486  self.schedule_update_ha_stateschedule_update_ha_state()
487  response = self.clientclient.toggle_airplay_device(self._id_id, True)
488  self.update_stateupdate_state(response)
489 
490  def turn_off(self) -> None:
491  """Deselect AirPlay."""
492  self.update_stateupdate_state({"selected": False})
493  self.schedule_update_ha_stateschedule_update_ha_state()
494  response = self.clientclient.toggle_airplay_device(self._id_id, False)
495  self.update_stateupdate_state(response)
def __init__(self, name, host, port, use_ssl, add_entities)
None play_media(self, MediaType|str media_type, str media_id, **Any kwargs)
def _request(self, method, path, params=None)
Definition: media_player.py:62
def toggle_airplay_device(self, device_id, toggle)
def set_volume_airplay_device(self, device_id, level)
def play_playlist(self, playlist_id_or_name)
None schedule_update_ha_state(self, bool force_refresh=False)
Definition: entity.py:1244
None add_entities(AsusWrtRouter router, AddEntitiesCallback async_add_entities, set[str] tracked)
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback add_entities, DiscoveryInfoType|None discovery_info=None)