Home Assistant Unofficial Reference 2024.12.1
media_player.py
Go to the documentation of this file.
1 """Platform allowing several media players to be grouped into one media player."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable, Mapping
6 from contextlib import suppress
7 from typing import Any
8 
9 import voluptuous as vol
10 
12  ATTR_MEDIA_CONTENT_ID,
13  ATTR_MEDIA_CONTENT_TYPE,
14  ATTR_MEDIA_SEEK_POSITION,
15  ATTR_MEDIA_SHUFFLE,
16  ATTR_MEDIA_VOLUME_LEVEL,
17  ATTR_MEDIA_VOLUME_MUTED,
18  DOMAIN as MEDIA_PLAYER_DOMAIN,
19  PLATFORM_SCHEMA as MEDIA_PLAYER_PLATFORM_SCHEMA,
20  SERVICE_CLEAR_PLAYLIST,
21  SERVICE_PLAY_MEDIA,
22  MediaPlayerEntity,
23  MediaPlayerEntityFeature,
24  MediaPlayerState,
25  MediaType,
26 )
27 from homeassistant.config_entries import ConfigEntry
28 from homeassistant.const import (
29  ATTR_ENTITY_ID,
30  ATTR_SUPPORTED_FEATURES,
31  CONF_ENTITIES,
32  CONF_NAME,
33  CONF_UNIQUE_ID,
34  SERVICE_MEDIA_NEXT_TRACK,
35  SERVICE_MEDIA_PAUSE,
36  SERVICE_MEDIA_PLAY,
37  SERVICE_MEDIA_PREVIOUS_TRACK,
38  SERVICE_MEDIA_SEEK,
39  SERVICE_MEDIA_STOP,
40  SERVICE_SHUFFLE_SET,
41  SERVICE_TURN_OFF,
42  SERVICE_TURN_ON,
43  SERVICE_VOLUME_MUTE,
44  SERVICE_VOLUME_SET,
45  STATE_UNAVAILABLE,
46  STATE_UNKNOWN,
47 )
48 from homeassistant.core import (
49  CALLBACK_TYPE,
50  Event,
51  EventStateChangedData,
52  HomeAssistant,
53  State,
54  callback,
55 )
56 from homeassistant.helpers import config_validation as cv, entity_registry as er
57 from homeassistant.helpers.entity_platform import AddEntitiesCallback
58 from homeassistant.helpers.event import async_track_state_change_event
59 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
60 
61 KEY_ANNOUNCE = "announce"
62 KEY_CLEAR_PLAYLIST = "clear_playlist"
63 KEY_ENQUEUE = "enqueue"
64 KEY_ON_OFF = "on_off"
65 KEY_PAUSE_PLAY_STOP = "play"
66 KEY_PLAY_MEDIA = "play_media"
67 KEY_SHUFFLE = "shuffle"
68 KEY_SEEK = "seek"
69 KEY_TRACKS = "tracks"
70 KEY_VOLUME = "volume"
71 
72 DEFAULT_NAME = "Media Group"
73 
74 PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend(
75  {
76  vol.Required(CONF_ENTITIES): cv.entities_domain(MEDIA_PLAYER_DOMAIN),
77  vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
78  vol.Optional(CONF_UNIQUE_ID): cv.string,
79  }
80 )
81 
82 
84  hass: HomeAssistant,
85  config: ConfigType,
86  async_add_entities: AddEntitiesCallback,
87  discovery_info: DiscoveryInfoType | None = None,
88 ) -> None:
89  """Set up the MediaPlayer Group platform."""
91  [
93  config.get(CONF_UNIQUE_ID), config[CONF_NAME], config[CONF_ENTITIES]
94  )
95  ]
96  )
97 
98 
100  hass: HomeAssistant,
101  config_entry: ConfigEntry,
102  async_add_entities: AddEntitiesCallback,
103 ) -> None:
104  """Initialize MediaPlayer Group config entry."""
105  registry = er.async_get(hass)
106  entities = er.async_validate_entity_ids(
107  registry, config_entry.options[CONF_ENTITIES]
108  )
109 
111  [MediaPlayerGroup(config_entry.entry_id, config_entry.title, entities)]
112  )
113 
114 
115 @callback
117  hass: HomeAssistant, name: str, validated_config: dict[str, Any]
118 ) -> MediaPlayerGroup:
119  """Create a preview sensor."""
120  return MediaPlayerGroup(
121  None,
122  name,
123  validated_config[CONF_ENTITIES],
124  )
125 
126 
128  """Representation of a Media Group."""
129 
130  _unrecorded_attributes = frozenset({ATTR_ENTITY_ID})
131 
132  _attr_available: bool = False
133  _attr_should_poll = False
134 
135  def __init__(self, unique_id: str | None, name: str, entities: list[str]) -> None:
136  """Initialize a Media Group entity."""
137  self._name_name = name
138  self._attr_unique_id_attr_unique_id = unique_id
139 
140  self._entities_entities = entities
141  self._features: dict[str, set[str]] = {
142  KEY_ANNOUNCE: set(),
143  KEY_CLEAR_PLAYLIST: set(),
144  KEY_ENQUEUE: set(),
145  KEY_ON_OFF: set(),
146  KEY_PAUSE_PLAY_STOP: set(),
147  KEY_PLAY_MEDIA: set(),
148  KEY_SHUFFLE: set(),
149  KEY_SEEK: set(),
150  KEY_TRACKS: set(),
151  KEY_VOLUME: set(),
152  }
153 
154  @callback
155  def async_on_state_change(self, event: Event[EventStateChangedData]) -> None:
156  """Update supported features and state when a new state is received."""
157  self.async_set_contextasync_set_context(event.context)
158  self.async_update_supported_featuresasync_update_supported_features(
159  event.data["entity_id"], event.data["new_state"]
160  )
161  self.async_update_group_stateasync_update_group_state()
162  self.async_write_ha_stateasync_write_ha_state()
163 
164  @callback
166  self,
167  entity_id: str,
168  new_state: State | None,
169  ) -> None:
170  """Update dictionaries with supported features."""
171  if not new_state:
172  for players in self._features.values():
173  players.discard(entity_id)
174  return
175 
176  new_features = new_state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
177  if new_features & MediaPlayerEntityFeature.CLEAR_PLAYLIST:
178  self._features[KEY_CLEAR_PLAYLIST].add(entity_id)
179  else:
180  self._features[KEY_CLEAR_PLAYLIST].discard(entity_id)
181  if new_features & (
182  MediaPlayerEntityFeature.NEXT_TRACK
183  | MediaPlayerEntityFeature.PREVIOUS_TRACK
184  ):
185  self._features[KEY_TRACKS].add(entity_id)
186  else:
187  self._features[KEY_TRACKS].discard(entity_id)
188  if new_features & (
189  MediaPlayerEntityFeature.PAUSE
190  | MediaPlayerEntityFeature.PLAY
191  | MediaPlayerEntityFeature.STOP
192  ):
193  self._features[KEY_PAUSE_PLAY_STOP].add(entity_id)
194  else:
195  self._features[KEY_PAUSE_PLAY_STOP].discard(entity_id)
196  if new_features & MediaPlayerEntityFeature.PLAY_MEDIA:
197  self._features[KEY_PLAY_MEDIA].add(entity_id)
198  else:
199  self._features[KEY_PLAY_MEDIA].discard(entity_id)
200  if new_features & MediaPlayerEntityFeature.SEEK:
201  self._features[KEY_SEEK].add(entity_id)
202  else:
203  self._features[KEY_SEEK].discard(entity_id)
204  if new_features & MediaPlayerEntityFeature.SHUFFLE_SET:
205  self._features[KEY_SHUFFLE].add(entity_id)
206  else:
207  self._features[KEY_SHUFFLE].discard(entity_id)
208  if new_features & (
209  MediaPlayerEntityFeature.TURN_ON | MediaPlayerEntityFeature.TURN_OFF
210  ):
211  self._features[KEY_ON_OFF].add(entity_id)
212  else:
213  self._features[KEY_ON_OFF].discard(entity_id)
214  if new_features & (
215  MediaPlayerEntityFeature.VOLUME_MUTE
216  | MediaPlayerEntityFeature.VOLUME_SET
217  | MediaPlayerEntityFeature.VOLUME_STEP
218  ):
219  self._features[KEY_VOLUME].add(entity_id)
220  else:
221  self._features[KEY_VOLUME].discard(entity_id)
222  if new_features & MediaPlayerEntityFeature.MEDIA_ANNOUNCE:
223  self._features[KEY_ANNOUNCE].add(entity_id)
224  else:
225  self._features[KEY_ANNOUNCE].discard(entity_id)
226  if new_features & MediaPlayerEntityFeature.MEDIA_ENQUEUE:
227  self._features[KEY_ENQUEUE].add(entity_id)
228  else:
229  self._features[KEY_ENQUEUE].discard(entity_id)
230 
231  @callback
233  self,
234  preview_callback: Callable[[str, Mapping[str, Any]], None],
235  ) -> CALLBACK_TYPE:
236  """Render a preview."""
237 
238  @callback
239  def async_state_changed_listener(
240  event: Event[EventStateChangedData] | None,
241  ) -> None:
242  """Handle child updates."""
243  self.async_update_group_stateasync_update_group_state()
244  calculated_state = self._async_calculate_state_async_calculate_state()
245  preview_callback(calculated_state.state, calculated_state.attributes)
246 
247  async_state_changed_listener(None)
249  self.hasshass, self._entities_entities, async_state_changed_listener
250  )
251 
252  async def async_added_to_hass(self) -> None:
253  """Register listeners."""
254  for entity_id in self._entities_entities:
255  new_state = self.hasshass.states.get(entity_id)
256  self.async_update_supported_featuresasync_update_supported_features(entity_id, new_state)
258  self.hasshass, self._entities_entities, self.async_on_state_changeasync_on_state_change
259  )
260  self.async_update_group_stateasync_update_group_state()
261  self.async_write_ha_stateasync_write_ha_state()
262 
263  @property
264  def name(self) -> str:
265  """Return the name of the entity."""
266  return self._name_name
267 
268  @property
269  def extra_state_attributes(self) -> Mapping[str, Any]:
270  """Return the state attributes for the media group."""
271  return {ATTR_ENTITY_ID: self._entities_entities}
272 
273  async def async_clear_playlist(self) -> None:
274  """Clear players playlist."""
275  data = {ATTR_ENTITY_ID: self._features[KEY_CLEAR_PLAYLIST]}
276  await self.hasshass.services.async_call(
277  MEDIA_PLAYER_DOMAIN,
278  SERVICE_CLEAR_PLAYLIST,
279  data,
280  context=self._context_context,
281  )
282 
283  async def async_media_next_track(self) -> None:
284  """Send next track command."""
285  data = {ATTR_ENTITY_ID: self._features[KEY_TRACKS]}
286  await self.hasshass.services.async_call(
287  MEDIA_PLAYER_DOMAIN,
288  SERVICE_MEDIA_NEXT_TRACK,
289  data,
290  context=self._context_context,
291  )
292 
293  async def async_media_pause(self) -> None:
294  """Send pause command."""
295  data = {ATTR_ENTITY_ID: self._features[KEY_PAUSE_PLAY_STOP]}
296  await self.hasshass.services.async_call(
297  MEDIA_PLAYER_DOMAIN,
298  SERVICE_MEDIA_PAUSE,
299  data,
300  context=self._context_context,
301  )
302 
303  async def async_media_play(self) -> None:
304  """Send play command."""
305  data = {ATTR_ENTITY_ID: self._features[KEY_PAUSE_PLAY_STOP]}
306  await self.hasshass.services.async_call(
307  MEDIA_PLAYER_DOMAIN,
308  SERVICE_MEDIA_PLAY,
309  data,
310  context=self._context_context,
311  )
312 
313  async def async_media_previous_track(self) -> None:
314  """Send previous track command."""
315  data = {ATTR_ENTITY_ID: self._features[KEY_TRACKS]}
316  await self.hasshass.services.async_call(
317  MEDIA_PLAYER_DOMAIN,
318  SERVICE_MEDIA_PREVIOUS_TRACK,
319  data,
320  context=self._context_context,
321  )
322 
323  async def async_media_seek(self, position: float) -> None:
324  """Send seek command."""
325  data = {
326  ATTR_ENTITY_ID: self._features[KEY_SEEK],
327  ATTR_MEDIA_SEEK_POSITION: position,
328  }
329  await self.hasshass.services.async_call(
330  MEDIA_PLAYER_DOMAIN,
331  SERVICE_MEDIA_SEEK,
332  data,
333  context=self._context_context,
334  )
335 
336  async def async_media_stop(self) -> None:
337  """Send stop command."""
338  data = {ATTR_ENTITY_ID: self._features[KEY_PAUSE_PLAY_STOP]}
339  await self.hasshass.services.async_call(
340  MEDIA_PLAYER_DOMAIN,
341  SERVICE_MEDIA_STOP,
342  data,
343  context=self._context_context,
344  )
345 
346  async def async_mute_volume(self, mute: bool) -> None:
347  """Mute the volume."""
348  data = {
349  ATTR_ENTITY_ID: self._features[KEY_VOLUME],
350  ATTR_MEDIA_VOLUME_MUTED: mute,
351  }
352  await self.hasshass.services.async_call(
353  MEDIA_PLAYER_DOMAIN,
354  SERVICE_VOLUME_MUTE,
355  data,
356  context=self._context_context,
357  )
358 
359  async def async_play_media(
360  self, media_type: MediaType | str, media_id: str, **kwargs: Any
361  ) -> None:
362  """Play a piece of media."""
363  data = {
364  ATTR_ENTITY_ID: self._features[KEY_PLAY_MEDIA],
365  ATTR_MEDIA_CONTENT_ID: media_id,
366  ATTR_MEDIA_CONTENT_TYPE: media_type,
367  }
368  if kwargs:
369  data.update(kwargs)
370  await self.hasshass.services.async_call(
371  MEDIA_PLAYER_DOMAIN,
372  SERVICE_PLAY_MEDIA,
373  data,
374  context=self._context_context,
375  )
376 
377  async def async_set_shuffle(self, shuffle: bool) -> None:
378  """Enable/disable shuffle mode."""
379  data = {
380  ATTR_ENTITY_ID: self._features[KEY_SHUFFLE],
381  ATTR_MEDIA_SHUFFLE: shuffle,
382  }
383  await self.hasshass.services.async_call(
384  MEDIA_PLAYER_DOMAIN,
385  SERVICE_SHUFFLE_SET,
386  data,
387  context=self._context_context,
388  )
389 
390  async def async_turn_on(self) -> None:
391  """Forward the turn_on command to all media in the media group."""
392  data = {ATTR_ENTITY_ID: self._features[KEY_ON_OFF]}
393  await self.hasshass.services.async_call(
394  MEDIA_PLAYER_DOMAIN,
395  SERVICE_TURN_ON,
396  data,
397  context=self._context_context,
398  )
399 
400  async def async_set_volume_level(self, volume: float) -> None:
401  """Set volume level(s)."""
402  data = {
403  ATTR_ENTITY_ID: self._features[KEY_VOLUME],
404  ATTR_MEDIA_VOLUME_LEVEL: volume,
405  }
406  await self.hasshass.services.async_call(
407  MEDIA_PLAYER_DOMAIN,
408  SERVICE_VOLUME_SET,
409  data,
410  context=self._context_context,
411  )
412 
413  async def async_turn_off(self) -> None:
414  """Forward the turn_off command to all media in the media group."""
415  data = {ATTR_ENTITY_ID: self._features[KEY_ON_OFF]}
416  await self.hasshass.services.async_call(
417  MEDIA_PLAYER_DOMAIN,
418  SERVICE_TURN_OFF,
419  data,
420  context=self._context_context,
421  )
422 
423  async def async_volume_up(self) -> None:
424  """Turn volume up for media player(s)."""
425  for entity in self._features[KEY_VOLUME]:
426  volume_level = self.hasshass.states.get(entity).attributes["volume_level"] # type: ignore[union-attr]
427  if volume_level < 1:
428  await self.async_set_volume_levelasync_set_volume_levelasync_set_volume_level(min(1, volume_level + 0.1))
429 
430  async def async_volume_down(self) -> None:
431  """Turn volume down for media player(s)."""
432  for entity in self._features[KEY_VOLUME]:
433  volume_level = self.hasshass.states.get(entity).attributes["volume_level"] # type: ignore[union-attr]
434  if volume_level > 0:
435  await self.async_set_volume_levelasync_set_volume_levelasync_set_volume_level(max(0, volume_level - 0.1))
436 
437  @callback
438  def async_update_group_state(self) -> None:
439  """Query all members and determine the media group state."""
440  states = [
441  state.state
442  for entity_id in self._entities_entities
443  if (state := self.hasshass.states.get(entity_id)) is not None
444  ]
445 
446  # Set group as unavailable if all members are unavailable or missing
447  self._attr_available_attr_available = any(state != STATE_UNAVAILABLE for state in states)
448 
449  valid_state = any(
450  state not in (STATE_UNKNOWN, STATE_UNAVAILABLE) for state in states
451  )
452  if not valid_state:
453  # Set as unknown if all members are unknown or unavailable
454  self._attr_state_attr_state = None
455  else:
456  off_values = {MediaPlayerState.OFF, STATE_UNAVAILABLE, STATE_UNKNOWN}
457  if states.count(single_state := states[0]) == len(states):
458  self._attr_state_attr_state = None
459  with suppress(ValueError):
460  self._attr_state_attr_state = MediaPlayerState(single_state)
461  elif any(state for state in states if state not in off_values):
462  self._attr_state_attr_state = MediaPlayerState.ON
463  else:
464  self._attr_state_attr_state = MediaPlayerState.OFF
465 
466  supported_features = MediaPlayerEntityFeature(0)
467  if self._features[KEY_CLEAR_PLAYLIST]:
468  supported_features |= MediaPlayerEntityFeature.CLEAR_PLAYLIST
469  if self._features[KEY_TRACKS]:
470  supported_features |= (
471  MediaPlayerEntityFeature.NEXT_TRACK
472  | MediaPlayerEntityFeature.PREVIOUS_TRACK
473  )
474  if self._features[KEY_PAUSE_PLAY_STOP]:
475  supported_features |= (
476  MediaPlayerEntityFeature.PAUSE
477  | MediaPlayerEntityFeature.PLAY
478  | MediaPlayerEntityFeature.STOP
479  )
480  if self._features[KEY_PLAY_MEDIA]:
481  supported_features |= MediaPlayerEntityFeature.PLAY_MEDIA
482  if self._features[KEY_SEEK]:
483  supported_features |= MediaPlayerEntityFeature.SEEK
484  if self._features[KEY_SHUFFLE]:
485  supported_features |= MediaPlayerEntityFeature.SHUFFLE_SET
486  if self._features[KEY_ON_OFF]:
487  supported_features |= (
488  MediaPlayerEntityFeature.TURN_ON | MediaPlayerEntityFeature.TURN_OFF
489  )
490  if self._features[KEY_VOLUME]:
491  supported_features |= (
492  MediaPlayerEntityFeature.VOLUME_MUTE
493  | MediaPlayerEntityFeature.VOLUME_SET
494  | MediaPlayerEntityFeature.VOLUME_STEP
495  )
496  if self._features[KEY_ANNOUNCE]:
497  supported_features |= MediaPlayerEntityFeature.MEDIA_ANNOUNCE
498  if self._features[KEY_ENQUEUE]:
499  supported_features |= MediaPlayerEntityFeature.MEDIA_ENQUEUE
500 
501  self._attr_supported_features_attr_supported_features = supported_features
None async_update_supported_features(self, str entity_id, State|None new_state)
None async_on_state_change(self, Event[EventStateChangedData] event)
None async_play_media(self, MediaType|str media_type, str media_id, **Any kwargs)
None __init__(self, str|None unique_id, str name, list[str] entities)
CALLBACK_TYPE async_start_preview(self, Callable[[str, Mapping[str, Any]], None] preview_callback)
CalculatedState _async_calculate_state(self)
Definition: entity.py:1059
None async_set_context(self, Context context)
Definition: entity.py:937
bool add(self, _T matcher)
Definition: match.py:185
MediaPlayerGroup async_create_preview_media_player(HomeAssistant hass, str name, dict[str, Any] validated_config)
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
Definition: media_player.py:88
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
CALLBACK_TYPE async_track_state_change_event(HomeAssistant hass, str|Iterable[str] entity_ids, Callable[[Event[EventStateChangedData]], Any] action, HassJobType|None job_type=None)
Definition: event.py:314