diff --git a/.coveragerc b/.coveragerc
index c74de0026b1..9f538911a6e 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -14,6 +14,9 @@ omit =
homeassistant/components/apcupsd.py
homeassistant/components/*/apcupsd.py
+ homeassistant/components/apple_tv.py
+ homeassistant/components/*/apple_tv.py
+
homeassistant/components/arduino.py
homeassistant/components/*/arduino.py
@@ -302,7 +305,6 @@ omit =
homeassistant/components/lock/lockitron.py
homeassistant/components/lock/sesame.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
diff --git a/homeassistant/components/apple_tv.py b/homeassistant/components/apple_tv.py
new file mode 100644
index 00000000000..17cc46f3318
--- /dev/null
+++ b/homeassistant/components/apple_tv.py
@@ -0,0 +1,259 @@
+"""
+Support for Apple TV.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/apple_tv/
+"""
+import os
+import asyncio
+import logging
+
+import voluptuous as vol
+
+from homeassistant.const import (CONF_HOST, CONF_NAME, ATTR_ENTITY_ID)
+from homeassistant.config import load_yaml_config_file
+from homeassistant.helpers.aiohttp_client import async_get_clientsession
+from homeassistant.helpers import discovery
+from homeassistant.components.discovery import SERVICE_APPLE_TV
+from homeassistant.loader import get_component
+import homeassistant.helpers.config_validation as cv
+
+REQUIREMENTS = ['pyatv==0.3.2']
+
+_LOGGER = logging.getLogger(__name__)
+
+DOMAIN = 'apple_tv'
+
+SERVICE_SCAN = 'apple_tv_scan'
+SERVICE_AUTHENTICATE = 'apple_tv_authenticate'
+
+ATTR_ATV = 'atv'
+ATTR_POWER = 'power'
+
+CONF_LOGIN_ID = 'login_id'
+CONF_START_OFF = 'start_off'
+CONF_CREDENTIALS = 'credentials'
+
+DEFAULT_NAME = 'Apple TV'
+
+DATA_APPLE_TV = 'data_apple_tv'
+DATA_ENTITIES = 'data_apple_tv_entities'
+
+KEY_CONFIG = 'apple_tv_configuring'
+
+NOTIFICATION_AUTH_ID = 'apple_tv_auth_notification'
+NOTIFICATION_AUTH_TITLE = 'Apple TV Authentication'
+NOTIFICATION_SCAN_ID = 'apple_tv_scan_notification'
+NOTIFICATION_SCAN_TITLE = 'Apple TV Scan'
+
+CONFIG_SCHEMA = vol.Schema({
+ DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
+ vol.Required(CONF_HOST): cv.string,
+ vol.Required(CONF_LOGIN_ID): cv.string,
+ vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
+ vol.Optional(CONF_CREDENTIALS, default=None): cv.string,
+ vol.Optional(CONF_START_OFF, default=False): cv.boolean
+ })])
+}, extra=vol.ALLOW_EXTRA)
+
+# Currently no attributes but it might change later
+APPLE_TV_SCAN_SCHEMA = vol.Schema({})
+
+APPLE_TV_AUTHENTICATE_SCHEMA = vol.Schema({
+ ATTR_ENTITY_ID: cv.entity_ids,
+})
+
+
+def request_configuration(hass, config, atv, credentials):
+ """Request configuration steps from the user."""
+ configurator = get_component('configurator')
+
+ @asyncio.coroutine
+ def configuration_callback(callback_data):
+ """Handle the submitted configuration."""
+ from pyatv import exceptions
+ pin = callback_data.get('pin')
+ notification = get_component('persistent_notification')
+
+ try:
+ yield from atv.airplay.finish_authentication(pin)
+ notification.async_create(
+ hass,
+ 'Authentication succeeded!
Add the following '
+ 'to credentials: in your apple_tv configuration:
'
+ '{0}'.format(credentials),
+ title=NOTIFICATION_AUTH_TITLE,
+ notification_id=NOTIFICATION_AUTH_ID)
+ except exceptions.DeviceAuthenticationError as ex:
+ notification.async_create(
+ hass,
+ 'Authentication failed! Did you enter correct PIN?
'
+ 'Details: {0}'.format(ex),
+ title=NOTIFICATION_AUTH_TITLE,
+ notification_id=NOTIFICATION_AUTH_ID)
+
+ hass.async_add_job(configurator.request_done, instance)
+
+ instance = configurator.request_config(
+ hass, 'Apple TV Authentication', configuration_callback,
+ description='Please enter PIN code shown on screen.',
+ submit_caption='Confirm',
+ fields=[{'id': 'pin', 'name': 'PIN Code', 'type': 'password'}]
+ )
+
+
+@asyncio.coroutine
+def scan_for_apple_tvs(hass):
+ """Scan for devices and present a notification of the ones found."""
+ import pyatv
+ atvs = yield from pyatv.scan_for_apple_tvs(hass.loop, timeout=3)
+
+ devices = []
+ for atv in atvs:
+ login_id = atv.login_id
+ if login_id is None:
+ login_id = 'Home Sharing disabled'
+ devices.append('Name: {0}
Host: {1}
Login ID: {2}'.format(
+ atv.name, atv.address, login_id))
+
+ if not devices:
+ devices = ['No device(s) found']
+
+ notification = get_component('persistent_notification')
+ notification.async_create(
+ hass,
+ 'The following devices were found:
' +
+ '
'.join(devices),
+ title=NOTIFICATION_SCAN_TITLE,
+ notification_id=NOTIFICATION_SCAN_ID)
+
+
+@asyncio.coroutine
+def async_setup(hass, config):
+ """Set up the Apple TV component."""
+ if DATA_APPLE_TV not in hass.data:
+ hass.data[DATA_APPLE_TV] = {}
+
+ @asyncio.coroutine
+ def async_service_handler(service):
+ """Handler for service calls."""
+ entity_ids = service.data.get(ATTR_ENTITY_ID)
+
+ if entity_ids:
+ devices = [device for device in hass.data[DATA_ENTITIES]
+ if device.entity_id in entity_ids]
+ else:
+ devices = hass.data[DATA_ENTITIES]
+
+ for device in devices:
+ atv = device.atv
+ if service.service == SERVICE_AUTHENTICATE:
+ credentials = yield from atv.airplay.generate_credentials()
+ yield from atv.airplay.load_credentials(credentials)
+ _LOGGER.debug('Generated new credentials: %s', credentials)
+ yield from atv.airplay.start_authentication()
+ hass.async_add_job(request_configuration,
+ hass, config, atv, credentials)
+ elif service.service == SERVICE_SCAN:
+ hass.async_add_job(scan_for_apple_tvs, hass)
+
+ @asyncio.coroutine
+ def atv_discovered(service, info):
+ """Setup an Apple TV that was auto discovered."""
+ yield from _setup_atv(hass, {
+ CONF_NAME: info['name'],
+ CONF_HOST: info['host'],
+ CONF_LOGIN_ID: info['properties']['hG'],
+ CONF_START_OFF: False
+ })
+
+ discovery.async_listen(hass, SERVICE_APPLE_TV, atv_discovered)
+
+ tasks = [_setup_atv(hass, conf) for conf in config.get(DOMAIN, [])]
+ if tasks:
+ yield from asyncio.wait(tasks, loop=hass.loop)
+
+ descriptions = yield from hass.async_add_job(
+ load_yaml_config_file, os.path.join(
+ os.path.dirname(__file__), 'services.yaml'))
+
+ hass.services.async_register(
+ DOMAIN, SERVICE_SCAN, async_service_handler,
+ descriptions.get(SERVICE_SCAN),
+ schema=APPLE_TV_SCAN_SCHEMA)
+
+ hass.services.async_register(
+ DOMAIN, SERVICE_AUTHENTICATE, async_service_handler,
+ descriptions.get(SERVICE_AUTHENTICATE),
+ schema=APPLE_TV_AUTHENTICATE_SCHEMA)
+
+ return True
+
+
+@asyncio.coroutine
+def _setup_atv(hass, atv_config):
+ """Setup an Apple TV."""
+ import pyatv
+ name = atv_config.get(CONF_NAME)
+ host = atv_config.get(CONF_HOST)
+ login_id = atv_config.get(CONF_LOGIN_ID)
+ start_off = atv_config.get(CONF_START_OFF)
+ credentials = atv_config.get(CONF_CREDENTIALS)
+
+ if host in hass.data[DATA_APPLE_TV]:
+ return
+
+ details = pyatv.AppleTVDevice(name, host, login_id)
+ session = async_get_clientsession(hass)
+ atv = pyatv.connect_to_apple_tv(details, hass.loop, session=session)
+ if credentials:
+ yield from atv.airplay.load_credentials(credentials)
+
+ power = AppleTVPowerManager(hass, atv, start_off)
+ hass.data[DATA_APPLE_TV][host] = {
+ ATTR_ATV: atv,
+ ATTR_POWER: power
+ }
+
+ hass.async_add_job(discovery.async_load_platform(
+ hass, 'media_player', DOMAIN, atv_config))
+
+ hass.async_add_job(discovery.async_load_platform(
+ hass, 'remote', DOMAIN, atv_config))
+
+
+class AppleTVPowerManager:
+ """Manager for global power management of an Apple TV.
+
+ An instance is used per device to share the same power state between
+ several platforms.
+ """
+
+ def __init__(self, hass, atv, is_off):
+ """Initialize power manager."""
+ self.hass = hass
+ self.atv = atv
+ self.listeners = []
+ self._is_on = not is_off
+
+ def init(self):
+ """Initialize power management."""
+ if self._is_on:
+ self.atv.push_updater.start()
+
+ @property
+ def turned_on(self):
+ """If device is on or off."""
+ return self._is_on
+
+ def set_power_on(self, value):
+ """Change if a device is on or off."""
+ if value != self._is_on:
+ self._is_on = value
+ if not self._is_on:
+ self.atv.push_updater.stop()
+ else:
+ self.atv.push_updater.start()
+
+ for listener in self.listeners:
+ self.hass.async_add_job(listener.async_update_ha_state())
diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py
index fc239bf70c5..3dfe4b9731c 100644
--- a/homeassistant/components/discovery.py
+++ b/homeassistant/components/discovery.py
@@ -32,6 +32,7 @@ SERVICE_HASS_IOS_APP = 'hass_ios'
SERVICE_IKEA_TRADFRI = 'ikea_tradfri'
SERVICE_HASSIO = 'hassio'
SERVICE_AXIS = 'axis'
+SERVICE_APPLE_TV = 'apple_tv'
SERVICE_HANDLERS = {
SERVICE_HASS_IOS_APP: ('ios', None),
@@ -40,6 +41,7 @@ SERVICE_HANDLERS = {
SERVICE_IKEA_TRADFRI: ('tradfri', None),
SERVICE_HASSIO: ('hassio', None),
SERVICE_AXIS: ('axis', None),
+ SERVICE_APPLE_TV: ('apple_tv', None),
'philips_hue': ('light', 'hue'),
'google_cast': ('media_player', 'cast'),
'panasonic_viera': ('media_player', 'panasonic_viera'),
@@ -52,7 +54,6 @@ SERVICE_HANDLERS = {
'denonavr': ('media_player', 'denonavr'),
'samsung_tv': ('media_player', 'samsungtv'),
'yeelight': ('light', 'yeelight'),
- 'apple_tv': ('media_player', 'apple_tv'),
'frontier_silicon': ('media_player', 'frontier_silicon'),
'openhome': ('media_player', 'openhome'),
'harmony': ('remote', 'harmony'),
diff --git a/homeassistant/components/media_player/apple_tv.py b/homeassistant/components/media_player/apple_tv.py
index 97114a6bc84..a7017f73fc4 100644
--- a/homeassistant/components/media_player/apple_tv.py
+++ b/homeassistant/components/media_player/apple_tv.py
@@ -6,70 +6,41 @@ https://home-assistant.io/components/media_player.apple_tv/
"""
import asyncio
import logging
-import hashlib
-
-import voluptuous as vol
from homeassistant.core import callback
+from homeassistant.components.apple_tv import (
+ ATTR_ATV, ATTR_POWER, DATA_APPLE_TV, DATA_ENTITIES)
from homeassistant.components.media_player import (
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
SUPPORT_STOP, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_TURN_ON,
- SUPPORT_TURN_OFF, MediaPlayerDevice, PLATFORM_SCHEMA, MEDIA_TYPE_MUSIC,
+ SUPPORT_TURN_OFF, MediaPlayerDevice, MEDIA_TYPE_MUSIC,
MEDIA_TYPE_VIDEO, MEDIA_TYPE_TVSHOW)
from homeassistant.const import (
STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_STANDBY, CONF_HOST,
STATE_OFF, CONF_NAME, EVENT_HOMEASSISTANT_STOP)
-from homeassistant.helpers.aiohttp_client import async_get_clientsession
-import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util
-REQUIREMENTS = ['pyatv==0.2.1']
+DEPENDENCIES = ['apple_tv']
_LOGGER = logging.getLogger(__name__)
-CONF_LOGIN_ID = 'login_id'
-CONF_START_OFF = 'start_off'
-
-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,
- vol.Optional(CONF_START_OFF, default=False): cv.boolean
-})
-
@asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the Apple TV platform."""
- import pyatv
+ if not discovery_info:
+ return
- if discovery_info is not None:
- name = discovery_info['name']
- host = discovery_info['host']
- login_id = discovery_info['properties']['hG']
- start_off = False
- else:
- name = config.get(CONF_NAME)
- host = config.get(CONF_HOST)
- login_id = config.get(CONF_LOGIN_ID)
- start_off = config.get(CONF_START_OFF)
+ # Manage entity cache for service handler
+ if DATA_ENTITIES not in hass.data:
+ hass.data[DATA_ENTITIES] = []
- if DATA_APPLE_TV not in hass.data:
- hass.data[DATA_APPLE_TV] = []
-
- if host in hass.data[DATA_APPLE_TV]:
- return False
- hass.data[DATA_APPLE_TV].append(host)
-
- details = pyatv.AppleTVDevice(name, host, login_id)
- session = async_get_clientsession(hass)
- atv = pyatv.connect_to_apple_tv(details, hass.loop, session=session)
- entity = AppleTvDevice(atv, name, start_off)
+ name = discovery_info[CONF_NAME]
+ host = discovery_info[CONF_HOST]
+ atv = hass.data[DATA_APPLE_TV][host][ATTR_ATV]
+ power = hass.data[DATA_APPLE_TV][host][ATTR_POWER]
+ entity = AppleTvDevice(atv, name, power)
@callback
def on_hass_stop(event):
@@ -78,44 +49,39 @@ def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop)
+ if entity not in hass.data[DATA_ENTITIES]:
+ hass.data[DATA_ENTITIES].append(entity)
+
async_add_devices([entity])
class AppleTvDevice(MediaPlayerDevice):
"""Representation of an Apple TV device."""
- def __init__(self, atv, name, is_off):
+ def __init__(self, atv, name, power):
"""Initialize the Apple TV device."""
- self._atv = atv
+ self.atv = atv
self._name = name
- self._is_off = is_off
self._playing = None
- self._artwork_hash = None
- self._atv.push_updater.listener = self
+ self._power = power
+ self._power.listeners.append(self)
+ self.atv.push_updater.listener = self
@asyncio.coroutine
def async_added_to_hass(self):
"""Handle when an entity is about to be added to Home Assistant."""
- if not self._is_off:
- self._atv.push_updater.start()
-
- @callback
- def _set_power_off(self, is_off):
- """Set the power to off."""
- self._playing = None
- self._artwork_hash = None
- self._is_off = is_off
- if is_off:
- self._atv.push_updater.stop()
- else:
- self._atv.push_updater.start()
- self.hass.async_add_job(self.async_update_ha_state())
+ self._power.init()
@property
def name(self):
"""Return the name of the device."""
return self._name
+ @property
+ def unique_id(self):
+ """Return an unique ID."""
+ return self.atv.metadata.device_id
+
@property
def should_poll(self):
"""No polling needed."""
@@ -124,16 +90,16 @@ class AppleTvDevice(MediaPlayerDevice):
@property
def state(self):
"""Return the state of the device."""
- if self._is_off:
+ if not self._power.turned_on:
return STATE_OFF
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 \
+ if state == const.PLAY_STATE_NO_MEDIA or \
state == const.PLAY_STATE_LOADING:
+ return STATE_IDLE
+ elif state == const.PLAY_STATE_PLAYING:
return STATE_PLAYING
elif state == const.PLAY_STATE_PAUSED or \
state == const.PLAY_STATE_FAST_FORWARD or \
@@ -147,24 +113,8 @@ class AppleTvDevice(MediaPlayerDevice):
def playstatus_update(self, updater, playing):
"""Print what is currently playing when it changes."""
self._playing = playing
-
- if self.state == STATE_IDLE:
- self._artwork_hash = None
- elif self._has_playing_media_changed(playing):
- base = str(playing.title) + str(playing.artist) + \
- str(playing.album) + str(playing.total_time)
- self._artwork_hash = hashlib.md5(
- base.encode('utf-8')).hexdigest()
-
self.hass.async_add_job(self.async_update_ha_state())
- def _has_playing_media_changed(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
-
@callback
def playstatus_error(self, updater, exception):
"""Inform about an error and restart push updates."""
@@ -177,7 +127,6 @@ class AppleTvDevice(MediaPlayerDevice):
# implemented here later.
updater.start(initial_delay=10)
self._playing = None
- self._artwork_hash = None
self.hass.async_add_job(self.async_update_ha_state())
@property
@@ -215,18 +164,18 @@ class AppleTvDevice(MediaPlayerDevice):
@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)
+ yield from self.atv.airplay.play_url(media_id)
@property
def media_image_hash(self):
"""Hash value for media image."""
- if self.state != STATE_IDLE:
- return self._artwork_hash
+ if self._playing is not None and self.state != STATE_IDLE:
+ return self._playing.hash
@asyncio.coroutine
def async_get_media_image(self):
"""Fetch media image of current playing image."""
- return (yield from self._atv.metadata.artwork()), 'image/png'
+ return (yield from self.atv.metadata.artwork()), 'image/png'
@property
def media_title(self):
@@ -235,9 +184,9 @@ class AppleTvDevice(MediaPlayerDevice):
if self.state == STATE_IDLE:
return 'Nothing playing'
title = self._playing.title
- return title if title else "No title"
+ return title if title else 'No title'
- return 'Not connected to Apple TV'
+ return 'Establishing a connection to {0}...'.format(self._name)
@property
def supported_features(self):
@@ -254,12 +203,13 @@ class AppleTvDevice(MediaPlayerDevice):
@asyncio.coroutine
def async_turn_on(self):
"""Turn the media player on."""
- self._set_power_off(False)
+ self._power.set_power_on(True)
@asyncio.coroutine
def async_turn_off(self):
"""Turn the media player off."""
- self._set_power_off(True)
+ self._playing = None
+ self._power.set_power_on(False)
def async_media_play_pause(self):
"""Pause media on media player.
@@ -269,9 +219,9 @@ class AppleTvDevice(MediaPlayerDevice):
if self._playing is not None:
state = self.state
if state == STATE_PAUSED:
- return self._atv.remote_control.play()
+ return self.atv.remote_control.play()
elif state == STATE_PLAYING:
- return self._atv.remote_control.pause()
+ return self.atv.remote_control.pause()
def async_media_play(self):
"""Play media.
@@ -279,7 +229,15 @@ class AppleTvDevice(MediaPlayerDevice):
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()
+ return self.atv.remote_control.play()
+
+ def async_media_stop(self):
+ """Stop 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.stop()
def async_media_pause(self):
"""Pause the media player.
@@ -287,7 +245,7 @@ class AppleTvDevice(MediaPlayerDevice):
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()
+ return self.atv.remote_control.pause()
def async_media_next_track(self):
"""Send next track command.
@@ -295,7 +253,7 @@ class AppleTvDevice(MediaPlayerDevice):
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()
+ return self.atv.remote_control.next()
def async_media_previous_track(self):
"""Send previous track command.
@@ -303,7 +261,7 @@ class AppleTvDevice(MediaPlayerDevice):
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()
+ return self.atv.remote_control.previous()
def async_media_seek(self, position):
"""Send seek command.
@@ -311,4 +269,4 @@ class AppleTvDevice(MediaPlayerDevice):
This method must be run in the event loop and returns a coroutine.
"""
if self._playing is not None:
- return self._atv.remote_control.set_position(position)
+ return self.atv.remote_control.set_position(position)
diff --git a/homeassistant/components/remote/apple_tv.py b/homeassistant/components/remote/apple_tv.py
new file mode 100644
index 00000000000..a7ea113c2db
--- /dev/null
+++ b/homeassistant/components/remote/apple_tv.py
@@ -0,0 +1,87 @@
+"""
+Remote control support for Apple TV.
+
+For more details about this platform, please refer to the documentation at
+https://home-assistant.io/components/remote.apple_tv/
+"""
+import asyncio
+
+from homeassistant.components.apple_tv import (
+ ATTR_ATV, ATTR_POWER, DATA_APPLE_TV)
+from homeassistant.components.remote import ATTR_COMMAND
+from homeassistant.components import remote
+from homeassistant.const import (CONF_NAME, CONF_HOST)
+
+
+DEPENDENCIES = ['apple_tv']
+
+
+@asyncio.coroutine
+def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
+ """Set up the Apple TV remote platform."""
+ if not discovery_info:
+ return
+
+ name = discovery_info[CONF_NAME]
+ host = discovery_info[CONF_HOST]
+ atv = hass.data[DATA_APPLE_TV][host][ATTR_ATV]
+ power = hass.data[DATA_APPLE_TV][host][ATTR_POWER]
+ async_add_devices([AppleTVRemote(atv, power, name)])
+
+
+class AppleTVRemote(remote.RemoteDevice):
+ """Device that sends commands to an Apple TV."""
+
+ def __init__(self, atv, power, name):
+ """Initialize device."""
+ self._atv = atv
+ self._name = name
+ self._power = power
+ self._power.listeners.append(self)
+
+ @property
+ def name(self):
+ """Return the name of the device."""
+ return self._name
+
+ @property
+ def is_on(self):
+ """Return true if device is on."""
+ return self._power.turned_on
+
+ @property
+ def should_poll(self):
+ """No polling needed for Apple TV."""
+ return False
+
+ @asyncio.coroutine
+ def async_turn_on(self, **kwargs):
+ """Turn the device on.
+
+ This method is a coroutine.
+ """
+ self._power.set_power_on(True)
+
+ @asyncio.coroutine
+ def async_turn_off(self):
+ """Turn the device off.
+
+ This method is a coroutine.
+ """
+ self._power.set_power_on(False)
+
+ def async_send_command(self, **kwargs):
+ """Send a command to one device.
+
+ This method must be run in the event loop and returns a coroutine.
+ """
+ # Send commands in specified order but schedule only one coroutine
+ @asyncio.coroutine
+ def _send_commads():
+ for command in kwargs[ATTR_COMMAND]:
+ if not hasattr(self._atv.remote_control, command):
+ continue
+
+ yield from getattr(self._atv.remote_control, command)()
+
+ return _send_commads()
diff --git a/homeassistant/components/services.yaml b/homeassistant/components/services.yaml
index d81d14fc991..db71b2322fa 100644
--- a/homeassistant/components/services.yaml
+++ b/homeassistant/components/services.yaml
@@ -470,3 +470,16 @@ axis:
param:
description: What parameter to operate on. [Required]
example: 'package=VideoMotionDetection'
+
+apple_tv:
+ apple_tv_authenticate:
+ description: Start AirPlay device authentication.
+
+ fields:
+ entity_id:
+ description: Name(s) of entities to authenticate with.
+ example: 'media_player.apple_tv'
+
+ apple_tv_scan:
+ description: Scan for Apple TV devices.
+
diff --git a/requirements_all.txt b/requirements_all.txt
index fc71a3ab3ff..d3f24dad542 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -514,8 +514,8 @@ pyasn1-modules==0.0.9
# homeassistant.components.notify.xmpp
pyasn1==0.2.3
-# homeassistant.components.media_player.apple_tv
-pyatv==0.2.1
+# homeassistant.components.apple_tv
+pyatv==0.3.2
# homeassistant.components.device_tracker.bbox
# homeassistant.components.sensor.bbox