Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
238 changes: 238 additions & 0 deletions homeassistant/components/media_player/apple_tv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
"""
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, 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()

@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

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