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_CAST = 'google_cast'
SERVICE_NETGEAR = 'netgear_router' SERVICE_NETGEAR = 'netgear_router'
SERVICE_SONOS = 'sonos' SERVICE_SONOS = 'sonos'
SERVICE_PLEX = 'plex_mediaserver'
SERVICE_HANDLERS = { SERVICE_HANDLERS = {
SERVICE_WEMO: "switch", SERVICE_WEMO: "switch",
@ -35,6 +36,7 @@ SERVICE_HANDLERS = {
SERVICE_HUE: "light", SERVICE_HUE: "light",
SERVICE_NETGEAR: 'device_tracker', SERVICE_NETGEAR: 'device_tracker',
SERVICE_SONOS: 'media_player', SERVICE_SONOS: 'media_player',
SERVICE_PLEX: 'media_player',
} }
@ -88,6 +90,7 @@ def setup(hass, config):
ATTR_DISCOVERED: info ATTR_DISCOVERED: info
}) })
# pylint: disable=unused-argument
def start_discovery(event): def start_discovery(event):
""" Start discovering. """ """ Start discovering. """
netdisco = DiscoveryService(SCAN_INTERVAL) 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_PLATFORMS = {
discovery.SERVICE_CAST: 'cast', discovery.SERVICE_CAST: 'cast',
discovery.SERVICE_SONOS: 'sonos', discovery.SERVICE_SONOS: 'sonos',
discovery.SERVICE_PLEX: 'plex',
} }
SERVICE_YOUTUBE_VIDEO = 'play_youtube_video' 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 For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/media_player.plex.html https://home-assistant.io/components/media_player.plex.html
""" """
import os
import json
import logging import logging
from datetime import timedelta 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 ( from homeassistant.components.media_player import (
MediaPlayerDevice, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK, MediaPlayerDevice, SUPPORT_PAUSE, SUPPORT_PREVIOUS_TRACK,
SUPPORT_NEXT_TRACK, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO) SUPPORT_NEXT_TRACK, MEDIA_TYPE_TVSHOW, MEDIA_TYPE_VIDEO)
from homeassistant.const import ( from homeassistant.const import (
STATE_IDLE, STATE_PLAYING, STATE_PAUSED, STATE_OFF, STATE_UNKNOWN) DEVICE_DEFAULT_NAME, STATE_IDLE, STATE_PLAYING,
import homeassistant.util as util STATE_PAUSED, STATE_OFF, STATE_UNKNOWN)
REQUIREMENTS = ['https://github.com/adrienbrault/python-plexapi/archive/' REQUIREMENTS = ['plexapi==1.1.0']
'df2d0847e801d6d5cda920326d693cf75f304f1a.zip'
'#python-plexapi==1.0.2']
MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10) MIN_TIME_BETWEEN_SCANS = timedelta(seconds=10)
MIN_TIME_BETWEEN_FORCED_SCANS = timedelta(seconds=1) 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__) _LOGGER = logging.getLogger(__name__)
SUPPORT_PLEX = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK SUPPORT_PLEX = SUPPORT_PAUSE | SUPPORT_PREVIOUS_TRACK | SUPPORT_NEXT_TRACK
# pylint: disable=abstract-method, unused-argument def config_from_file(filename, config=None):
def setup_platform(hass, config, add_devices, discovery_info=None): ''' Small configuration file management function'''
""" Sets up the plex platform. """ if config:
from plexapi.myplex import MyPlexUser # We're writing configuration
from plexapi.exceptions import BadRequest 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_clients = {}
plex_sessions = {} plex_sessions = {}
@ -45,34 +121,34 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
def update_devices(): def update_devices():
""" Updates the devices objects. """ """ Updates the devices objects. """
try: try:
devices = plexuser.devices() devices = plexserver.clients()
except BadRequest: except plexapi.exceptions.BadRequest:
_LOGGER.exception("Error listing plex devices") _LOGGER.exception("Error listing plex devices")
return return
new_plex_clients = [] new_plex_clients = []
for device in devices: for device in devices:
if (all(x not in ['client', 'player'] for x in device.provides) # For now, let's allow all deviceClass types
or 'PlexAPI' == device.product): if device.deviceClass in ['badClient']:
continue continue
if device.clientIdentifier not in plex_clients: if device.machineIdentifier not in plex_clients:
new_client = PlexClient(device, plex_sessions, update_devices, new_client = PlexClient(device, plex_sessions, update_devices,
update_sessions) update_sessions)
plex_clients[device.clientIdentifier] = new_client plex_clients[device.machineIdentifier] = new_client
new_plex_clients.append(new_client) new_plex_clients.append(new_client)
else: else:
plex_clients[device.clientIdentifier].set_device(device) plex_clients[device.machineIdentifier].set_device(device)
if new_plex_clients: 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) @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS)
def update_sessions(): def update_sessions():
""" Updates the sessions objects. """ """ Updates the sessions objects. """
try: try:
sessions = plexserver.sessions() sessions = plexserver.sessions()
except BadRequest: except plexapi.exceptions.BadRequest:
_LOGGER.exception("Error listing plex sessions") _LOGGER.exception("Error listing plex sessions")
return return
@ -84,10 +160,34 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
update_sessions() 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): class PlexClient(MediaPlayerDevice):
""" Represents a Plex device. """ """ 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): def __init__(self, device, plex_sessions, update_devices, update_sessions):
self.plex_sessions = plex_sessions self.plex_sessions = plex_sessions
self.update_devices = update_devices self.update_devices = update_devices
@ -99,17 +199,23 @@ class PlexClient(MediaPlayerDevice):
self.device = device self.device = device
@property @property
def session(self): def unique_id(self):
""" Returns the session, if any. """ """ Returns the id of this plex client """
if self.device.clientIdentifier not in self.plex_sessions: return "{}.{}".format(
return None self.__class__, self.device.machineIdentifier or self.device.name)
return self.plex_sessions[self.device.clientIdentifier]
@property @property
def name(self): def name(self):
""" Returns the name of the device. """ """ 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 @property
def state(self): def state(self):
@ -120,7 +226,8 @@ class PlexClient(MediaPlayerDevice):
return STATE_PLAYING return STATE_PLAYING
elif state == 'paused': elif state == 'paused':
return 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 return STATE_IDLE
else: else:
return STATE_OFF return STATE_OFF
@ -196,16 +303,16 @@ class PlexClient(MediaPlayerDevice):
def media_play(self): def media_play(self):
""" media_play media player. """ """ media_play media player. """
self.device.play({'type': 'video'}) self.device.play()
def media_pause(self): def media_pause(self):
""" media_pause media player. """ """ media_pause media player. """
self.device.pause({'type': 'video'}) self.device.pause()
def media_next_track(self): def media_next_track(self):
""" Send next track command. """ """ Send next track command. """
self.device.skipNext({'type': 'video'}) self.device.skipNext()
def media_previous_track(self): def media_previous_track(self):
""" Send previous track command. """ """ 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 SoCo==0.11.1
# PlexAPI (media_player.plex) # 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) # SNMP (device_tracker.snmp)
pysnmp==4.2.5 pysnmp==4.2.5