mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +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_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 |
@ -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'
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user