Home Assistant Unofficial Reference 2024.12.1
media_player.py
Go to the documentation of this file.
1 """Support to interface with the Emby API."""
2 
3 from __future__ import annotations
4 
5 import logging
6 
7 from pyemby import EmbyServer
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 (
18  CONF_API_KEY,
19  CONF_HOST,
20  CONF_PORT,
21  CONF_SSL,
22  DEVICE_DEFAULT_NAME,
23  EVENT_HOMEASSISTANT_START,
24  EVENT_HOMEASSISTANT_STOP,
25 )
26 from homeassistant.core import HomeAssistant, callback
28 from homeassistant.helpers.entity_platform import AddEntitiesCallback
29 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
30 import homeassistant.util.dt as dt_util
31 
32 _LOGGER = logging.getLogger(__name__)
33 
34 MEDIA_TYPE_TRAILER = "trailer"
35 
36 DEFAULT_HOST = "localhost"
37 DEFAULT_PORT = 8096
38 DEFAULT_SSL_PORT = 8920
39 DEFAULT_SSL = False
40 
41 SUPPORT_EMBY = (
42  MediaPlayerEntityFeature.PAUSE
43  | MediaPlayerEntityFeature.PREVIOUS_TRACK
44  | MediaPlayerEntityFeature.NEXT_TRACK
45  | MediaPlayerEntityFeature.STOP
46  | MediaPlayerEntityFeature.SEEK
47  | MediaPlayerEntityFeature.PLAY
48 )
49 
50 PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend(
51  {
52  vol.Required(CONF_API_KEY): cv.string,
53  vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
54  vol.Optional(CONF_PORT): cv.port,
55  vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
56  }
57 )
58 
59 
61  hass: HomeAssistant,
62  config: ConfigType,
63  async_add_entities: AddEntitiesCallback,
64  discovery_info: DiscoveryInfoType | None = None,
65 ) -> None:
66  """Set up the Emby platform."""
67 
68  host = config.get(CONF_HOST)
69  key = config.get(CONF_API_KEY)
70  port = config.get(CONF_PORT)
71  ssl = config[CONF_SSL]
72 
73  if port is None:
74  port = DEFAULT_SSL_PORT if ssl else DEFAULT_PORT
75 
76  _LOGGER.debug("Setting up Emby server at: %s:%s", host, port)
77 
78  emby = EmbyServer(host, key, port, ssl, hass.loop)
79 
80  active_emby_devices: dict[str, EmbyDevice] = {}
81  inactive_emby_devices: dict[str, EmbyDevice] = {}
82 
83  @callback
84  def device_update_callback(data):
85  """Handle devices which are added to Emby."""
86  new_devices = []
87  active_devices = []
88  for dev_id, dev in emby.devices.items():
89  active_devices.append(dev_id)
90  if (
91  dev_id not in active_emby_devices
92  and dev_id not in inactive_emby_devices
93  ):
94  new = EmbyDevice(emby, dev_id)
95  active_emby_devices[dev_id] = new
96  new_devices.append(new)
97 
98  elif dev_id in inactive_emby_devices and dev.state != "Off":
99  add = inactive_emby_devices.pop(dev_id)
100  active_emby_devices[dev_id] = add
101  _LOGGER.debug("Showing %s, item: %s", dev_id, add)
102  add.set_available(True)
103 
104  if new_devices:
105  _LOGGER.debug("Adding new devices: %s", new_devices)
106  async_add_entities(new_devices, True)
107 
108  @callback
109  def device_removal_callback(data):
110  """Handle the removal of devices from Emby."""
111  if data in active_emby_devices:
112  rem = active_emby_devices.pop(data)
113  inactive_emby_devices[data] = rem
114  _LOGGER.debug("Inactive %s, item: %s", data, rem)
115  rem.set_available(False)
116 
117  @callback
118  def start_emby(event):
119  """Start Emby connection."""
120  emby.start()
121 
122  async def stop_emby(event):
123  """Stop Emby connection."""
124  await emby.stop()
125 
126  emby.add_new_devices_callback(device_update_callback)
127  emby.add_stale_devices_callback(device_removal_callback)
128 
129  hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_emby)
130  hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_emby)
131 
132 
134  """Representation of an Emby device."""
135 
136  _attr_should_poll = False
137 
138  def __init__(self, emby, device_id):
139  """Initialize the Emby device."""
140  _LOGGER.debug("New Emby Device initialized with ID: %s", device_id)
141  self.embyemby = emby
142  self.device_iddevice_id = device_id
143  self.devicedevice = self.embyemby.devices[self.device_iddevice_id]
144 
145  self.media_status_last_positionmedia_status_last_position = None
146  self.media_status_receivedmedia_status_received = None
147 
148  self._attr_unique_id_attr_unique_id = device_id
149 
150  async def async_added_to_hass(self) -> None:
151  """Register callback."""
152  self.embyemby.add_update_callback(self.async_update_callbackasync_update_callback, self.device_iddevice_id)
153 
154  @callback
155  def async_update_callback(self, msg):
156  """Handle device updates."""
157  # Check if we should update progress
158  if self.devicedevice.media_position:
159  if self.devicedevice.media_position != self.media_status_last_positionmedia_status_last_position:
160  self.media_status_last_positionmedia_status_last_position = self.devicedevice.media_position
161  self.media_status_receivedmedia_status_received = dt_util.utcnow()
162  elif not self.devicedevice.is_nowplaying:
163  # No position, but we have an old value and are still playing
164  self.media_status_last_positionmedia_status_last_position = None
165  self.media_status_receivedmedia_status_received = None
166 
167  self.async_write_ha_stateasync_write_ha_state()
168 
169  def set_available(self, value: bool) -> None:
170  """Set available property."""
171  self._attr_available_attr_available = value
172 
173  @property
175  """Return control ability."""
176  return self.devicedevice.supports_remote_control
177 
178  @property
179  def name(self):
180  """Return the name of the device."""
181  return f"Emby {self.device.name}" or DEVICE_DEFAULT_NAME
182 
183  @property
184  def state(self) -> MediaPlayerState | None:
185  """Return the state of the device."""
186  state = self.devicedevice.state
187  if state == "Paused":
188  return MediaPlayerState.PAUSED
189  if state == "Playing":
190  return MediaPlayerState.PLAYING
191  if state == "Idle":
192  return MediaPlayerState.IDLE
193  if state == "Off":
194  return MediaPlayerState.OFF
195  return None
196 
197  @property
198  def app_name(self):
199  """Return current user as app_name."""
200  # Ideally the media_player object would have a user property.
201  return self.devicedevice.username
202 
203  @property
204  def media_content_id(self):
205  """Content ID of current playing media."""
206  return self.devicedevice.media_id
207 
208  @property
209  def media_content_type(self) -> MediaType | str | None:
210  """Content type of current playing media."""
211  media_type = self.devicedevice.media_type
212  if media_type == "Episode":
213  return MediaType.TVSHOW
214  if media_type == "Movie":
215  return MediaType.MOVIE
216  if media_type == "Trailer":
217  return MEDIA_TYPE_TRAILER
218  if media_type == "Music":
219  return MediaType.MUSIC
220  if media_type == "Video":
221  return MediaType.VIDEO
222  if media_type == "Audio":
223  return MediaType.MUSIC
224  if media_type == "TvChannel":
225  return MediaType.CHANNEL
226  return None
227 
228  @property
229  def media_duration(self):
230  """Return the duration of current playing media in seconds."""
231  return self.devicedevice.media_runtime
232 
233  @property
234  def media_position(self):
235  """Return the position of current playing media in seconds."""
236  return self.media_status_last_positionmedia_status_last_position
237 
238  @property
240  """When was the position of the current playing media valid.
241 
242  Returns value from homeassistant.util.dt.utcnow().
243  """
244  return self.media_status_receivedmedia_status_received
245 
246  @property
247  def media_image_url(self):
248  """Return the image URL of current playing media."""
249  return self.devicedevice.media_image_url
250 
251  @property
252  def media_title(self):
253  """Return the title of current playing media."""
254  return self.devicedevice.media_title
255 
256  @property
257  def media_season(self):
258  """Season of current playing media (TV Show only)."""
259  return self.devicedevice.media_season
260 
261  @property
263  """Return the title of the series of current playing media (TV)."""
264  return self.devicedevice.media_series_title
265 
266  @property
267  def media_episode(self):
268  """Return the episode of current playing media (TV only)."""
269  return self.devicedevice.media_episode
270 
271  @property
272  def media_album_name(self):
273  """Return the album name of current playing media (Music only)."""
274  return self.devicedevice.media_album_name
275 
276  @property
277  def media_artist(self):
278  """Return the artist of current playing media (Music track only)."""
279  return self.devicedevice.media_artist
280 
281  @property
283  """Return the album artist of current playing media (Music only)."""
284  return self.devicedevice.media_album_artist
285 
286  @property
287  def supported_features(self) -> MediaPlayerEntityFeature:
288  """Flag media player features that are supported."""
289  if self.supports_remote_controlsupports_remote_control:
290  return SUPPORT_EMBY
291  return MediaPlayerEntityFeature(0)
292 
293  async def async_media_play(self) -> None:
294  """Play media."""
295  await self.devicedevice.media_play()
296 
297  async def async_media_pause(self) -> None:
298  """Pause the media player."""
299  await self.devicedevice.media_pause()
300 
301  async def async_media_stop(self) -> None:
302  """Stop the media player."""
303  await self.devicedevice.media_stop()
304 
305  async def async_media_next_track(self) -> None:
306  """Send next track command."""
307  await self.devicedevice.media_next()
308 
309  async def async_media_previous_track(self) -> None:
310  """Send next track command."""
311  await self.devicedevice.media_previous()
312 
313  async def async_media_seek(self, position: float) -> None:
314  """Send seek command."""
315  await self.devicedevice.media_seek(position)
MediaPlayerEntityFeature supported_features(self)
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
Definition: media_player.py:65