Add new feature to Apple TV platform (#8122)
* Lift Apple TV to pyatv 0.3.2 Update code to use basic new features. * Support button presses in Apple TV * Support device authentication * Convert Apple TV to a component A media_player platform and a remote platform will be loaded for each manually configured or discovered device. * Move device auth to apple_tv component * Update requirements and coverage config * Add scan support to apple_tv
This commit is contained in:
parent
8185587100
commit
ea5bec3ef4
7 changed files with 422 additions and 102 deletions
|
@ -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
|
||||
|
|
259
homeassistant/components/apple_tv.py
Normal file
259
homeassistant/components/apple_tv.py
Normal file
|
@ -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!<br /><br />Add the following '
|
||||
'to credentials: in your apple_tv configuration:<br /><br />'
|
||||
'{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?<br /><br />'
|
||||
'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}<br />Host: {1}<br />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:<br /><br />' +
|
||||
'<br /><br />'.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())
|
|
@ -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'),
|
||||
|
|
|
@ -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)
|
||||
|
|
87
homeassistant/components/remote/apple_tv.py
Normal file
87
homeassistant/components/remote/apple_tv.py
Normal file
|
@ -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()
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue