Add support for app launching in Apple TV (#61732)

pull/61738/head
Pierre Ståhl 2021-12-13 23:13:04 +01:00 committed by GitHub
parent 82e280d2ac
commit 905295707d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 90 additions and 2 deletions

View File

@ -0,0 +1,44 @@
"""Support for media browsing."""
from homeassistant.components.media_player import BrowseMedia
from homeassistant.components.media_player.const import (
MEDIA_CLASS_APP,
MEDIA_CLASS_DIRECTORY,
MEDIA_TYPE_APP,
MEDIA_TYPE_APPS,
)
def build_app_list(app_list):
"""Create response payload for app list."""
app_list = [
{"app_id": app_id, "title": app_name, "type": MEDIA_TYPE_APP}
for app_name, app_id in app_list.items()
]
return BrowseMedia(
media_class=MEDIA_CLASS_DIRECTORY,
media_content_id=None,
media_content_type=MEDIA_TYPE_APPS,
title="Apps",
can_play=True,
can_expand=False,
children=[item_payload(item) for item in app_list],
children_media_class=MEDIA_CLASS_APP,
)
def item_payload(item):
"""
Create response payload for a single media item.
Used by async_browse_media.
"""
return BrowseMedia(
title=item["title"],
media_class=MEDIA_CLASS_APP,
media_content_type=MEDIA_TYPE_APP,
media_content_id=item["app_id"],
can_play=False,
can_expand=False,
)

View File

@ -1,6 +1,7 @@
"""Support for Apple TV media player."""
import logging
from pyatv import exceptions
from pyatv.const import (
DeviceState,
FeatureName,
@ -12,14 +13,16 @@ from pyatv.const import (
)
from pyatv.helpers import is_streamable
from homeassistant.components.media_player import MediaPlayerEntity
from homeassistant.components.media_player import BrowseMedia, MediaPlayerEntity
from homeassistant.components.media_player.const import (
MEDIA_TYPE_APP,
MEDIA_TYPE_MUSIC,
MEDIA_TYPE_TVSHOW,
MEDIA_TYPE_VIDEO,
REPEAT_MODE_ALL,
REPEAT_MODE_OFF,
REPEAT_MODE_ONE,
SUPPORT_BROWSE_MEDIA,
SUPPORT_NEXT_TRACK,
SUPPORT_PAUSE,
SUPPORT_PLAY,
@ -27,6 +30,7 @@ from homeassistant.components.media_player.const import (
SUPPORT_PREVIOUS_TRACK,
SUPPORT_REPEAT_SET,
SUPPORT_SEEK,
SUPPORT_SELECT_SOURCE,
SUPPORT_SHUFFLE_SET,
SUPPORT_STOP,
SUPPORT_TURN_OFF,
@ -46,6 +50,7 @@ from homeassistant.core import callback
import homeassistant.util.dt as dt_util
from . import AppleTVEntity
from .browse_media import build_app_list
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@ -60,6 +65,7 @@ SUPPORT_BASE = SUPPORT_TURN_ON | SUPPORT_TURN_OFF
# of these).
SUPPORT_APPLE_TV = (
SUPPORT_BASE
| SUPPORT_BROWSE_MEDIA
| SUPPORT_PLAY_MEDIA
| SUPPORT_PAUSE
| SUPPORT_PLAY
@ -89,6 +95,8 @@ SUPPORT_FEATURE_MAPPING = {
FeatureName.SetRepeat: SUPPORT_REPEAT_SET,
FeatureName.SetShuffle: SUPPORT_SHUFFLE_SET,
FeatureName.SetVolume: SUPPORT_VOLUME_SET,
FeatureName.AppList: SUPPORT_BROWSE_MEDIA | SUPPORT_SELECT_SOURCE,
FeatureName.LaunchApp: SUPPORT_BROWSE_MEDIA | SUPPORT_SELECT_SOURCE,
}
@ -108,6 +116,7 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity):
"""Initialize the Apple TV media player."""
super().__init__(name, identifier, manager, **kwargs)
self._playing = None
self._app_list = {}
@callback
def async_device_connected(self, atv):
@ -135,6 +144,21 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity):
# Listen to power updates
self.atv.power.listener = self
if self.atv.features.in_state(FeatureState.Available, FeatureName.AppList):
self.hass.create_task(self._update_app_list())
async def _update_app_list(self):
_LOGGER.debug("Updating app list")
try:
apps = await self.atv.apps.app_list()
except exceptions.NotSupportedError:
_LOGGER.error("Listing apps is not supported")
except exceptions.ProtocolError:
_LOGGER.exception("Failed to update app list")
else:
self._app_list = {app.name: app.identifier for app in apps}
self.async_write_ha_state()
@callback
def async_device_disconnected(self):
"""Handle when connection was lost to device."""
@ -198,6 +222,11 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity):
return self.atv.metadata.app.name
return None
@property
def source_list(self):
"""List of available input sources."""
return list(self._app_list.keys())
@property
def media_content_type(self):
"""Content type of current playing media."""
@ -248,7 +277,9 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity):
"""Send the play_media command to the media player."""
# If input (file) has a file format supported by pyatv, then stream it with
# RAOP. Otherwise try to play it with regular AirPlay.
if self._is_feature_available(FeatureName.StreamFile) and (
if media_type == MEDIA_TYPE_APP:
await self.atv.apps.launch_app(media_id)
elif self._is_feature_available(FeatureName.StreamFile) and (
await is_streamable(media_id) or media_type == MEDIA_TYPE_MUSIC
):
_LOGGER.debug("Streaming %s via RAOP", media_id)
@ -346,6 +377,14 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity):
return self.atv.features.in_state(FeatureState.Available, feature)
return False
async def async_browse_media(
self,
media_content_type=None,
media_content_id=None,
) -> BrowseMedia:
"""Implement the websocket media browsing helper."""
return build_app_list(self._app_list)
async def async_turn_on(self):
"""Turn the media player on."""
if self._is_feature_available(FeatureName.TurnOn):
@ -425,3 +464,8 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity):
await self.atv.remote_control.set_shuffle(
ShuffleState.Songs if shuffle else ShuffleState.Off
)
async def async_select_source(self, source: str) -> None:
"""Select input source."""
if app_id := self._app_list.get(source):
await self.atv.apps.launch_app(app_id)