mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
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
@ -14,6 +14,9 @@ omit =
|
|||||||
homeassistant/components/apcupsd.py
|
homeassistant/components/apcupsd.py
|
||||||
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
|
||||||
homeassistant/components/*/arduino.py
|
homeassistant/components/*/arduino.py
|
||||||
|
|
||||||
@ -302,7 +305,6 @@ omit =
|
|||||||
homeassistant/components/lock/lockitron.py
|
homeassistant/components/lock/lockitron.py
|
||||||
homeassistant/components/lock/sesame.py
|
homeassistant/components/lock/sesame.py
|
||||||
homeassistant/components/media_player/anthemav.py
|
homeassistant/components/media_player/anthemav.py
|
||||||
homeassistant/components/media_player/apple_tv.py
|
|
||||||
homeassistant/components/media_player/aquostv.py
|
homeassistant/components/media_player/aquostv.py
|
||||||
homeassistant/components/media_player/braviatv.py
|
homeassistant/components/media_player/braviatv.py
|
||||||
homeassistant/components/media_player/cast.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_IKEA_TRADFRI = 'ikea_tradfri'
|
||||||
SERVICE_HASSIO = 'hassio'
|
SERVICE_HASSIO = 'hassio'
|
||||||
SERVICE_AXIS = 'axis'
|
SERVICE_AXIS = 'axis'
|
||||||
|
SERVICE_APPLE_TV = 'apple_tv'
|
||||||
|
|
||||||
SERVICE_HANDLERS = {
|
SERVICE_HANDLERS = {
|
||||||
SERVICE_HASS_IOS_APP: ('ios', None),
|
SERVICE_HASS_IOS_APP: ('ios', None),
|
||||||
@ -40,6 +41,7 @@ SERVICE_HANDLERS = {
|
|||||||
SERVICE_IKEA_TRADFRI: ('tradfri', None),
|
SERVICE_IKEA_TRADFRI: ('tradfri', None),
|
||||||
SERVICE_HASSIO: ('hassio', None),
|
SERVICE_HASSIO: ('hassio', None),
|
||||||
SERVICE_AXIS: ('axis', None),
|
SERVICE_AXIS: ('axis', None),
|
||||||
|
SERVICE_APPLE_TV: ('apple_tv', None),
|
||||||
'philips_hue': ('light', 'hue'),
|
'philips_hue': ('light', 'hue'),
|
||||||
'google_cast': ('media_player', 'cast'),
|
'google_cast': ('media_player', 'cast'),
|
||||||
'panasonic_viera': ('media_player', 'panasonic_viera'),
|
'panasonic_viera': ('media_player', 'panasonic_viera'),
|
||||||
@ -52,7 +54,6 @@ SERVICE_HANDLERS = {
|
|||||||
'denonavr': ('media_player', 'denonavr'),
|
'denonavr': ('media_player', 'denonavr'),
|
||||||
'samsung_tv': ('media_player', 'samsungtv'),
|
'samsung_tv': ('media_player', 'samsungtv'),
|
||||||
'yeelight': ('light', 'yeelight'),
|
'yeelight': ('light', 'yeelight'),
|
||||||
'apple_tv': ('media_player', 'apple_tv'),
|
|
||||||
'frontier_silicon': ('media_player', 'frontier_silicon'),
|
'frontier_silicon': ('media_player', 'frontier_silicon'),
|
||||||
'openhome': ('media_player', 'openhome'),
|
'openhome': ('media_player', 'openhome'),
|
||||||
'harmony': ('remote', 'harmony'),
|
'harmony': ('remote', 'harmony'),
|
||||||
|
@ -6,70 +6,41 @@ https://home-assistant.io/components/media_player.apple_tv/
|
|||||||
"""
|
"""
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import hashlib
|
|
||||||
|
|
||||||
import voluptuous as vol
|
|
||||||
|
|
||||||
from homeassistant.core import callback
|
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 (
|
from homeassistant.components.media_player import (
|
||||||
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
|
SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, SUPPORT_SEEK,
|
||||||
SUPPORT_STOP, SUPPORT_PLAY, SUPPORT_PLAY_MEDIA, SUPPORT_TURN_ON,
|
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)
|
MEDIA_TYPE_VIDEO, MEDIA_TYPE_TVSHOW)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_STANDBY, CONF_HOST,
|
STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_STANDBY, CONF_HOST,
|
||||||
STATE_OFF, CONF_NAME, EVENT_HOMEASSISTANT_STOP)
|
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
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
|
|
||||||
REQUIREMENTS = ['pyatv==0.2.1']
|
DEPENDENCIES = ['apple_tv']
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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
|
@asyncio.coroutine
|
||||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||||
"""Set up the Apple TV platform."""
|
"""Set up the Apple TV platform."""
|
||||||
import pyatv
|
if not discovery_info:
|
||||||
|
return
|
||||||
|
|
||||||
if discovery_info is not None:
|
# Manage entity cache for service handler
|
||||||
name = discovery_info['name']
|
if DATA_ENTITIES not in hass.data:
|
||||||
host = discovery_info['host']
|
hass.data[DATA_ENTITIES] = []
|
||||||
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)
|
|
||||||
|
|
||||||
if DATA_APPLE_TV not in hass.data:
|
name = discovery_info[CONF_NAME]
|
||||||
hass.data[DATA_APPLE_TV] = []
|
host = discovery_info[CONF_HOST]
|
||||||
|
atv = hass.data[DATA_APPLE_TV][host][ATTR_ATV]
|
||||||
if host in hass.data[DATA_APPLE_TV]:
|
power = hass.data[DATA_APPLE_TV][host][ATTR_POWER]
|
||||||
return False
|
entity = AppleTvDevice(atv, name, power)
|
||||||
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)
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def on_hass_stop(event):
|
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)
|
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])
|
async_add_devices([entity])
|
||||||
|
|
||||||
|
|
||||||
class AppleTvDevice(MediaPlayerDevice):
|
class AppleTvDevice(MediaPlayerDevice):
|
||||||
"""Representation of an Apple TV device."""
|
"""Representation of an Apple TV device."""
|
||||||
|
|
||||||
def __init__(self, atv, name, is_off):
|
def __init__(self, atv, name, power):
|
||||||
"""Initialize the Apple TV device."""
|
"""Initialize the Apple TV device."""
|
||||||
self._atv = atv
|
self.atv = atv
|
||||||
self._name = name
|
self._name = name
|
||||||
self._is_off = is_off
|
|
||||||
self._playing = None
|
self._playing = None
|
||||||
self._artwork_hash = None
|
self._power = power
|
||||||
self._atv.push_updater.listener = self
|
self._power.listeners.append(self)
|
||||||
|
self.atv.push_updater.listener = self
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_added_to_hass(self):
|
def async_added_to_hass(self):
|
||||||
"""Handle when an entity is about to be added to Home Assistant."""
|
"""Handle when an entity is about to be added to Home Assistant."""
|
||||||
if not self._is_off:
|
self._power.init()
|
||||||
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())
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Return the name of the device."""
|
"""Return the name of the device."""
|
||||||
return self._name
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self):
|
||||||
|
"""Return an unique ID."""
|
||||||
|
return self.atv.metadata.device_id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def should_poll(self):
|
def should_poll(self):
|
||||||
"""No polling needed."""
|
"""No polling needed."""
|
||||||
@ -124,16 +90,16 @@ class AppleTvDevice(MediaPlayerDevice):
|
|||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
"""Return the state of the device."""
|
"""Return the state of the device."""
|
||||||
if self._is_off:
|
if not self._power.turned_on:
|
||||||
return STATE_OFF
|
return STATE_OFF
|
||||||
|
|
||||||
if self._playing is not None:
|
if self._playing is not None:
|
||||||
from pyatv import const
|
from pyatv import const
|
||||||
state = self._playing.play_state
|
state = self._playing.play_state
|
||||||
if state == const.PLAY_STATE_NO_MEDIA:
|
if state == const.PLAY_STATE_NO_MEDIA or \
|
||||||
return STATE_IDLE
|
|
||||||
elif state == const.PLAY_STATE_PLAYING or \
|
|
||||||
state == const.PLAY_STATE_LOADING:
|
state == const.PLAY_STATE_LOADING:
|
||||||
|
return STATE_IDLE
|
||||||
|
elif state == const.PLAY_STATE_PLAYING:
|
||||||
return STATE_PLAYING
|
return STATE_PLAYING
|
||||||
elif state == const.PLAY_STATE_PAUSED or \
|
elif state == const.PLAY_STATE_PAUSED or \
|
||||||
state == const.PLAY_STATE_FAST_FORWARD or \
|
state == const.PLAY_STATE_FAST_FORWARD or \
|
||||||
@ -147,24 +113,8 @@ class AppleTvDevice(MediaPlayerDevice):
|
|||||||
def playstatus_update(self, updater, playing):
|
def playstatus_update(self, updater, playing):
|
||||||
"""Print what is currently playing when it changes."""
|
"""Print what is currently playing when it changes."""
|
||||||
self._playing = playing
|
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())
|
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
|
@callback
|
||||||
def playstatus_error(self, updater, exception):
|
def playstatus_error(self, updater, exception):
|
||||||
"""Inform about an error and restart push updates."""
|
"""Inform about an error and restart push updates."""
|
||||||
@ -177,7 +127,6 @@ class AppleTvDevice(MediaPlayerDevice):
|
|||||||
# implemented here later.
|
# implemented here later.
|
||||||
updater.start(initial_delay=10)
|
updater.start(initial_delay=10)
|
||||||
self._playing = None
|
self._playing = None
|
||||||
self._artwork_hash = None
|
|
||||||
self.hass.async_add_job(self.async_update_ha_state())
|
self.hass.async_add_job(self.async_update_ha_state())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -215,18 +164,18 @@ class AppleTvDevice(MediaPlayerDevice):
|
|||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_play_media(self, media_type, media_id, **kwargs):
|
def async_play_media(self, media_type, media_id, **kwargs):
|
||||||
"""Send the play_media command to the media player."""
|
"""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
|
@property
|
||||||
def media_image_hash(self):
|
def media_image_hash(self):
|
||||||
"""Hash value for media image."""
|
"""Hash value for media image."""
|
||||||
if self.state != STATE_IDLE:
|
if self._playing is not None and self.state != STATE_IDLE:
|
||||||
return self._artwork_hash
|
return self._playing.hash
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_get_media_image(self):
|
def async_get_media_image(self):
|
||||||
"""Fetch media image of current playing image."""
|
"""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
|
@property
|
||||||
def media_title(self):
|
def media_title(self):
|
||||||
@ -235,9 +184,9 @@ class AppleTvDevice(MediaPlayerDevice):
|
|||||||
if self.state == STATE_IDLE:
|
if self.state == STATE_IDLE:
|
||||||
return 'Nothing playing'
|
return 'Nothing playing'
|
||||||
title = self._playing.title
|
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
|
@property
|
||||||
def supported_features(self):
|
def supported_features(self):
|
||||||
@ -254,12 +203,13 @@ class AppleTvDevice(MediaPlayerDevice):
|
|||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_turn_on(self):
|
def async_turn_on(self):
|
||||||
"""Turn the media player on."""
|
"""Turn the media player on."""
|
||||||
self._set_power_off(False)
|
self._power.set_power_on(True)
|
||||||
|
|
||||||
@asyncio.coroutine
|
@asyncio.coroutine
|
||||||
def async_turn_off(self):
|
def async_turn_off(self):
|
||||||
"""Turn the media player off."""
|
"""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):
|
def async_media_play_pause(self):
|
||||||
"""Pause media on media player.
|
"""Pause media on media player.
|
||||||
@ -269,9 +219,9 @@ class AppleTvDevice(MediaPlayerDevice):
|
|||||||
if self._playing is not None:
|
if self._playing is not None:
|
||||||
state = self.state
|
state = self.state
|
||||||
if state == STATE_PAUSED:
|
if state == STATE_PAUSED:
|
||||||
return self._atv.remote_control.play()
|
return self.atv.remote_control.play()
|
||||||
elif state == STATE_PLAYING:
|
elif state == STATE_PLAYING:
|
||||||
return self._atv.remote_control.pause()
|
return self.atv.remote_control.pause()
|
||||||
|
|
||||||
def async_media_play(self):
|
def async_media_play(self):
|
||||||
"""Play media.
|
"""Play media.
|
||||||
@ -279,7 +229,15 @@ class AppleTvDevice(MediaPlayerDevice):
|
|||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
if self._playing is not None:
|
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):
|
def async_media_pause(self):
|
||||||
"""Pause the media player.
|
"""Pause the media player.
|
||||||
@ -287,7 +245,7 @@ class AppleTvDevice(MediaPlayerDevice):
|
|||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
if self._playing is not None:
|
if self._playing is not None:
|
||||||
return self._atv.remote_control.pause()
|
return self.atv.remote_control.pause()
|
||||||
|
|
||||||
def async_media_next_track(self):
|
def async_media_next_track(self):
|
||||||
"""Send next track command.
|
"""Send next track command.
|
||||||
@ -295,7 +253,7 @@ class AppleTvDevice(MediaPlayerDevice):
|
|||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
if self._playing is not None:
|
if self._playing is not None:
|
||||||
return self._atv.remote_control.next()
|
return self.atv.remote_control.next()
|
||||||
|
|
||||||
def async_media_previous_track(self):
|
def async_media_previous_track(self):
|
||||||
"""Send previous track command.
|
"""Send previous track command.
|
||||||
@ -303,7 +261,7 @@ class AppleTvDevice(MediaPlayerDevice):
|
|||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
if self._playing is not None:
|
if self._playing is not None:
|
||||||
return self._atv.remote_control.previous()
|
return self.atv.remote_control.previous()
|
||||||
|
|
||||||
def async_media_seek(self, position):
|
def async_media_seek(self, position):
|
||||||
"""Send seek command.
|
"""Send seek command.
|
||||||
@ -311,4 +269,4 @@ class AppleTvDevice(MediaPlayerDevice):
|
|||||||
This method must be run in the event loop and returns a coroutine.
|
This method must be run in the event loop and returns a coroutine.
|
||||||
"""
|
"""
|
||||||
if self._playing is not None:
|
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:
|
param:
|
||||||
description: What parameter to operate on. [Required]
|
description: What parameter to operate on. [Required]
|
||||||
example: 'package=VideoMotionDetection'
|
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
|
# homeassistant.components.notify.xmpp
|
||||||
pyasn1==0.2.3
|
pyasn1==0.2.3
|
||||||
|
|
||||||
# homeassistant.components.media_player.apple_tv
|
# homeassistant.components.apple_tv
|
||||||
pyatv==0.2.1
|
pyatv==0.3.2
|
||||||
|
|
||||||
# homeassistant.components.device_tracker.bbox
|
# homeassistant.components.device_tracker.bbox
|
||||||
# homeassistant.components.sensor.bbox
|
# homeassistant.components.sensor.bbox
|
||||||
|
Loading…
x
Reference in New Issue
Block a user