mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 20:57:21 +00:00
Merge pull request #551 from balloob/mp_plex_discovery
Media_player/plex discovery
This commit is contained in:
commit
004bad7f00
@ -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 |
@ -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'
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user