Standardize Plex server connections (#26444)

* Common connection class

* Omit tests for new Plex files

* Oops

* Add missing properties

* Remove redundant log message

* Stopgap to avoid duplicate setups

* Cleaner check for server setup

Co-Authored-By: Martin Hjelmare <marhje52@kth.se>

* Cleaner check for server setup

Co-Authored-By: Martin Hjelmare <marhje52@kth.se>

* Not needed with previous setup check

* Remove username/password support

* Reduce log level

Co-Authored-By: Martin Hjelmare <marhje52@kth.se>

* Don't do setup in __init__

* Oops

* Committing too fast...

* Connect after init

* Catch update exceptions like media_player

* Pass in validated PlexServer instance

* Remove unnecessary check

* Counter should be unknown on init

* Remove servername config option
This commit is contained in:
jjlawren 2019-09-05 12:50:26 -05:00 committed by Martin Hjelmare
parent a000125729
commit 2cd845fb25
5 changed files with 136 additions and 88 deletions

View File

@ -470,8 +470,7 @@ omit =
homeassistant/components/pioneer/media_player.py homeassistant/components/pioneer/media_player.py
homeassistant/components/pjlink/media_player.py homeassistant/components/pjlink/media_player.py
homeassistant/components/plaato/* homeassistant/components/plaato/*
homeassistant/components/plex/media_player.py homeassistant/components/plex/*
homeassistant/components/plex/sensor.py
homeassistant/components/plugwise/* homeassistant/components/plugwise/*
homeassistant/components/plum_lightpad/* homeassistant/components/plum_lightpad/*
homeassistant/components/pocketcasts/sensor.py homeassistant/components/pocketcasts/sensor.py

View File

@ -0,0 +1,15 @@
"""Constants for the Plex component."""
DOMAIN = "plex"
NAME_FORMAT = "Plex {}"
DEFAULT_PORT = 32400
DEFAULT_SSL = False
DEFAULT_VERIFY_SSL = True
PLEX_CONFIG_FILE = "plex.conf"
PLEX_SERVER_CONFIG = "server_config"
CONF_USE_EPISODE_ART = "use_episode_art"
CONF_SHOW_ALL_CONTROLS = "show_all_controls"
CONF_REMOVE_UNAVAILABLE_CLIENTS = "remove_unavailable_clients"
CONF_CLIENT_REMOVE_INTERVAL = "client_remove_interval"

View File

@ -2,8 +2,7 @@
from datetime import timedelta from datetime import timedelta
import json import json
import logging import logging
import requests.exceptions
import requests
import voluptuous as vol import voluptuous as vol
from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA
@ -21,6 +20,9 @@ from homeassistant.components.media_player.const import (
SUPPORT_VOLUME_SET, SUPPORT_VOLUME_SET,
) )
from homeassistant.const import ( from homeassistant.const import (
CONF_URL,
CONF_TOKEN,
CONF_VERIFY_SSL,
DEVICE_DEFAULT_NAME, DEVICE_DEFAULT_NAME,
STATE_IDLE, STATE_IDLE,
STATE_OFF, STATE_OFF,
@ -32,18 +34,22 @@ from homeassistant.helpers.event import track_time_interval
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from homeassistant.util.json import load_json, save_json from homeassistant.util.json import load_json, save_json
from .const import (
CONF_USE_EPISODE_ART,
CONF_SHOW_ALL_CONTROLS,
CONF_REMOVE_UNAVAILABLE_CLIENTS,
CONF_CLIENT_REMOVE_INTERVAL,
DOMAIN as PLEX_DOMAIN,
NAME_FORMAT,
PLEX_CONFIG_FILE,
)
from .server import PlexServer
SERVER_SETUP = "server_setup"
_CONFIGURING = {} _CONFIGURING = {}
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
NAME_FORMAT = "Plex {}"
PLEX_CONFIG_FILE = "plex.conf"
PLEX_DATA = "plex"
CONF_USE_EPISODE_ART = "use_episode_art"
CONF_SHOW_ALL_CONTROLS = "show_all_controls"
CONF_REMOVE_UNAVAILABLE_CLIENTS = "remove_unavailable_clients"
CONF_CLIENT_REMOVE_INTERVAL = "client_remove_interval"
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{ {
vol.Optional(CONF_USE_EPISODE_ART, default=False): cv.boolean, vol.Optional(CONF_USE_EPISODE_ART, default=False): cv.boolean,
@ -58,8 +64,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities_callback, discovery_info=None): def setup_platform(hass, config, add_entities_callback, discovery_info=None):
"""Set up the Plex platform.""" """Set up the Plex platform."""
if PLEX_DATA not in hass.data: plex_data = hass.data.setdefault(PLEX_DOMAIN, {})
hass.data[PLEX_DATA] = {} server_setup = plex_data.setdefault(SERVER_SETUP, False)
if server_setup:
return
# get config from plex.conf # get config from plex.conf
file_config = load_json(hass.config.path(PLEX_CONFIG_FILE)) file_config = load_json(hass.config.path(PLEX_CONFIG_FILE))
@ -102,20 +110,19 @@ def setup_plexserver(
host, token, has_ssl, verify_ssl, hass, config, add_entities_callback host, token, has_ssl, verify_ssl, hass, config, add_entities_callback
): ):
"""Set up a plexserver based on host parameter.""" """Set up a plexserver based on host parameter."""
import plexapi.server
import plexapi.exceptions import plexapi.exceptions
cert_session = None
http_prefix = "https" if has_ssl else "http" http_prefix = "https" if has_ssl else "http"
if has_ssl and (verify_ssl is False):
_LOGGER.info("Ignoring SSL verification") server_config = {
cert_session = requests.Session() CONF_URL: f"{http_prefix}://{host}",
cert_session.verify = False CONF_TOKEN: token,
CONF_VERIFY_SSL: verify_ssl,
}
try: try:
plexserver = plexapi.server.PlexServer( plexserver = PlexServer(server_config)
f"{http_prefix}://{host}", token, cert_session plexserver.connect()
)
_LOGGER.info("Discovery configuration done (no token needed)")
except ( except (
plexapi.exceptions.BadRequest, plexapi.exceptions.BadRequest,
plexapi.exceptions.Unauthorized, plexapi.exceptions.Unauthorized,
@ -125,6 +132,8 @@ def setup_plexserver(
# No token or wrong token # No token or wrong token
request_configuration(host, hass, config, add_entities_callback) request_configuration(host, hass, config, add_entities_callback)
return return
else:
hass.data[PLEX_DOMAIN][SERVER_SETUP] = True
# If we came here and configuring this host, mark as done # If we came here and configuring this host, mark as done
if host in _CONFIGURING: if host in _CONFIGURING:
@ -139,9 +148,7 @@ def setup_plexserver(
{host: {"token": token, "ssl": has_ssl, "verify": verify_ssl}}, {host: {"token": token, "ssl": has_ssl, "verify": verify_ssl}},
) )
_LOGGER.info("Connected to: %s://%s", http_prefix, host) plex_clients = {}
plex_clients = hass.data[PLEX_DATA]
plex_sessions = {} plex_sessions = {}
track_time_interval(hass, lambda now: update_devices(), timedelta(seconds=10)) track_time_interval(hass, lambda now: update_devices(), timedelta(seconds=10))

View File

@ -1,32 +1,30 @@
"""Support for Plex media server monitoring.""" """Support for Plex media server monitoring."""
from datetime import timedelta from datetime import timedelta
import logging import logging
import plexapi.exceptions
import requests.exceptions
import voluptuous as vol import voluptuous as vol
from homeassistant.components.switch import PLATFORM_SCHEMA from homeassistant.components.switch import PLATFORM_SCHEMA
from homeassistant.const import ( from homeassistant.const import (
CONF_NAME, CONF_NAME,
CONF_USERNAME,
CONF_PASSWORD,
CONF_HOST, CONF_HOST,
CONF_PORT, CONF_PORT,
CONF_TOKEN, CONF_TOKEN,
CONF_SSL, CONF_SSL,
CONF_URL,
CONF_VERIFY_SSL, CONF_VERIFY_SSL,
) )
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle from homeassistant.util import Throttle
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
_LOGGER = logging.getLogger(__name__) from .const import DEFAULT_PORT, DEFAULT_SSL, DEFAULT_VERIFY_SSL
from .server import PlexServer
CONF_SERVER = "server"
DEFAULT_HOST = "localhost" DEFAULT_HOST = "localhost"
DEFAULT_NAME = "Plex" DEFAULT_NAME = "Plex"
DEFAULT_PORT = 32400 _LOGGER = logging.getLogger(__name__)
DEFAULT_SSL = False
DEFAULT_VERIFY_SSL = True
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1)
@ -34,11 +32,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{ {
vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_TOKEN): cv.string, vol.Optional(CONF_TOKEN): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
vol.Optional(CONF_SERVER): cv.string,
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean,
} }
@ -48,34 +43,20 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
def setup_platform(hass, config, add_entities, discovery_info=None): def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Plex sensor.""" """Set up the Plex sensor."""
name = config.get(CONF_NAME) name = config.get(CONF_NAME)
plex_user = config.get(CONF_USERNAME)
plex_password = config.get(CONF_PASSWORD)
plex_server = config.get(CONF_SERVER)
plex_host = config.get(CONF_HOST) plex_host = config.get(CONF_HOST)
plex_port = config.get(CONF_PORT) plex_port = config.get(CONF_PORT)
plex_token = config.get(CONF_TOKEN) plex_token = config.get(CONF_TOKEN)
verify_ssl = config.get(CONF_VERIFY_SSL)
plex_url = "{}://{}:{}".format( plex_url = "{}://{}:{}".format(
"https" if config.get(CONF_SSL) else "http", plex_host, plex_port "https" if config.get(CONF_SSL) else "http", plex_host, plex_port
) )
import plexapi.exceptions
try: try:
add_entities( plex_server = PlexServer(
[ {CONF_URL: plex_url, CONF_TOKEN: plex_token, CONF_VERIFY_SSL: verify_ssl}
PlexSensor(
name,
plex_url,
plex_user,
plex_password,
plex_server,
plex_token,
config.get(CONF_VERIFY_SSL),
)
],
True,
) )
plex_server.connect()
except ( except (
plexapi.exceptions.BadRequest, plexapi.exceptions.BadRequest,
plexapi.exceptions.Unauthorized, plexapi.exceptions.Unauthorized,
@ -84,43 +65,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
_LOGGER.error(error) _LOGGER.error(error)
return return
add_entities([PlexSensor(name, plex_server)], True)
class PlexSensor(Entity): class PlexSensor(Entity):
"""Representation of a Plex now playing sensor.""" """Representation of a Plex now playing sensor."""
def __init__( def __init__(self, name, plex_server):
self,
name,
plex_url,
plex_user,
plex_password,
plex_server,
plex_token,
verify_ssl,
):
"""Initialize the sensor.""" """Initialize the sensor."""
from plexapi.myplex import MyPlexAccount
from plexapi.server import PlexServer
from requests import Session
self._name = name self._name = name
self._state = 0 self._state = None
self._now_playing = [] self._now_playing = []
self._server = plex_server
cert_session = None
if not verify_ssl:
_LOGGER.info("Ignoring SSL verification")
cert_session = Session()
cert_session.verify = False
if plex_token:
self._server = PlexServer(plex_url, plex_token, cert_session)
elif plex_user and plex_password:
user = MyPlexAccount(plex_user, plex_password)
server = plex_server if plex_server else user.resources()[0].name
self._server = user.resource(server).connect()
else:
self._server = PlexServer(plex_url, None, cert_session)
@property @property
def name(self): def name(self):
@ -145,7 +101,19 @@ class PlexSensor(Entity):
@Throttle(MIN_TIME_BETWEEN_UPDATES) @Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self): def update(self):
"""Update method for Plex sensor.""" """Update method for Plex sensor."""
sessions = self._server.sessions() try:
sessions = self._server.sessions()
except plexapi.exceptions.BadRequest:
_LOGGER.error(
"Error listing current Plex sessions on %s", self._server.friendly_name
)
return
except requests.exceptions.RequestException as ex:
_LOGGER.warning(
"Temporary error connecting to %s (%s)", self._server.friendly_name, ex
)
return
now_playing = [] now_playing = []
for sess in sessions: for sess in sessions:
user = sess.usernames[0] user = sess.usernames[0]

View File

@ -0,0 +1,59 @@
"""Shared class to maintain Plex server instances."""
import logging
import plexapi.server
from requests import Session
from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL
from .const import DEFAULT_VERIFY_SSL
_LOGGER = logging.getLogger(__package__)
class PlexServer:
"""Manages a single Plex server connection."""
def __init__(self, server_config):
"""Initialize a Plex server instance."""
self._plex_server = None
self._url = server_config.get(CONF_URL)
self._token = server_config.get(CONF_TOKEN)
self._verify_ssl = server_config.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL)
def connect(self):
"""Connect to a Plex server directly, obtaining direct URL if necessary."""
def _connect_with_url():
session = None
if self._url.startswith("https") and not self._verify_ssl:
session = Session()
session.verify = False
self._plex_server = plexapi.server.PlexServer(
self._url, self._token, session
)
_LOGGER.debug("Connected to: %s (%s)", self.friendly_name, self.url_in_use)
_connect_with_url()
def clients(self):
"""Pass through clients call to plexapi."""
return self._plex_server.clients()
def sessions(self):
"""Pass through sessions call to plexapi."""
return self._plex_server.sessions()
@property
def friendly_name(self):
"""Return name of connected Plex server."""
return self._plex_server.friendlyName
@property
def machine_identifier(self):
"""Return unique identifier of connected Plex server."""
return self._plex_server.machineIdentifier
@property
def url_in_use(self):
"""Return URL used for connected Plex server."""
return self._plex_server._baseurl # pylint: disable=W0212