1 """Combination of multiple media players for a universal controller."""
3 from __future__
import annotations
8 import voluptuous
as vol
14 ATTR_INPUT_SOURCE_LIST,
15 ATTR_MEDIA_ALBUM_ARTIST,
16 ATTR_MEDIA_ALBUM_NAME,
19 ATTR_MEDIA_CONTENT_ID,
20 ATTR_MEDIA_CONTENT_TYPE,
25 ATTR_MEDIA_POSITION_UPDATED_AT,
28 ATTR_MEDIA_SEEK_POSITION,
29 ATTR_MEDIA_SERIES_TITLE,
33 ATTR_MEDIA_VOLUME_LEVEL,
34 ATTR_MEDIA_VOLUME_MUTED,
37 DEVICE_CLASSES_SCHEMA,
38 DOMAIN
as MEDIA_PLAYER_DOMAIN,
39 PLATFORM_SCHEMA
as MEDIA_PLAYER_PLATFORM_SCHEMA,
40 SERVICE_CLEAR_PLAYLIST,
42 SERVICE_SELECT_SOUND_MODE,
43 SERVICE_SELECT_SOURCE,
46 MediaPlayerEntityFeature,
55 ATTR_SUPPORTED_FEATURES,
61 EVENT_HOMEASSISTANT_START,
62 SERVICE_MEDIA_NEXT_TRACK,
65 SERVICE_MEDIA_PLAY_PAUSE,
66 SERVICE_MEDIA_PREVIOUS_TRACK,
90 async_track_state_change_event,
91 async_track_template_result,
97 ATTR_ACTIVE_CHILD =
"active_child"
99 CONF_ACTIVE_CHILD_TEMPLATE =
"active_child_template"
100 CONF_ATTRS =
"attributes"
101 CONF_CHILDREN =
"children"
102 CONF_COMMANDS =
"commands"
103 CONF_BROWSE_MEDIA_ENTITY =
"browse_media_entity"
108 MediaPlayerState.OFF,
109 MediaPlayerState.IDLE,
110 MediaPlayerState.STANDBY,
112 MediaPlayerState.PAUSED,
113 MediaPlayerState.BUFFERING,
114 MediaPlayerState.PLAYING,
116 STATES_ORDER_LOOKUP = {state: idx
for idx, state
in enumerate(STATES_ORDER)}
117 STATES_ORDER_IDLE = STATES_ORDER_LOOKUP[MediaPlayerState.IDLE]
119 ATTRS_SCHEMA = cv.schema_with_slug_keys(cv.string)
120 CMD_SCHEMA = cv.schema_with_slug_keys(cv.SERVICE_SCHEMA)
122 PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend(
124 vol.Required(CONF_NAME): cv.string,
125 vol.Optional(CONF_CHILDREN, default=[]): cv.entity_ids,
126 vol.Optional(CONF_COMMANDS, default={}): CMD_SCHEMA,
127 vol.Optional(CONF_ATTRS, default={}): vol.Or(
128 cv.ensure_list(ATTRS_SCHEMA), ATTRS_SCHEMA
130 vol.Optional(CONF_BROWSE_MEDIA_ENTITY): cv.string,
131 vol.Optional(CONF_UNIQUE_ID): cv.string,
132 vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
133 vol.Optional(CONF_ACTIVE_CHILD_TEMPLATE): cv.template,
134 vol.Optional(CONF_STATE_TEMPLATE): cv.template,
136 extra=vol.REMOVE_EXTRA,
143 async_add_entities: AddEntitiesCallback,
144 discovery_info: DiscoveryInfoType |
None =
None,
146 """Set up the universal media players."""
154 """Representation of an universal media player."""
156 _attr_should_poll =
False
163 """Initialize the Universal media device."""
169 self.
_cmds_cmds = config.get(CONF_COMMANDS)
171 for key, val
in config.get(CONF_ATTRS).items():
172 attr =
list(map(str.strip, val.split(
"|", 1)))
175 self.
_attrs_attrs[key] = attr
184 """Subscribe to children and template state changes."""
187 def _async_on_dependency_update(
188 event: Event[EventStateChangedData],
190 """Update ha state when dependencies update."""
196 def _async_on_template_update(
197 event: Event[EventStateChangedData] |
None,
198 updates: list[TrackTemplateResult],
200 """Update state when template state changes."""
202 template = data.template
207 None if isinstance(result, TemplateError)
else result
211 None if isinstance(result, TemplateError)
else result
220 track_templates: list[TrackTemplate] = []
230 _async_on_template_update,
232 self.
hasshasshass.bus.async_listen_once(
233 EVENT_HOMEASSISTANT_START, callback(
lambda _: result.async_refresh())
239 for entity
in self.
_attrs_attrs.values():
240 depend.append(entity[0])
244 self.
hasshasshass,
list(set(depend)), _async_on_dependency_update
249 """Look up an entity state."""
250 if (state_obj := self.
hasshasshass.states.get(entity_id))
is None:
254 return state_obj.attributes.get(state_attr)
255 return state_obj.state
258 """Return either the override or the active child for attr_name."""
259 if attr_name
in self.
_attrs_attrs:
261 self.
_attrs_attrs[attr_name][0], self.
_attrs_attrs[attr_name][1]
267 """Return the active child's attributes."""
269 return active_child.attributes.get(attr_name)
if active_child
else None
272 self, service_name, service_data=None, allow_override=False
274 """Call either a specified or active child's service."""
275 if service_data
is None:
278 if allow_override
and service_name
in self.
_cmds_cmds:
281 self.
_cmds_cmds[service_name],
282 variables=service_data,
284 validate_config=
False,
288 if (active_child := self.
_child_state_child_state)
is None:
292 service_data[ATTR_ENTITY_ID] = active_child.entity_id
294 await self.
hasshasshass.services.async_call(
304 """Return the master state for entity or None."""
307 if CONF_STATE
in self.
_attrs_attrs:
309 self.
_attrs_attrs[CONF_STATE][0], self.
_attrs_attrs[CONF_STATE][1]
311 return master_state
if master_state
else MediaPlayerState.OFF
317 """Return True if unable to access real state of the entity."""
318 return self.
_child_attr_child_attr(ATTR_ASSUMED_STATE)
322 """Return the current state of media player.
324 Off if master state is off
325 else Status of first active child
326 else master state or off
329 if (master_state == MediaPlayerState.OFF)
or (self.
_state_template_state_template
is not None):
333 return active_child.state
335 return master_state
if master_state
else MediaPlayerState.OFF
339 """Volume level of entity specified in attributes or active child."""
342 except (TypeError, ValueError):
347 """Boolean if volume is muted."""
352 """Return the content ID of current playing media."""
353 return self.
_child_attr_child_attr(ATTR_MEDIA_CONTENT_ID)
357 """Return the content type of current playing media."""
358 return self.
_child_attr_child_attr(ATTR_MEDIA_CONTENT_TYPE)
362 """Return the duration of current playing media in seconds."""
363 return self.
_child_attr_child_attr(ATTR_MEDIA_DURATION)
367 """Image url of current playing media."""
368 return self.
_child_attr_child_attr(ATTR_ENTITY_PICTURE)
372 """Return image of the media playing.
374 The universal media player doesn't use the parent class logic, since
375 the url is coming from child entity pictures which have already been
376 sent through the API proxy.
382 """Title of current playing media."""
383 return self.
_child_attr_child_attr(ATTR_MEDIA_TITLE)
387 """Artist of current playing media (Music track only)."""
388 return self.
_child_attr_child_attr(ATTR_MEDIA_ARTIST)
392 """Album name of current playing media (Music track only)."""
393 return self.
_child_attr_child_attr(ATTR_MEDIA_ALBUM_NAME)
397 """Album artist of current playing media (Music track only)."""
398 return self.
_child_attr_child_attr(ATTR_MEDIA_ALBUM_ARTIST)
402 """Track number of current playing media (Music track only)."""
403 return self.
_child_attr_child_attr(ATTR_MEDIA_TRACK)
407 """Return the title of the series of current playing media (TV)."""
408 return self.
_child_attr_child_attr(ATTR_MEDIA_SERIES_TITLE)
412 """Season of current playing media (TV Show only)."""
413 return self.
_child_attr_child_attr(ATTR_MEDIA_SEASON)
417 """Episode of current playing media (TV Show only)."""
418 return self.
_child_attr_child_attr(ATTR_MEDIA_EPISODE)
422 """Channel currently playing."""
423 return self.
_child_attr_child_attr(ATTR_MEDIA_CHANNEL)
427 """Title of Playlist currently playing."""
428 return self.
_child_attr_child_attr(ATTR_MEDIA_PLAYLIST)
432 """ID of the current running app."""
437 """Name of the current running app."""
442 """Return the current sound mode of the device."""
447 """List of available sound modes."""
452 """Return the current input source of the device."""
457 """List of available input sources."""
462 """Boolean if repeating is enabled."""
467 """Boolean if shuffling is enabled."""
472 """Flag media player features that are supported."""
473 flags: MediaPlayerEntityFeature = self.
_child_attr_child_attr(
474 ATTR_SUPPORTED_FEATURES
477 if SERVICE_TURN_ON
in self.
_cmds_cmds:
478 flags |= MediaPlayerEntityFeature.TURN_ON
479 if SERVICE_TURN_OFF
in self.
_cmds_cmds:
480 flags |= MediaPlayerEntityFeature.TURN_OFF
482 if SERVICE_MEDIA_PLAY_PAUSE
in self.
_cmds_cmds:
483 flags |= MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.PAUSE
485 if SERVICE_MEDIA_PLAY
in self.
_cmds_cmds:
486 flags |= MediaPlayerEntityFeature.PLAY
487 if SERVICE_MEDIA_PAUSE
in self.
_cmds_cmds:
488 flags |= MediaPlayerEntityFeature.PAUSE
490 if SERVICE_MEDIA_STOP
in self.
_cmds_cmds:
491 flags |= MediaPlayerEntityFeature.STOP
493 if SERVICE_MEDIA_NEXT_TRACK
in self.
_cmds_cmds:
494 flags |= MediaPlayerEntityFeature.NEXT_TRACK
495 if SERVICE_MEDIA_PREVIOUS_TRACK
in self.
_cmds_cmds:
496 flags |= MediaPlayerEntityFeature.PREVIOUS_TRACK
498 if any(cmd
in self.
_cmds_cmds
for cmd
in (SERVICE_VOLUME_UP, SERVICE_VOLUME_DOWN)):
499 flags |= MediaPlayerEntityFeature.VOLUME_STEP
500 if SERVICE_VOLUME_SET
in self.
_cmds_cmds:
501 flags |= MediaPlayerEntityFeature.VOLUME_SET
503 if SERVICE_VOLUME_MUTE
in self.
_cmds_cmds
and ATTR_MEDIA_VOLUME_MUTED
in self.
_attrs_attrs:
504 flags |= MediaPlayerEntityFeature.VOLUME_MUTE
507 SERVICE_SELECT_SOURCE
in self.
_cmds_cmds
508 and ATTR_INPUT_SOURCE_LIST
in self.
_attrs_attrs
510 flags |= MediaPlayerEntityFeature.SELECT_SOURCE
512 if SERVICE_PLAY_MEDIA
in self.
_cmds_cmds:
513 flags |= MediaPlayerEntityFeature.PLAY_MEDIA
516 flags |= MediaPlayerEntityFeature.BROWSE_MEDIA
518 if SERVICE_CLEAR_PLAYLIST
in self.
_cmds_cmds:
519 flags |= MediaPlayerEntityFeature.CLEAR_PLAYLIST
521 if SERVICE_SHUFFLE_SET
in self.
_cmds_cmds
and ATTR_MEDIA_SHUFFLE
in self.
_attrs_attrs:
522 flags |= MediaPlayerEntityFeature.SHUFFLE_SET
524 if SERVICE_REPEAT_SET
in self.
_cmds_cmds
and ATTR_MEDIA_REPEAT
in self.
_attrs_attrs:
525 flags |= MediaPlayerEntityFeature.REPEAT_SET
528 SERVICE_SELECT_SOUND_MODE
in self.
_cmds_cmds
529 and ATTR_SOUND_MODE_LIST
in self.
_attrs_attrs
531 flags |= MediaPlayerEntityFeature.SELECT_SOUND_MODE
537 """Return device specific state attributes."""
539 return {ATTR_ACTIVE_CHILD: active_child.entity_id}
if active_child
else {}
543 """Position of current playing media in seconds."""
544 return self.
_child_attr_child_attr(ATTR_MEDIA_POSITION)
548 """When was the position of the current playing media valid."""
549 return self.
_child_attr_child_attr(ATTR_MEDIA_POSITION_UPDATED_AT)
552 """Turn the media player on."""
556 """Turn the media player off."""
560 """Mute the volume."""
561 data = {ATTR_MEDIA_VOLUME_MUTED: mute}
562 await self.
_async_call_service_async_call_service(SERVICE_VOLUME_MUTE, data, allow_override=
True)
565 """Set volume level, range 0..1."""
566 data = {ATTR_MEDIA_VOLUME_LEVEL: volume}
567 await self.
_async_call_service_async_call_service(SERVICE_VOLUME_SET, data, allow_override=
True)
570 """Send play command."""
574 """Send pause command."""
575 await self.
_async_call_service_async_call_service(SERVICE_MEDIA_PAUSE, allow_override=
True)
578 """Send stop command."""
582 """Send previous track command."""
584 SERVICE_MEDIA_PREVIOUS_TRACK, allow_override=
True
588 """Send next track command."""
589 await self.
_async_call_service_async_call_service(SERVICE_MEDIA_NEXT_TRACK, allow_override=
True)
592 """Send seek command."""
593 data = {ATTR_MEDIA_SEEK_POSITION: position}
597 self, media_type: MediaType | str, media_id: str, **kwargs: Any
599 """Play a piece of media."""
600 data = {ATTR_MEDIA_CONTENT_TYPE: media_type, ATTR_MEDIA_CONTENT_ID: media_id}
601 await self.
_async_call_service_async_call_service(SERVICE_PLAY_MEDIA, data, allow_override=
True)
604 """Turn volume up for media player."""
608 """Turn volume down for media player."""
609 await self.
_async_call_service_async_call_service(SERVICE_VOLUME_DOWN, allow_override=
True)
612 """Play or pause the media player."""
613 await self.
_async_call_service_async_call_service(SERVICE_MEDIA_PLAY_PAUSE, allow_override=
True)
616 """Select sound mode."""
617 data = {ATTR_SOUND_MODE: sound_mode}
619 SERVICE_SELECT_SOUND_MODE, data, allow_override=
True
623 """Set the input source."""
624 data = {ATTR_INPUT_SOURCE: source}
625 await self.
_async_call_service_async_call_service(SERVICE_SELECT_SOURCE, data, allow_override=
True)
628 """Clear players playlist."""
629 await self.
_async_call_service_async_call_service(SERVICE_CLEAR_PLAYLIST, allow_override=
True)
632 """Enable/disable shuffling."""
633 data = {ATTR_MEDIA_SHUFFLE: shuffle}
634 await self.
_async_call_service_async_call_service(SERVICE_SHUFFLE_SET, data, allow_override=
True)
637 """Set repeat mode."""
638 data = {ATTR_MEDIA_REPEAT: repeat}
639 await self.
_async_call_service_async_call_service(SERVICE_REPEAT_SET, data, allow_override=
True)
642 """Toggle the power on the media player."""
643 if SERVICE_TOGGLE
in self.
_cmds_cmds:
651 media_content_type: MediaType | str |
None =
None,
652 media_content_id: str |
None =
None,
654 """Return a BrowseMedia instance."""
658 component: EntityComponent[MediaPlayerEntity] = self.
hasshasshass.data[
661 if entity_id
and (entity := component.get_entity(entity_id)):
662 return await entity.async_browse_media(media_content_type, media_content_id)
663 raise NotImplementedError
667 """Update state in HA."""
672 for child_name
in self.
_children_children:
673 if (child_state := self.
hasshasshass.states.get(child_name))
and (
674 child_state_order := STATES_ORDER_LOOKUP.get(child_state.state, 0)
675 ) >= STATES_ORDER_IDLE:
677 if child_state_order > STATES_ORDER_LOOKUP.get(
685 """Manual update from API."""
None async_write_ha_state(self)
None async_on_remove(self, CALLBACK_TYPE func)
None async_set_context(self, Context context)
CALLBACK_TYPE async_track_state_change_event(HomeAssistant hass, str|Iterable[str] entity_ids, Callable[[Event[EventStateChangedData]], Any] action, HassJobType|None job_type=None)
TrackTemplateResultInfo async_track_template_result(HomeAssistant hass, Sequence[TrackTemplate] track_templates, TrackTemplateResultListener action, bool strict=False, Callable[[int, str], None]|None log_fn=None, bool has_super_template=False)
None async_setup_reload_service(HomeAssistant hass, str domain, Iterable[str] platforms)
None async_call_from_config(HomeAssistant hass, ConfigType config, bool blocking=False, TemplateVarsType variables=None, bool validate_config=True, Context|None context=None)