Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ omit =
homeassistant/components/lirc.py
homeassistant/components/lock/nuki.py
homeassistant/components/media_player/anthemav.py
homeassistant/components/media_player/apple_tv.py
homeassistant/components/media_player/aquostv.py
homeassistant/components/media_player/braviatv.py
homeassistant/components/media_player/cast.py
Expand Down
246 changes: 246 additions & 0 deletions homeassistant/components/media_player/apple_tv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
"""
Support for Apple TV.

For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.apple_tv/
"""
import asyncio
import logging
import hashlib

import voluptuous as vol

from homeassistant.components.media_player import (
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
SUPPORT_STOP, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, MediaPlayerDevice,
PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC, MEDIA_TYPE_VIDEO, MEDIA_TYPE_TVSHOW)
from homeassistant.const import (
STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_STANDBY, CONF_HOST,
CONF_NAME, EVENT_HOMEASSISTANT_STOP)
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util


REQUIREMENTS = ['pyatv==0.0.1']

_LOGGER = logging.getLogger(__name__)

CONF_LOGIN_ID = 'login_id'

DEFAULT_NAME = 'Apple TV'

DATA_APPLE_TV = 'apple_tv'

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_LOGIN_ID): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})


@asyncio.coroutine
def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Setup the Apple TV platform."""
import pyatv

if DATA_APPLE_TV not in hass.data:
hass.data[DATA_APPLE_TV] = []

name = config.get(CONF_NAME)
host = config.get(CONF_HOST)
login_id = config.get(CONF_LOGIN_ID)

key = '{}:{}'.format(host, name)
if key in hass.data[DATA_APPLE_TV]:
return False
hass.data[DATA_APPLE_TV].append(key)

details = pyatv.AppleTVDevice(name, host, login_id)
atv = pyatv.connect_to_apple_tv(details, hass.loop)
entity = AppleTvDevice(atv, name)

@asyncio.coroutine
def async_stop_subscription(event):
"""Logout device to close its session."""
_LOGGER.info("Closing Apple TV session")
yield from atv.logout()

hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP,
async_stop_subscription)

yield from async_add_entities([entity], update_before_add=True)


class AppleTvDevice(MediaPlayerDevice):
"""Representation of an Apple TV device."""

def __init__(self, atv, name):
"""Initialize the Apple TV device."""
self._name = name
self._atv = atv
self._playing = None
self._artwork = None
self._artwork_hash = None

@property
def name(self):
"""Return the name of the device."""
return self._name

@property
def state(self):
"""Return the state of the device."""
if self._playing is not None:
from pyatv import const
state = self._playing.play_state
if state == const.PLAY_STATE_NO_MEDIA:
return STATE_IDLE
elif state == const.PLAY_STATE_PLAYING or \
state == const.PLAY_STATE_LOADING:
return STATE_PLAYING
elif state == const.PLAY_STATE_PAUSED or \
state == const.PLAY_STATE_FAST_FORWARD or \
state == const.PLAY_STATE_FAST_BACKWARD:
# Catch fast forward/backward here so "play" is default action
return STATE_PAUSED
else:
return STATE_STANDBY # Bad or unknown state?

@asyncio.coroutine
def async_update(self):
"""Retrieve latest state."""
from pyatv import exceptions
try:
playing = yield from self._atv.metadata.playing()

if self._should_download_artwork(playing):
self._artwork = None
self._artwork_hash = None
self._artwork = yield from self._atv.metadata.artwork()
if self._artwork:
self._artwork_hash = hashlib.md5(self._artwork).hexdigest()

self._playing = playing
except exceptions.AuthenticationError as ex:
_LOGGER.warning('%s (bad login id?)', str(ex))
except asyncio.TimeoutError:
_LOGGER.warning('timed out while connecting to Apple TV')

def _should_download_artwork(self, new_playing):
if self._playing is None:
return True
old_playing = self._playing
return new_playing.media_type != old_playing.media_type or \
new_playing.title != old_playing.title

@property
def media_content_type(self):
"""Content type of current playing media."""
if self._playing is not None:
from pyatv import const
media_type = self._playing.media_type
if media_type == const.MEDIA_TYPE_VIDEO:
return MEDIA_TYPE_VIDEO
elif media_type == const.MEDIA_TYPE_MUSIC:
return MEDIA_TYPE_MUSIC
elif media_type == const.MEDIA_TYPE_TV:
return MEDIA_TYPE_TVSHOW

@property
def media_duration(self):
"""Duration of current playing media in seconds."""
if self._playing is not None:
return self._playing.total_time

@property
def media_position(self):
"""Position of current playing media in seconds."""
if self._playing is not None:
return self._playing.position

@property
def media_position_updated_at(self):
"""Last valid time of media position."""
state = self.state
if state == STATE_PLAYING or state == STATE_PAUSED:
return dt_util.utcnow()

@asyncio.coroutine
def async_play_media(self, media_type, media_id, **kwargs):
"""Send the play_media command to the media player."""
yield from self._atv.remote_control.play_url(media_id, 0)

@property
def media_image(self):
"""Artwork for what is currently playing."""
return self._artwork, 'image/png', self._artwork_hash

@property
def media_title(self):
"""Title of current playing media."""
if self._playing is not None:
title = self._playing.title
return title if title else "No title"

@property
def supported_media_commands(self):
"""Flag of media commands that are supported."""
if self._playing is not None:
if self.state != STATE_IDLE:
return SUPPORT_PAUSE | SUPPORT_PLAY | \
SUPPORT_SEEK | SUPPORT_STOP | \
SUPPORT_NEXT_TRACK | SUPPORT_PREVIOUS_TRACK | \
SUPPORT_PLAY_MEDIA
else:
return SUPPORT_PLAY_MEDIA

def async_media_play_pause(self):
"""Pause media on media player.

This method must be run in the event loop and returns a coroutine.
"""
if self._playing is not None:
state = self.state
if state == STATE_PAUSED:
return self._atv.remote_control.play()
elif state == STATE_PLAYING:
return self._atv.remote_control.pause()

def async_media_play(self):
"""Play media.

This method must be run in the event loop and returns a coroutine.
"""
if self._playing is not None:
return self._atv.remote_control.play()

def async_media_pause(self):
"""Pause the media player.

This method must be run in the event loop and returns a coroutine.
"""
if self._playing is not None:
return self._atv.remote_control.pause()

def async_media_next_track(self):
"""Send next track command.

This method must be run in the event loop and returns a coroutine.
"""
if self._playing is not None:
return self._atv.remote_control.next()

def async_media_previous_track(self):
"""Send previous track command.

This method must be run in the event loop and returns a coroutine.
"""
if self._playing is not None:
return self._atv.remote_control.previous()

@asyncio.coroutine
def async_media_seek(self, position):
"""Send seek command."""
if self._playing is not None:
yield from self._atv.remote_control.set_position(position)
3 changes: 3 additions & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,9 @@ pyasn1-modules==0.0.8
# homeassistant.components.notify.xmpp
pyasn1==0.2.1

# homeassistant.components.media_player.apple_tv
pyatv==0.0.1

# homeassistant.components.device_tracker.bbox
# homeassistant.components.sensor.bbox
pybbox==0.0.5-alpha
Expand Down