Update Emby component to async (#6664)

* Update Emby component to async

* Address comments

* Make SSL default

* Bump library

* Port based on SSL, use available property
This commit is contained in:
John Mihalic 2017-03-17 10:55:07 -04:00 committed by Pascal Vizeli
parent edf20f542a
commit 30d4c54187
2 changed files with 220 additions and 203 deletions

View File

@ -4,32 +4,37 @@ Support to interface with the Emby API.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.emby/ https://home-assistant.io/components/media_player.emby/
""" """
import asyncio
import logging import logging
from datetime import timedelta
import voluptuous as vol import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
SUPPORT_SEEK, SUPPORT_STOP, SUPPORT_PREVIOUS_TRACK, MediaPlayerDevice, SUPPORT_PAUSE, SUPPORT_SEEK, SUPPORT_STOP, SUPPORT_PREVIOUS_TRACK,
SUPPORT_PLAY, PLATFORM_SCHEMA) MediaPlayerDevice, SUPPORT_PLAY, PLATFORM_SCHEMA)
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_API_KEY, CONF_PORT, CONF_SSL, DEVICE_DEFAULT_NAME, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING,
STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, STATE_UNKNOWN) CONF_HOST, CONF_PORT, CONF_SSL, CONF_API_KEY, DEVICE_DEFAULT_NAME,
from homeassistant.helpers.event import (track_utc_time_change) EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
from homeassistant.util import Throttle from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
REQUIREMENTS = ['pyemby==0.2'] REQUIREMENTS = ['pyemby==1.1']
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) _LOGGER = logging.getLogger(__name__)
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
CONF_AUTO_HIDE = 'auto_hide'
MEDIA_TYPE_TRAILER = 'trailer' MEDIA_TYPE_TRAILER = 'trailer'
MEDIA_TYPE_GENERIC_VIDEO = 'video'
DEFAULT_HOST = 'localhost'
DEFAULT_PORT = 8096 DEFAULT_PORT = 8096
DEFAULT_SSL_PORT = 8920
DEFAULT_SSL = False
DEFAULT_AUTO_HIDE = False
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -37,219 +42,210 @@ SUPPORT_EMBY = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
SUPPORT_STOP | SUPPORT_SEEK | SUPPORT_PLAY SUPPORT_STOP | SUPPORT_SEEK | SUPPORT_PLAY
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_HOST, default='localhost'): cv.string, vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_SSL, default=False): cv.boolean, vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
vol.Required(CONF_API_KEY): cv.string, vol.Required(CONF_API_KEY): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_PORT, default=None): cv.port,
vol.Optional(CONF_AUTO_HIDE, default=DEFAULT_AUTO_HIDE): cv.boolean,
}) })
def setup_platform(hass, config, add_devices_callback, discovery_info=None): @asyncio.coroutine
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Setup the Emby platform.""" """Setup the Emby platform."""
from pyemby.emby import EmbyRemote from pyemby import EmbyServer
_host = config.get(CONF_HOST) host = config.get(CONF_HOST)
_key = config.get(CONF_API_KEY) key = config.get(CONF_API_KEY)
_port = config.get(CONF_PORT) port = config.get(CONF_PORT)
ssl = config.get(CONF_SSL)
auto_hide = config.get(CONF_AUTO_HIDE)
if config.get(CONF_SSL): if port is None:
_protocol = "https" port = DEFAULT_SSL_PORT if ssl else DEFAULT_PORT
else:
_protocol = "http"
_url = '{}://{}:{}'.format(_protocol, _host, _port) _LOGGER.debug('Setting up Emby server at: %s:%s', host, port)
_LOGGER.debug('Setting up Emby server at: %s', _url) emby = EmbyServer(host, key, port, ssl, hass.loop)
embyserver = EmbyRemote(_key, _url) active_emby_devices = {}
inactive_emby_devices = {}
emby_clients = {} @callback
emby_sessions = {} def device_update_callback(data):
track_utc_time_change(hass, lambda now: update_devices(), second=30) """Callback for when devices are added to emby."""
new_devices = []
active_devices = []
for dev_id in emby.devices:
active_devices.append(dev_id)
if dev_id not in active_emby_devices and \
dev_id not in inactive_emby_devices:
new = EmbyDevice(emby, dev_id)
active_emby_devices[dev_id] = new
new_devices.append(new)
@Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) elif dev_id in inactive_emby_devices:
def update_devices(): if emby.devices[dev_id].state != 'Off':
"""Update the devices objects.""" add = inactive_emby_devices.pop(dev_id)
devices = embyserver.get_sessions() active_emby_devices[dev_id] = add
if devices is None: _LOGGER.debug("Showing %s, item: %s", dev_id, add)
_LOGGER.error('Error listing Emby devices.') add.set_available(True)
return add.set_hidden(False)
new_emby_clients = [] if new_devices:
for device in devices: _LOGGER.debug("Adding new devices to HASS: %s", new_devices)
if device['DeviceId'] == embyserver.unique_id: async_add_devices(new_devices, update_before_add=True)
break
if device['DeviceId'] not in emby_clients: @callback
_LOGGER.debug('New Emby DeviceID: %s. Adding to Clients.', def device_removal_callback(data):
device['DeviceId']) """Callback for when devices are removed from emby."""
new_client = EmbyClient(embyserver, device, emby_sessions, if data in active_emby_devices:
update_devices, update_sessions) rem = active_emby_devices.pop(data)
emby_clients[device['DeviceId']] = new_client inactive_emby_devices[data] = rem
new_emby_clients.append(new_client) _LOGGER.debug("Inactive %s, item: %s", data, rem)
else: rem.set_available(False)
emby_clients[device['DeviceId']].set_device(device) if auto_hide:
rem.set_hidden(True)
if new_emby_clients: @callback
add_devices_callback(new_emby_clients) def start_emby(event):
"""Start emby connection."""
emby.start()
@Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) @asyncio.coroutine
def update_sessions(): def stop_emby(event):
"""Update the sessions objects.""" """Stop emby connection."""
sessions = embyserver.get_sessions() yield from emby.stop()
if sessions is None:
_LOGGER.error('Error listing Emby sessions')
return
emby_sessions.clear() emby.add_new_devices_callback(device_update_callback)
for session in sessions: emby.add_stale_devices_callback(device_removal_callback)
emby_sessions[session['DeviceId']] = session
update_devices() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_emby)
update_sessions() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_emby)
class EmbyClient(MediaPlayerDevice): class EmbyDevice(MediaPlayerDevice):
"""Representation of a Emby device.""" """Representation of an Emby device."""
# pylint: disable=too-many-arguments, too-many-public-methods, def __init__(self, emby, device_id):
def __init__(self, client, device, emby_sessions, update_devices,
update_sessions):
"""Initialize the Emby device.""" """Initialize the Emby device."""
self.emby_sessions = emby_sessions _LOGGER.debug('New Emby Device initialized with ID: %s', device_id)
self.update_devices = update_devices self.emby = emby
self.update_sessions = update_sessions self.device_id = device_id
self.client = client self.device = self.emby.devices[self.device_id]
self.set_device(device)
self._hidden = False
self._available = True
self.media_status_last_position = None self.media_status_last_position = None
self.media_status_received = None self.media_status_received = None
def set_device(self, device): @asyncio.coroutine
"""Set the device property.""" def async_added_to_hass(self):
self.device = device """Register callback."""
self.emby.add_update_callback(self.async_update_callback,
self.device_id)
@callback
def async_update_callback(self, msg):
"""Callback for device updates."""
# Check if we should update progress
if self.device.media_position:
if self.device.media_position != self.media_status_last_position:
self.media_status_last_position = self.device.media_position
self.media_status_received = dt_util.utcnow()
elif not self.device.is_nowplaying:
# No position, but we have an old value and are still playing
self.media_status_last_position = None
self.media_status_received = None
self.hass.async_add_job(self.async_update_ha_state())
@property
def hidden(self):
"""Return True if entity should be hidden from UI."""
return self._hidden
def set_hidden(self, value):
"""Set hidden property."""
self._hidden = value
@property
def available(self):
"""Return True if entity is available."""
return self._available
def set_available(self, value):
"""Set available property."""
self._available = value
@property @property
def unique_id(self): def unique_id(self):
"""Return the id of this emby client.""" """Return the id of this emby client."""
return '{}.{}'.format( return '{}.{}'.format(self.__class__, self.device_id)
self.__class__, self.device['DeviceId'])
@property @property
def supports_remote_control(self): def supports_remote_control(self):
"""Return control ability.""" """Return control ability."""
return self.device['SupportsRemoteControl'] return self.device.supports_remote_control
@property @property
def name(self): def name(self):
"""Return the name of the device.""" """Return the name of the device."""
return 'emby_{}'.format(self.device['DeviceName']) or \ return 'Emby - {} - {}'.format(self.device.client, self.device.name) \
DEVICE_DEFAULT_NAME or DEVICE_DEFAULT_NAME
@property @property
def session(self): def should_poll(self):
"""Return the session, if any.""" """Return True if entity has to be polled for state."""
if self.device['DeviceId'] not in self.emby_sessions: return False
return None
return self.emby_sessions[self.device['DeviceId']]
@property
def now_playing_item(self):
"""Return the currently playing item, if any."""
session = self.session
if session is not None and 'NowPlayingItem' in session:
return session['NowPlayingItem']
@property @property
def state(self): def state(self):
"""Return the state of the device.""" """Return the state of the device."""
session = self.session state = self.device.state
if session: if state == 'Paused':
if 'NowPlayingItem' in session: return STATE_PAUSED
if session['PlayState']['IsPaused']: elif state == 'Playing':
return STATE_PAUSED return STATE_PLAYING
else: elif state == 'Idle':
return STATE_PLAYING return STATE_IDLE
else: elif state == 'Off':
return STATE_IDLE
# This is nasty. Need to find a way to determine alive
else:
return STATE_OFF return STATE_OFF
return STATE_UNKNOWN
def update(self):
"""Get the latest details."""
self.update_devices(no_throttle=True)
self.update_sessions(no_throttle=True)
# Check if we should update progress
try:
position = self.session['PlayState']['PositionTicks']
except (KeyError, TypeError):
self.media_status_last_position = None
self.media_status_received = None
else:
position = int(position) / 10000000
if position != self.media_status_last_position:
self.media_status_last_position = position
self.media_status_received = dt_util.utcnow()
def play_percent(self):
"""Return current media percent complete."""
if self.now_playing_item['RunTimeTicks'] and \
self.session['PlayState']['PositionTicks']:
try:
return int(self.session['PlayState']['PositionTicks']) / \
int(self.now_playing_item['RunTimeTicks']) * 100
except KeyError:
return 0
else:
return 0
@property @property
def app_name(self): def app_name(self):
"""Return current user as app_name.""" """Return current user as app_name."""
# Ideally the media_player object would have a user property. # Ideally the media_player object would have a user property.
try: return self.device.username
return self.device['UserName']
except KeyError:
return None
@property @property
def media_content_id(self): def media_content_id(self):
"""Content ID of current playing media.""" """Content ID of current playing media."""
if self.now_playing_item is not None: return self.device.media_id
try:
return self.now_playing_item['Id']
except KeyError:
return None
@property @property
def media_content_type(self): def media_content_type(self):
"""Content type of current playing media.""" """Content type of current playing media."""
if self.now_playing_item is None: media_type = self.device.media_type
return None if media_type == 'Episode':
try: return MEDIA_TYPE_TVSHOW
media_type = self.now_playing_item['Type'] elif media_type == 'Movie':
if media_type == 'Episode': return MEDIA_TYPE_VIDEO
return MEDIA_TYPE_TVSHOW elif media_type == 'Trailer':
elif media_type == 'Movie': return MEDIA_TYPE_TRAILER
return MEDIA_TYPE_VIDEO elif media_type == 'Music':
elif media_type == 'Trailer': return MEDIA_TYPE_MUSIC
return MEDIA_TYPE_TRAILER elif media_type == 'Video':
return None return MEDIA_TYPE_GENERIC_VIDEO
except KeyError: elif media_type == 'Audio':
return None return MEDIA_TYPE_MUSIC
return None
@property @property
def media_duration(self): def media_duration(self):
"""Duration of current playing media in seconds.""" """Duration of current playing media in seconds."""
if self.now_playing_item and self.media_content_type: return self.device.media_runtime
try:
return int(self.now_playing_item['RunTimeTicks']) / 10000000
except KeyError:
return None
@property @property
def media_position(self): def media_position(self):
@ -268,45 +264,42 @@ class EmbyClient(MediaPlayerDevice):
@property @property
def media_image_url(self): def media_image_url(self):
"""Image url of current playing media.""" """Image url of current playing media."""
if self.now_playing_item is not None: return self.device.media_image_url
try:
return self.client.get_image(
self.now_playing_item['ThumbItemId'], 'Thumb', 0)
except KeyError:
try:
return self.client.get_image(
self.now_playing_item[
'PrimaryImageItemId'], 'Primary', 0)
except KeyError:
return None
@property @property
def media_title(self): def media_title(self):
"""Title of current playing media.""" """Title of current playing media."""
# find a string we can use as a title return self.device.media_title
if self.now_playing_item is not None:
return self.now_playing_item['Name']
@property @property
def media_season(self): def media_season(self):
"""Season of curent playing media (TV Show only).""" """Season of curent playing media (TV Show only)."""
if self.now_playing_item is not None and \ return self.device.media_season
'ParentIndexNumber' in self.now_playing_item:
return self.now_playing_item['ParentIndexNumber']
@property @property
def media_series_title(self): def media_series_title(self):
"""The title of the series of current playing media (TV Show only).""" """The title of the series of current playing media (TV Show only)."""
if self.now_playing_item is not None and \ return self.device.media_series_title
'SeriesName' in self.now_playing_item:
return self.now_playing_item['SeriesName']
@property @property
def media_episode(self): def media_episode(self):
"""Episode of current playing media (TV Show only).""" """Episode of current playing media (TV Show only)."""
if self.now_playing_item is not None and \ return self.device.media_episode
'IndexNumber' in self.now_playing_item:
return self.now_playing_item['IndexNumber'] @property
def media_album_name(self):
"""Album name of current playing media (Music track only)."""
return self.device.media_album_name
@property
def media_artist(self):
"""Artist of current playing media (Music track only)."""
return self.device.media_artist
@property
def media_album_artist(self):
"""Album artist of current playing media (Music track only)."""
return self.device.media_album_artist
@property @property
def supported_features(self): def supported_features(self):
@ -316,20 +309,44 @@ class EmbyClient(MediaPlayerDevice):
else: else:
return None return None
def media_play(self): def async_media_play(self):
"""Send play command.""" """Play media.
if self.supports_remote_control:
self.client.play(self.session)
def media_pause(self): This method must be run in the event loop and returns a coroutine.
"""Send pause command.""" """
if self.supports_remote_control: return self.device.media_play()
self.client.pause(self.session)
def media_next_track(self): def async_media_pause(self):
"""Send next track command.""" """Pause the media player.
self.client.next_track(self.session)
def media_previous_track(self): This method must be run in the event loop and returns a coroutine.
"""Send previous track command.""" """
self.client.previous_track(self.session) return self.device.media_pause()
def async_media_stop(self):
"""Stop the media player.
This method must be run in the event loop and returns a coroutine.
"""
return self.device.media_stop()
def async_media_next_track(self):
"""Send next track command.
This method must be run in the event loop and returns a coroutine.
"""
return self.device.media_next()
def async_media_previous_track(self):
"""Send next track command.
This method must be run in the event loop and returns a coroutine.
"""
return self.device.media_previous()
def async_media_seek(self, position):
"""Send seek command.
This method must be run in the event loop and returns a coroutine.
"""
return self.device.media_seek(position)

View File

@ -487,7 +487,7 @@ pydroid-ipcam==0.6
pyebox==0.1.0 pyebox==0.1.0
# homeassistant.components.media_player.emby # homeassistant.components.media_player.emby
pyemby==0.2 pyemby==1.1
# homeassistant.components.envisalink # homeassistant.components.envisalink
pyenvisalink==2.0 pyenvisalink==2.0