1 """Platform allowing several media players to be grouped into one media player."""
3 from __future__
import annotations
5 from collections.abc
import Callable, Mapping
6 from contextlib
import suppress
9 import voluptuous
as vol
12 ATTR_MEDIA_CONTENT_ID,
13 ATTR_MEDIA_CONTENT_TYPE,
14 ATTR_MEDIA_SEEK_POSITION,
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,
23 MediaPlayerEntityFeature,
30 ATTR_SUPPORTED_FEATURES,
34 SERVICE_MEDIA_NEXT_TRACK,
37 SERVICE_MEDIA_PREVIOUS_TRACK,
51 EventStateChangedData,
61 KEY_ANNOUNCE =
"announce"
62 KEY_CLEAR_PLAYLIST =
"clear_playlist"
63 KEY_ENQUEUE =
"enqueue"
65 KEY_PAUSE_PLAY_STOP =
"play"
66 KEY_PLAY_MEDIA =
"play_media"
67 KEY_SHUFFLE =
"shuffle"
72 DEFAULT_NAME =
"Media Group"
74 PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend(
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,
86 async_add_entities: AddEntitiesCallback,
87 discovery_info: DiscoveryInfoType |
None =
None,
89 """Set up the MediaPlayer Group platform."""
93 config.get(CONF_UNIQUE_ID), config[CONF_NAME], config[CONF_ENTITIES]
101 config_entry: ConfigEntry,
102 async_add_entities: AddEntitiesCallback,
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]
117 hass: HomeAssistant, name: str, validated_config: dict[str, Any]
118 ) -> MediaPlayerGroup:
119 """Create a preview sensor."""
123 validated_config[CONF_ENTITIES],
128 """Representation of a Media Group."""
130 _unrecorded_attributes = frozenset({ATTR_ENTITY_ID})
132 _attr_available: bool =
False
133 _attr_should_poll =
False
135 def __init__(self, unique_id: str |
None, name: str, entities: list[str]) ->
None:
136 """Initialize a Media Group entity."""
141 self._features: dict[str, set[str]] = {
143 KEY_CLEAR_PLAYLIST: set(),
146 KEY_PAUSE_PLAY_STOP: set(),
147 KEY_PLAY_MEDIA: set(),
156 """Update supported features and state when a new state is received."""
159 event.data[
"entity_id"], event.data[
"new_state"]
168 new_state: State |
None,
170 """Update dictionaries with supported features."""
172 for players
in self._features.values():
173 players.discard(entity_id)
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)
180 self._features[KEY_CLEAR_PLAYLIST].discard(entity_id)
182 MediaPlayerEntityFeature.NEXT_TRACK
183 | MediaPlayerEntityFeature.PREVIOUS_TRACK
185 self._features[KEY_TRACKS].
add(entity_id)
187 self._features[KEY_TRACKS].discard(entity_id)
189 MediaPlayerEntityFeature.PAUSE
190 | MediaPlayerEntityFeature.PLAY
191 | MediaPlayerEntityFeature.STOP
193 self._features[KEY_PAUSE_PLAY_STOP].
add(entity_id)
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)
199 self._features[KEY_PLAY_MEDIA].discard(entity_id)
200 if new_features & MediaPlayerEntityFeature.SEEK:
201 self._features[KEY_SEEK].
add(entity_id)
203 self._features[KEY_SEEK].discard(entity_id)
204 if new_features & MediaPlayerEntityFeature.SHUFFLE_SET:
205 self._features[KEY_SHUFFLE].
add(entity_id)
207 self._features[KEY_SHUFFLE].discard(entity_id)
209 MediaPlayerEntityFeature.TURN_ON | MediaPlayerEntityFeature.TURN_OFF
211 self._features[KEY_ON_OFF].
add(entity_id)
213 self._features[KEY_ON_OFF].discard(entity_id)
215 MediaPlayerEntityFeature.VOLUME_MUTE
216 | MediaPlayerEntityFeature.VOLUME_SET
217 | MediaPlayerEntityFeature.VOLUME_STEP
219 self._features[KEY_VOLUME].
add(entity_id)
221 self._features[KEY_VOLUME].discard(entity_id)
222 if new_features & MediaPlayerEntityFeature.MEDIA_ANNOUNCE:
223 self._features[KEY_ANNOUNCE].
add(entity_id)
225 self._features[KEY_ANNOUNCE].discard(entity_id)
226 if new_features & MediaPlayerEntityFeature.MEDIA_ENQUEUE:
227 self._features[KEY_ENQUEUE].
add(entity_id)
229 self._features[KEY_ENQUEUE].discard(entity_id)
234 preview_callback: Callable[[str, Mapping[str, Any]],
None],
236 """Render a preview."""
239 def async_state_changed_listener(
240 event: Event[EventStateChangedData] |
None,
242 """Handle child updates."""
245 preview_callback(calculated_state.state, calculated_state.attributes)
247 async_state_changed_listener(
None)
249 self.
hasshass, self.
_entities_entities, async_state_changed_listener
253 """Register listeners."""
254 for entity_id
in self.
_entities_entities:
255 new_state = self.
hasshass.states.get(entity_id)
265 """Return the name of the entity."""
266 return self.
_name_name
270 """Return the state attributes for the media group."""
271 return {ATTR_ENTITY_ID: self.
_entities_entities}
274 """Clear players playlist."""
275 data = {ATTR_ENTITY_ID: self._features[KEY_CLEAR_PLAYLIST]}
276 await self.
hasshass.services.async_call(
278 SERVICE_CLEAR_PLAYLIST,
284 """Send next track command."""
285 data = {ATTR_ENTITY_ID: self._features[KEY_TRACKS]}
286 await self.
hasshass.services.async_call(
288 SERVICE_MEDIA_NEXT_TRACK,
294 """Send pause command."""
295 data = {ATTR_ENTITY_ID: self._features[KEY_PAUSE_PLAY_STOP]}
296 await self.
hasshass.services.async_call(
304 """Send play command."""
305 data = {ATTR_ENTITY_ID: self._features[KEY_PAUSE_PLAY_STOP]}
306 await self.
hasshass.services.async_call(
314 """Send previous track command."""
315 data = {ATTR_ENTITY_ID: self._features[KEY_TRACKS]}
316 await self.
hasshass.services.async_call(
318 SERVICE_MEDIA_PREVIOUS_TRACK,
324 """Send seek command."""
326 ATTR_ENTITY_ID: self._features[KEY_SEEK],
327 ATTR_MEDIA_SEEK_POSITION: position,
329 await self.
hasshass.services.async_call(
337 """Send stop command."""
338 data = {ATTR_ENTITY_ID: self._features[KEY_PAUSE_PLAY_STOP]}
339 await self.
hasshass.services.async_call(
347 """Mute the volume."""
349 ATTR_ENTITY_ID: self._features[KEY_VOLUME],
350 ATTR_MEDIA_VOLUME_MUTED: mute,
352 await self.
hasshass.services.async_call(
360 self, media_type: MediaType | str, media_id: str, **kwargs: Any
362 """Play a piece of media."""
364 ATTR_ENTITY_ID: self._features[KEY_PLAY_MEDIA],
365 ATTR_MEDIA_CONTENT_ID: media_id,
366 ATTR_MEDIA_CONTENT_TYPE: media_type,
370 await self.
hasshass.services.async_call(
378 """Enable/disable shuffle mode."""
380 ATTR_ENTITY_ID: self._features[KEY_SHUFFLE],
381 ATTR_MEDIA_SHUFFLE: shuffle,
383 await self.
hasshass.services.async_call(
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(
401 """Set volume level(s)."""
403 ATTR_ENTITY_ID: self._features[KEY_VOLUME],
404 ATTR_MEDIA_VOLUME_LEVEL: volume,
406 await self.
hasshass.services.async_call(
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(
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"]
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"]
439 """Query all members and determine the media group state."""
443 if (state := self.
hasshass.states.get(entity_id))
is not None
447 self.
_attr_available_attr_available = any(state != STATE_UNAVAILABLE
for state
in states)
450 state
not in (STATE_UNKNOWN, STATE_UNAVAILABLE)
for state
in states
456 off_values = {MediaPlayerState.OFF, STATE_UNAVAILABLE, STATE_UNKNOWN}
457 if states.count(single_state := states[0]) == len(states):
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):
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
474 if self._features[KEY_PAUSE_PLAY_STOP]:
475 supported_features |= (
476 MediaPlayerEntityFeature.PAUSE
477 | MediaPlayerEntityFeature.PLAY
478 | MediaPlayerEntityFeature.STOP
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
490 if self._features[KEY_VOLUME]:
491 supported_features |= (
492 MediaPlayerEntityFeature.VOLUME_MUTE
493 | MediaPlayerEntityFeature.VOLUME_SET
494 | MediaPlayerEntityFeature.VOLUME_STEP
496 if self._features[KEY_ANNOUNCE]:
497 supported_features |= MediaPlayerEntityFeature.MEDIA_ANNOUNCE
498 if self._features[KEY_ENQUEUE]:
499 supported_features |= MediaPlayerEntityFeature.MEDIA_ENQUEUE
None async_write_ha_state(self)
CalculatedState _async_calculate_state(self)
None async_set_context(self, Context context)
bool add(self, _T matcher)
CALLBACK_TYPE async_track_state_change_event(HomeAssistant hass, str|Iterable[str] entity_ids, Callable[[Event[EventStateChangedData]], Any] action, HassJobType|None job_type=None)