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:
parent
edf20f542a
commit
30d4c54187
2 changed files with 220 additions and 203 deletions
|
@ -4,32 +4,37 @@ Support to interface with the Emby API.
|
|||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/media_player.emby/
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.components.media_player import (
|
||||
MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE,
|
||||
SUPPORT_SEEK, SUPPORT_STOP, SUPPORT_PREVIOUS_TRACK, MediaPlayerDevice,
|
||||
SUPPORT_PLAY, PLATFORM_SCHEMA)
|
||||
MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO, MEDIA_TYPE_MUSIC, SUPPORT_NEXT_TRACK,
|
||||
SUPPORT_PAUSE, SUPPORT_SEEK, SUPPORT_STOP, SUPPORT_PREVIOUS_TRACK,
|
||||
MediaPlayerDevice, SUPPORT_PLAY, PLATFORM_SCHEMA)
|
||||
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_UNKNOWN)
|
||||
from homeassistant.helpers.event import (track_utc_time_change)
|
||||
from homeassistant.util import Throttle
|
||||
STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING,
|
||||
CONF_HOST, CONF_PORT, CONF_SSL, CONF_API_KEY, DEVICE_DEFAULT_NAME,
|
||||
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
|
||||
from homeassistant.core import callback
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
REQUIREMENTS = ['pyemby==0.2']
|
||||
REQUIREMENTS = ['pyemby==1.1']
|
||||
|
||||
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
|
||||
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_AUTO_HIDE = 'auto_hide'
|
||||
|
||||
MEDIA_TYPE_TRAILER = 'trailer'
|
||||
MEDIA_TYPE_GENERIC_VIDEO = 'video'
|
||||
|
||||
DEFAULT_HOST = 'localhost'
|
||||
DEFAULT_PORT = 8096
|
||||
DEFAULT_SSL_PORT = 8920
|
||||
DEFAULT_SSL = False
|
||||
DEFAULT_AUTO_HIDE = False
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -37,219 +42,210 @@ SUPPORT_EMBY = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK | \
|
|||
SUPPORT_STOP | SUPPORT_SEEK | SUPPORT_PLAY
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Optional(CONF_HOST, default='localhost'): cv.string,
|
||||
vol.Optional(CONF_SSL, default=False): cv.boolean,
|
||||
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
|
||||
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."""
|
||||
from pyemby.emby import EmbyRemote
|
||||
from pyemby import EmbyServer
|
||||
|
||||
_host = config.get(CONF_HOST)
|
||||
_key = config.get(CONF_API_KEY)
|
||||
_port = config.get(CONF_PORT)
|
||||
host = config.get(CONF_HOST)
|
||||
key = config.get(CONF_API_KEY)
|
||||
port = config.get(CONF_PORT)
|
||||
ssl = config.get(CONF_SSL)
|
||||
auto_hide = config.get(CONF_AUTO_HIDE)
|
||||
|
||||
if config.get(CONF_SSL):
|
||||
_protocol = "https"
|
||||
else:
|
||||
_protocol = "http"
|
||||
if port is None:
|
||||
port = DEFAULT_SSL_PORT if ssl else DEFAULT_PORT
|
||||
|
||||
_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 = {}
|
||||
emby_sessions = {}
|
||||
track_utc_time_change(hass, lambda now: update_devices(), second=30)
|
||||
@callback
|
||||
def device_update_callback(data):
|
||||
"""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)
|
||||
def update_devices():
|
||||
"""Update the devices objects."""
|
||||
devices = embyserver.get_sessions()
|
||||
if devices is None:
|
||||
_LOGGER.error('Error listing Emby devices.')
|
||||
return
|
||||
elif dev_id in inactive_emby_devices:
|
||||
if emby.devices[dev_id].state != 'Off':
|
||||
add = inactive_emby_devices.pop(dev_id)
|
||||
active_emby_devices[dev_id] = add
|
||||
_LOGGER.debug("Showing %s, item: %s", dev_id, add)
|
||||
add.set_available(True)
|
||||
add.set_hidden(False)
|
||||
|
||||
new_emby_clients = []
|
||||
for device in devices:
|
||||
if device['DeviceId'] == embyserver.unique_id:
|
||||
break
|
||||
if new_devices:
|
||||
_LOGGER.debug("Adding new devices to HASS: %s", new_devices)
|
||||
async_add_devices(new_devices, update_before_add=True)
|
||||
|
||||
if device['DeviceId'] not in emby_clients:
|
||||
_LOGGER.debug('New Emby DeviceID: %s. Adding to Clients.',
|
||||
device['DeviceId'])
|
||||
new_client = EmbyClient(embyserver, device, emby_sessions,
|
||||
update_devices, update_sessions)
|
||||
emby_clients[device['DeviceId']] = new_client
|
||||
new_emby_clients.append(new_client)
|
||||
else:
|
||||
emby_clients[device['DeviceId']].set_device(device)
|
||||
@callback
|
||||
def device_removal_callback(data):
|
||||
"""Callback for when devices are removed from emby."""
|
||||
if data in active_emby_devices:
|
||||
rem = active_emby_devices.pop(data)
|
||||
inactive_emby_devices[data] = rem
|
||||
_LOGGER.debug("Inactive %s, item: %s", data, rem)
|
||||
rem.set_available(False)
|
||||
if auto_hide:
|
||||
rem.set_hidden(True)
|
||||
|
||||
if new_emby_clients:
|
||||
add_devices_callback(new_emby_clients)
|
||||
@callback
|
||||
def start_emby(event):
|
||||
"""Start emby connection."""
|
||||
emby.start()
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
|
||||
def update_sessions():
|
||||
"""Update the sessions objects."""
|
||||
sessions = embyserver.get_sessions()
|
||||
if sessions is None:
|
||||
_LOGGER.error('Error listing Emby sessions')
|
||||
return
|
||||
@asyncio.coroutine
|
||||
def stop_emby(event):
|
||||
"""Stop emby connection."""
|
||||
yield from emby.stop()
|
||||
|
||||
emby_sessions.clear()
|
||||
for session in sessions:
|
||||
emby_sessions[session['DeviceId']] = session
|
||||
emby.add_new_devices_callback(device_update_callback)
|
||||
emby.add_stale_devices_callback(device_removal_callback)
|
||||
|
||||
update_devices()
|
||||
update_sessions()
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_emby)
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_emby)
|
||||
|
||||
|
||||
class EmbyClient(MediaPlayerDevice):
|
||||
"""Representation of a Emby device."""
|
||||
class EmbyDevice(MediaPlayerDevice):
|
||||
"""Representation of an Emby device."""
|
||||
|
||||
# pylint: disable=too-many-arguments, too-many-public-methods,
|
||||
|
||||
def __init__(self, client, device, emby_sessions, update_devices,
|
||||
update_sessions):
|
||||
def __init__(self, emby, device_id):
|
||||
"""Initialize the Emby device."""
|
||||
self.emby_sessions = emby_sessions
|
||||
self.update_devices = update_devices
|
||||
self.update_sessions = update_sessions
|
||||
self.client = client
|
||||
self.set_device(device)
|
||||
_LOGGER.debug('New Emby Device initialized with ID: %s', device_id)
|
||||
self.emby = emby
|
||||
self.device_id = device_id
|
||||
self.device = self.emby.devices[self.device_id]
|
||||
|
||||
self._hidden = False
|
||||
self._available = True
|
||||
|
||||
self.media_status_last_position = None
|
||||
self.media_status_received = None
|
||||
|
||||
def set_device(self, device):
|
||||
"""Set the device property."""
|
||||
self.device = device
|
||||
@asyncio.coroutine
|
||||
def async_added_to_hass(self):
|
||||
"""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
|
||||
def unique_id(self):
|
||||
"""Return the id of this emby client."""
|
||||
return '{}.{}'.format(
|
||||
self.__class__, self.device['DeviceId'])
|
||||
return '{}.{}'.format(self.__class__, self.device_id)
|
||||
|
||||
@property
|
||||
def supports_remote_control(self):
|
||||
"""Return control ability."""
|
||||
return self.device['SupportsRemoteControl']
|
||||
return self.device.supports_remote_control
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
return 'emby_{}'.format(self.device['DeviceName']) or \
|
||||
DEVICE_DEFAULT_NAME
|
||||
return 'Emby - {} - {}'.format(self.device.client, self.device.name) \
|
||||
or DEVICE_DEFAULT_NAME
|
||||
|
||||
@property
|
||||
def session(self):
|
||||
"""Return the session, if any."""
|
||||
if self.device['DeviceId'] not in self.emby_sessions:
|
||||
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']
|
||||
def should_poll(self):
|
||||
"""Return True if entity has to be polled for state."""
|
||||
return False
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the device."""
|
||||
session = self.session
|
||||
if session:
|
||||
if 'NowPlayingItem' in session:
|
||||
if session['PlayState']['IsPaused']:
|
||||
return STATE_PAUSED
|
||||
else:
|
||||
return STATE_PLAYING
|
||||
else:
|
||||
return STATE_IDLE
|
||||
# This is nasty. Need to find a way to determine alive
|
||||
else:
|
||||
state = self.device.state
|
||||
if state == 'Paused':
|
||||
return STATE_PAUSED
|
||||
elif state == 'Playing':
|
||||
return STATE_PLAYING
|
||||
elif state == 'Idle':
|
||||
return STATE_IDLE
|
||||
elif 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
|
||||
def app_name(self):
|
||||
"""Return current user as app_name."""
|
||||
# Ideally the media_player object would have a user property.
|
||||
try:
|
||||
return self.device['UserName']
|
||||
except KeyError:
|
||||
return None
|
||||
return self.device.username
|
||||
|
||||
@property
|
||||
def media_content_id(self):
|
||||
"""Content ID of current playing media."""
|
||||
if self.now_playing_item is not None:
|
||||
try:
|
||||
return self.now_playing_item['Id']
|
||||
except KeyError:
|
||||
return None
|
||||
return self.device.media_id
|
||||
|
||||
@property
|
||||
def media_content_type(self):
|
||||
"""Content type of current playing media."""
|
||||
if self.now_playing_item is None:
|
||||
return None
|
||||
try:
|
||||
media_type = self.now_playing_item['Type']
|
||||
if media_type == 'Episode':
|
||||
return MEDIA_TYPE_TVSHOW
|
||||
elif media_type == 'Movie':
|
||||
return MEDIA_TYPE_VIDEO
|
||||
elif media_type == 'Trailer':
|
||||
return MEDIA_TYPE_TRAILER
|
||||
return None
|
||||
except KeyError:
|
||||
return None
|
||||
media_type = self.device.media_type
|
||||
if media_type == 'Episode':
|
||||
return MEDIA_TYPE_TVSHOW
|
||||
elif media_type == 'Movie':
|
||||
return MEDIA_TYPE_VIDEO
|
||||
elif media_type == 'Trailer':
|
||||
return MEDIA_TYPE_TRAILER
|
||||
elif media_type == 'Music':
|
||||
return MEDIA_TYPE_MUSIC
|
||||
elif media_type == 'Video':
|
||||
return MEDIA_TYPE_GENERIC_VIDEO
|
||||
elif media_type == 'Audio':
|
||||
return MEDIA_TYPE_MUSIC
|
||||
return None
|
||||
|
||||
@property
|
||||
def media_duration(self):
|
||||
"""Duration of current playing media in seconds."""
|
||||
if self.now_playing_item and self.media_content_type:
|
||||
try:
|
||||
return int(self.now_playing_item['RunTimeTicks']) / 10000000
|
||||
except KeyError:
|
||||
return None
|
||||
return self.device.media_runtime
|
||||
|
||||
@property
|
||||
def media_position(self):
|
||||
|
@ -268,45 +264,42 @@ class EmbyClient(MediaPlayerDevice):
|
|||
@property
|
||||
def media_image_url(self):
|
||||
"""Image url of current playing media."""
|
||||
if self.now_playing_item is not None:
|
||||
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
|
||||
return self.device.media_image_url
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
"""Title of current playing media."""
|
||||
# find a string we can use as a title
|
||||
if self.now_playing_item is not None:
|
||||
return self.now_playing_item['Name']
|
||||
return self.device.media_title
|
||||
|
||||
@property
|
||||
def media_season(self):
|
||||
"""Season of curent playing media (TV Show only)."""
|
||||
if self.now_playing_item is not None and \
|
||||
'ParentIndexNumber' in self.now_playing_item:
|
||||
return self.now_playing_item['ParentIndexNumber']
|
||||
return self.device.media_season
|
||||
|
||||
@property
|
||||
def media_series_title(self):
|
||||
"""The title of the series of current playing media (TV Show only)."""
|
||||
if self.now_playing_item is not None and \
|
||||
'SeriesName' in self.now_playing_item:
|
||||
return self.now_playing_item['SeriesName']
|
||||
return self.device.media_series_title
|
||||
|
||||
@property
|
||||
def media_episode(self):
|
||||
"""Episode of current playing media (TV Show only)."""
|
||||
if self.now_playing_item is not None and \
|
||||
'IndexNumber' in self.now_playing_item:
|
||||
return self.now_playing_item['IndexNumber']
|
||||
return self.device.media_episode
|
||||
|
||||
@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
|
||||
def supported_features(self):
|
||||
|
@ -316,20 +309,44 @@ class EmbyClient(MediaPlayerDevice):
|
|||
else:
|
||||
return None
|
||||
|
||||
def media_play(self):
|
||||
"""Send play command."""
|
||||
if self.supports_remote_control:
|
||||
self.client.play(self.session)
|
||||
def async_media_play(self):
|
||||
"""Play media.
|
||||
|
||||
def media_pause(self):
|
||||
"""Send pause command."""
|
||||
if self.supports_remote_control:
|
||||
self.client.pause(self.session)
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
return self.device.media_play()
|
||||
|
||||
def media_next_track(self):
|
||||
"""Send next track command."""
|
||||
self.client.next_track(self.session)
|
||||
def async_media_pause(self):
|
||||
"""Pause the media player.
|
||||
|
||||
def media_previous_track(self):
|
||||
"""Send previous track command."""
|
||||
self.client.previous_track(self.session)
|
||||
This method must be run in the event loop and returns a coroutine.
|
||||
"""
|
||||
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)
|
||||
|
|
|
@ -487,7 +487,7 @@ pydroid-ipcam==0.6
|
|||
pyebox==0.1.0
|
||||
|
||||
# homeassistant.components.media_player.emby
|
||||
pyemby==0.2
|
||||
pyemby==1.1
|
||||
|
||||
# homeassistant.components.envisalink
|
||||
pyenvisalink==2.0
|
||||
|
|
Loading…
Add table
Reference in a new issue