From 55c2bd5eb48939bbba109fe08e90c1e95c4ef8ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Thu, 23 Feb 2017 16:29:08 +0100 Subject: [PATCH 1/2] Use push updates in Apple TV --- .../components/media_player/apple_tv.py | 87 ++++++++++++------- requirements_all.txt | 2 +- 2 files changed, 58 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/media_player/apple_tv.py b/homeassistant/components/media_player/apple_tv.py index 566ad7d69335e9..699220a61bcc2f 100644 --- a/homeassistant/components/media_player/apple_tv.py +++ b/homeassistant/components/media_player/apple_tv.py @@ -7,8 +7,8 @@ import asyncio import logging import hashlib +from concurrent.futures import _base -import aiohttp import voluptuous as vol from homeassistant.core import callback @@ -19,13 +19,13 @@ MEDIA_TYPE_VIDEO, MEDIA_TYPE_TVSHOW) from homeassistant.const import ( STATE_IDLE, STATE_PAUSED, STATE_PLAYING, STATE_STANDBY, CONF_HOST, - STATE_OFF, CONF_NAME) + 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.1.4'] +REQUIREMENTS = ['pyatv==0.2.1'] _LOGGER = logging.getLogger(__name__) @@ -71,33 +71,52 @@ def async_setup_platform(hass, config, async_add_entities, 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) + entity = AppleTvDevice(hass, atv, name, start_off) - yield from async_add_entities([entity], update_before_add=True) + def on_hass_stop(event): + """Stop push updates when hass stops.""" + atv.push_updater.stop() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) + + yield from async_add_entities([entity]) class AppleTvDevice(MediaPlayerDevice): """Representation of an Apple TV device.""" - def __init__(self, atv, name, is_off): + def __init__(self, hass, atv, name, is_off): """Initialize the Apple TV device.""" + self._hass = hass 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._atv.push_updater.start() @callback def _set_power_off(self, is_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 def name(self): """Return the name of the device.""" return self._name + @property + def should_poll(self): + """No polling needed.""" + return False + @property def state(self): """Return the state of the device.""" @@ -120,29 +139,18 @@ def state(self): else: return STATE_STANDBY # Bad or unknown state? - @asyncio.coroutine - def async_update(self): - """Retrieve latest state.""" - if self._is_off: - return - - from pyatv import exceptions - try: - playing = yield from self._atv.metadata.playing() - - if 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._playing = playing - except exceptions.AuthenticationError as ex: - _LOGGER.warning('%s (bad login id?)', str(ex)) - except aiohttp.errors.ClientOSError as ex: - _LOGGER.error('failed to connect to Apple TV (%s)', str(ex)) - except asyncio.TimeoutError: - _LOGGER.warning('timed out while connecting to Apple TV') + def playstatus_update(self, updater, playing): + """Print what is currently playing when it changes.""" + 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._playing = playing + self._hass.async_add_job(self.async_update_ha_state()) def _has_playing_media_changed(self, new_playing): if self._playing is None: @@ -151,6 +159,22 @@ def _has_playing_media_changed(self, new_playing): return new_playing.media_type != old_playing.media_type or \ new_playing.title != old_playing.title + def playstatus_error(self, updater, exception): + """Inform about an error and restart push updates.""" + if isinstance(exception, _base.TimeoutError): + _LOGGER.warning( + 'Timed out while connecting to device, trying again') + else: + _LOGGER.warning('A %s error occurred: %s', + exception.__class__, exception) + + # This will wait 10 seconds before restarting push updates. If the + # connection continues to fail, it will flood the log (every 10 + # seconds) until it succeeds. A better approach should probably be + # implemented here later. + updater.start(initial_delay=10) + self._hass.async_add_job(self.async_update_ha_state()) + @property def media_content_type(self): """Content type of current playing media.""" @@ -191,7 +215,8 @@ def async_play_media(self, media_type, media_id, **kwargs): @property def media_image_hash(self): """Hash value for media image.""" - return self._artwork_hash + if self.state != STATE_IDLE: + return self._artwork_hash @asyncio.coroutine def async_get_media_image(self): @@ -207,6 +232,8 @@ def media_title(self): title = self._playing.title return title if title else "No title" + return 'Not connected to Apple TV' + @property def supported_features(self): """Flag media player features that are supported.""" diff --git a/requirements_all.txt b/requirements_all.txt index a66163b40d165f..9e620289835d37 100755 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -454,7 +454,7 @@ pyasn1-modules==0.0.8 pyasn1==0.2.2 # homeassistant.components.media_player.apple_tv -pyatv==0.1.4 +pyatv==0.2.1 # homeassistant.components.device_tracker.bbox # homeassistant.components.sensor.bbox From 1947ee153d59657abe9c9d45415592cc9c7fa976 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20St=C3=A5hl?= Date: Thu, 2 Mar 2017 07:37:10 +0100 Subject: [PATCH 2/2] Fix review comments --- .../components/media_player/apple_tv.py | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/media_player/apple_tv.py b/homeassistant/components/media_player/apple_tv.py index 699220a61bcc2f..ad0adfb008a0ec 100644 --- a/homeassistant/components/media_player/apple_tv.py +++ b/homeassistant/components/media_player/apple_tv.py @@ -7,7 +7,6 @@ import asyncio import logging import hashlib -from concurrent.futures import _base import voluptuous as vol @@ -71,8 +70,9 @@ def async_setup_platform(hass, config, async_add_entities, 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(hass, atv, name, start_off) + entity = AppleTvDevice(atv, name, start_off) + @callback def on_hass_stop(event): """Stop push updates when hass stops.""" atv.push_updater.stop() @@ -85,15 +85,18 @@ def on_hass_stop(event): class AppleTvDevice(MediaPlayerDevice): """Representation of an Apple TV device.""" - def __init__(self, hass, atv, name, is_off): + def __init__(self, atv, name, is_off): """Initialize the Apple TV device.""" - self._hass = hass self._atv = atv self._name = name self._is_off = is_off self._playing = None self._artwork_hash = None self._atv.push_updater.listener = self + + @asyncio.coroutine + def async_added_to_hass(self): + """Called when entity is about to be added to HASS.""" self._atv.push_updater.start() @callback @@ -105,7 +108,7 @@ def _set_power_off(self, is_off): self._atv.push_updater.stop() else: self._atv.push_updater.start() - self._hass.async_add_job(self.async_update_ha_state()) + self.hass.async_add_job(self.async_update_ha_state()) @property def name(self): @@ -139,6 +142,7 @@ def state(self): else: return STATE_STANDBY # Bad or unknown state? + @callback def playstatus_update(self, updater, playing): """Print what is currently playing when it changes.""" if self.state == STATE_IDLE: @@ -150,7 +154,7 @@ def playstatus_update(self, updater, playing): base.encode('utf-8')).hexdigest() self._playing = playing - 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: @@ -159,21 +163,20 @@ def _has_playing_media_changed(self, new_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.""" - if isinstance(exception, _base.TimeoutError): - _LOGGER.warning( - 'Timed out while connecting to device, trying again') - else: - _LOGGER.warning('A %s error occurred: %s', - exception.__class__, exception) + _LOGGER.warning('A %s error occurred: %s', + exception.__class__, exception) # This will wait 10 seconds before restarting push updates. If the # connection continues to fail, it will flood the log (every 10 # seconds) until it succeeds. A better approach should probably be # implemented here later. updater.start(initial_delay=10) - self._hass.async_add_job(self.async_update_ha_state()) + self._playing = None + self._artwork_hash = None + self.hass.async_add_job(self.async_update_ha_state()) @property def media_content_type(self):