Merge pull request #551 from balloob/mp_plex_discovery

Media_player/plex discovery
This commit is contained in:
Paulus Schoutsen 2015-10-25 15:38:42 -07:00
commit 004bad7f00
5 changed files with 149 additions and 38 deletions

View File

@ -28,6 +28,7 @@ SERVICE_HUE = 'philips_hue'
SERVICE_CAST = 'google_cast'
SERVICE_NETGEAR = 'netgear_router'
SERVICE_SONOS = 'sonos'
SERVICE_PLEX = 'plex_mediaserver'
SERVICE_HANDLERS = {
SERVICE_WEMO: "switch",
@ -35,6 +36,7 @@ SERVICE_HANDLERS = {
SERVICE_HUE: "light",
SERVICE_NETGEAR: 'device_tracker',
SERVICE_SONOS: 'media_player',
SERVICE_PLEX: 'media_player',
}
@ -88,6 +90,7 @@ def setup(hass, config):
ATTR_DISCOVERED: info
})
# pylint: disable=unused-argument
def start_discovery(event):
""" Start discovering. """
netdisco = DiscoveryService(SCAN_INTERVAL)

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -28,6 +28,7 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}'
DISCOVERY_PLATFORMS = {
discovery.SERVICE_CAST: 'cast',
discovery.SERVICE_SONOS: 'sonos',
discovery.SERVICE_PLEX: 'plex',
}
SERVICE_YOUTUBE_VIDEO = 'play_youtube_video'

View File

@ -6,38 +6,114 @@ Provides an interface to the Plex API.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.plex.html
"""
import os
import json
import logging
from datetime import timedelta
from urllib.parse import urlparse
from homeassistant.loader import get_component
import homeassistant.util as util
from homeassistant.components.media_player import (
MediaPlayerDevice, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK,
SUPPORT_NEXT_TRACK, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO)
from homeassistant.const import (
STATE_IDLE, STATE_PLAYING, STATE_PAUSED, STATE_OFF, STATE_UNKNOWN)
import homeassistant.util as util
DEVICE_DEFAULT_NAME, STATE_IDLE, STATE_PLAYING,
STATE_PAUSED, STATE_OFF, STATE_UNKNOWN)
REQUIREMENTS = ['https://github.com/adrienbrault/python-plexapi/archive/'
'df2d0847e801d6d5cda920326d693cf75f304f1a.zip'
'#python-plexapi==1.0.2']
REQUIREMENTS = ['plexapi==1.1.0']
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1)
PLEX_CONFIG_FILE = 'plex.conf'
# Map ip to request id for configuring
_CONFIGURING = {}
_LOGGER = logging.getLogger(__name__)
SUPPORT_PLEX = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
# pylint: disable=abstract-method, unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
""" Sets up the plex platform. """
from plexapi.myplex import MyPlexUser
from plexapi.exceptions import BadRequest
def config_from_file(filename, config=None):
''' Small configuration file management function'''
if config:
# We're writing configuration
try:
with open(filename, 'w') as fdesc:
fdesc.write(json.dumps(config))
except IOError as error:
_LOGGER.error('Saving config file failed: %s', error)
return False
return True
else:
# We're reading config
if os.path.isfile(filename):
try:
with open(filename, 'r') as fdesc:
return json.loads(fdesc.read())
except IOError as error:
_LOGGER.error('Reading config file failed: %s', error)
# This won't work yet
return False
else:
return {}
# pylint: disable=abstract-method, unused-argument
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
""" Sets up the plex platform. """
config = config_from_file(hass.config.path(PLEX_CONFIG_FILE))
if len(config):
# Setup a configured PlexServer
host, token = config.popitem()
token = token['token']
# Via discovery
elif discovery_info is not None:
# Parse discovery data
host = urlparse(discovery_info[1]).netloc
_LOGGER.info('Discovered PLEX server: %s', host)
if host in _CONFIGURING:
return
token = None
else:
return
setup_plexserver(host, token, hass, add_devices_callback)
# pylint: disable=too-many-branches
def setup_plexserver(host, token, hass, add_devices_callback):
''' Setup a plexserver based on host parameter'''
import plexapi.server
import plexapi.exceptions
try:
plexserver = plexapi.server.PlexServer('http://%s' % host, token)
except (plexapi.exceptions.BadRequest,
plexapi.exceptions.Unauthorized,
plexapi.exceptions.NotFound) as error:
_LOGGER.info(error)
# No token or wrong token
request_configuration(host, hass, add_devices_callback)
return
# If we came here and configuring this host, mark as done
if host in _CONFIGURING:
request_id = _CONFIGURING.pop(host)
configurator = get_component('configurator')
configurator.request_done(request_id)
_LOGGER.info('Discovery configuration done!')
# Save config
if not config_from_file(
hass.config.path(PLEX_CONFIG_FILE),
{host: {'token': token}}):
_LOGGER.error('failed to save config file')
_LOGGER.info('Connected to: htts://%s', host)
name = config.get('name', '')
user = config.get('user', '')
password = config.get('password', '')
plexuser = MyPlexUser.signin(user, password)
plexserver = plexuser.getResource(name).connect()
plex_clients = {}
plex_sessions = {}
@ -45,34 +121,34 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
def update_devices():
""" Updates the devices objects. """
try:
devices = plexuser.devices()
except BadRequest:
devices = plexserver.clients()
except plexapi.exceptions.BadRequest:
_LOGGER.exception("Error listing plex devices")
return
new_plex_clients = []
for device in devices:
if (all(x not in ['client', 'player'] for x in device.provides)
or 'PlexAPI' == device.product):
# For now, let's allow all deviceClass types
if device.deviceClass in ['badClient']:
continue
if device.clientIdentifier not in plex_clients:
if device.machineIdentifier not in plex_clients:
new_client = PlexClient(device, plex_sessions, update_devices,
update_sessions)
plex_clients[device.clientIdentifier] = new_client
plex_clients[device.machineIdentifier] = new_client
new_plex_clients.append(new_client)
else:
plex_clients[device.clientIdentifier].set_device(device)
plex_clients[device.machineIdentifier].set_device(device)
if new_plex_clients:
add_devices(new_plex_clients)
add_devices_callback(new_plex_clients)
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
def update_sessions():
""" Updates the sessions objects. """
try:
sessions = plexserver.sessions()
except BadRequest:
except plexapi.exceptions.BadRequest:
_LOGGER.exception("Error listing plex sessions")
return
@ -84,10 +160,34 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
update_sessions()
def request_configuration(host, hass, add_devices_callback):
""" Request configuration steps from the user. """
configurator = get_component('configurator')
# We got an error if this method is called while we are configuring
if host in _CONFIGURING:
configurator.notify_errors(
_CONFIGURING[host], "Failed to register, please try again.")
return
def plex_configuration_callback(data):
""" Actions to do when our configuration callback is called. """
setup_plexserver(host, data.get('token'), hass, add_devices_callback)
_CONFIGURING[host] = configurator.request_config(
hass, "Plex Media Server", plex_configuration_callback,
description=('Enter the X-Plex-Token'),
description_image="/static/images/config_plex_mediaserver.png",
submit_caption="Confirm",
fields=[{'id': 'token', 'name': 'X-Plex-Token', 'type': ''}]
)
class PlexClient(MediaPlayerDevice):
""" Represents a Plex device. """
# pylint: disable=too-many-public-methods
# pylint: disable=too-many-public-methods, attribute-defined-outside-init
def __init__(self, device, plex_sessions, update_devices, update_sessions):
self.plex_sessions = plex_sessions
self.update_devices = update_devices
@ -99,17 +199,23 @@ class PlexClient(MediaPlayerDevice):
self.device = device
@property
def session(self):
""" Returns the session, if any. """
if self.device.clientIdentifier not in self.plex_sessions:
return None
return self.plex_sessions[self.device.clientIdentifier]
def unique_id(self):
""" Returns the id of this plex client """
return "{}.{}".format(
self.__class__, self.device.machineIdentifier or self.device.name)
@property
def name(self):
""" Returns the name of the device. """
return self.device.name or self.device.product or self.device.device
return self.device.name or DEVICE_DEFAULT_NAME
@property
def session(self):
""" Returns the session, if any. """
if self.device.machineIdentifier not in self.plex_sessions:
return None
return self.plex_sessions[self.device.machineIdentifier]
@property
def state(self):
@ -120,7 +226,8 @@ class PlexClient(MediaPlayerDevice):
return STATE_PLAYING
elif state == 'paused':
return STATE_PAUSED
elif self.device.isReachable:
# This is nasty. Need to find a way to determine alive
elif self.device:
return STATE_IDLE
else:
return STATE_OFF
@ -196,16 +303,16 @@ class PlexClient(MediaPlayerDevice):
def media_play(self):
""" media_play media player. """
self.device.play({'type': 'video'})
self.device.play()
def media_pause(self):
""" media_pause media player. """
self.device.pause({'type': 'video'})
self.device.pause()
def media_next_track(self):
""" Send next track command. """
self.device.skipNext({'type': 'video'})
self.device.skipNext()
def media_previous_track(self):
""" Send previous track command. """
self.device.skipPrevious({'type': 'video'})
self.device.skipPrevious()

View File

@ -134,7 +134,7 @@ https://github.com/balloob/home-assistant-vera-api/archive/a8f823066ead6c7da6fb5
SoCo==0.11.1
# PlexAPI (media_player.plex)
https://github.com/adrienbrault/python-plexapi/archive/df2d0847e801d6d5cda920326d693cf75f304f1a.zip#python-plexapi==1.0.2
plexapi==1.1.0
# SNMP (device_tracker.snmp)
pysnmp==4.2.5