From 2cd845fb253c54715f5674489e2d4e88cfd23294 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 5 Sep 2019 12:50:26 -0500 Subject: [PATCH] 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 * Cleaner check for server setup Co-Authored-By: Martin Hjelmare * Not needed with previous setup check * Remove username/password support * Reduce log level Co-Authored-By: Martin Hjelmare * 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 --- .coveragerc | 3 +- homeassistant/components/plex/const.py | 15 ++++ homeassistant/components/plex/media_player.py | 59 +++++++------ homeassistant/components/plex/sensor.py | 88 ++++++------------- homeassistant/components/plex/server.py | 59 +++++++++++++ 5 files changed, 136 insertions(+), 88 deletions(-) create mode 100644 homeassistant/components/plex/const.py create mode 100644 homeassistant/components/plex/server.py diff --git a/.coveragerc b/.coveragerc index e9b58c87baa..e75c4180ef6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -470,8 +470,7 @@ omit = homeassistant/components/pioneer/media_player.py homeassistant/components/pjlink/media_player.py homeassistant/components/plaato/* - homeassistant/components/plex/media_player.py - homeassistant/components/plex/sensor.py + homeassistant/components/plex/* homeassistant/components/plugwise/* homeassistant/components/plum_lightpad/* homeassistant/components/pocketcasts/sensor.py diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py new file mode 100644 index 00000000000..1c93ff24c40 --- /dev/null +++ b/homeassistant/components/plex/const.py @@ -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" diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 39694a061c4..5b427e6a353 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -2,8 +2,7 @@ from datetime import timedelta import json import logging - -import requests +import requests.exceptions import voluptuous as vol from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA @@ -21,6 +20,9 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_SET, ) from homeassistant.const import ( + CONF_URL, + CONF_TOKEN, + CONF_VERIFY_SSL, DEVICE_DEFAULT_NAME, STATE_IDLE, 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.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 = {} _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( { 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): """Set up the Plex platform.""" - if PLEX_DATA not in hass.data: - hass.data[PLEX_DATA] = {} + plex_data = hass.data.setdefault(PLEX_DOMAIN, {}) + server_setup = plex_data.setdefault(SERVER_SETUP, False) + if server_setup: + return # get config from plex.conf 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 ): """Set up a plexserver based on host parameter.""" - import plexapi.server import plexapi.exceptions - cert_session = None http_prefix = "https" if has_ssl else "http" - if has_ssl and (verify_ssl is False): - _LOGGER.info("Ignoring SSL verification") - cert_session = requests.Session() - cert_session.verify = False + + server_config = { + CONF_URL: f"{http_prefix}://{host}", + CONF_TOKEN: token, + CONF_VERIFY_SSL: verify_ssl, + } + try: - plexserver = plexapi.server.PlexServer( - f"{http_prefix}://{host}", token, cert_session - ) - _LOGGER.info("Discovery configuration done (no token needed)") + plexserver = PlexServer(server_config) + plexserver.connect() except ( plexapi.exceptions.BadRequest, plexapi.exceptions.Unauthorized, @@ -125,6 +132,8 @@ def setup_plexserver( # No token or wrong token request_configuration(host, hass, config, add_entities_callback) return + else: + hass.data[PLEX_DOMAIN][SERVER_SETUP] = True # If we came here and configuring this host, mark as done if host in _CONFIGURING: @@ -139,9 +148,7 @@ def setup_plexserver( {host: {"token": token, "ssl": has_ssl, "verify": verify_ssl}}, ) - _LOGGER.info("Connected to: %s://%s", http_prefix, host) - - plex_clients = hass.data[PLEX_DATA] + plex_clients = {} plex_sessions = {} track_time_interval(hass, lambda now: update_devices(), timedelta(seconds=10)) diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index d900b4de87c..d18e9506837 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -1,32 +1,30 @@ """Support for Plex media server monitoring.""" from datetime import timedelta import logging +import plexapi.exceptions +import requests.exceptions import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA from homeassistant.const import ( CONF_NAME, - CONF_USERNAME, - CONF_PASSWORD, CONF_HOST, CONF_PORT, CONF_TOKEN, CONF_SSL, + CONF_URL, CONF_VERIFY_SSL, ) from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv -_LOGGER = logging.getLogger(__name__) - -CONF_SERVER = "server" +from .const import DEFAULT_PORT, DEFAULT_SSL, DEFAULT_VERIFY_SSL +from .server import PlexServer DEFAULT_HOST = "localhost" DEFAULT_NAME = "Plex" -DEFAULT_PORT = 32400 -DEFAULT_SSL = False -DEFAULT_VERIFY_SSL = True +_LOGGER = logging.getLogger(__name__) 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_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, vol.Optional(CONF_TOKEN): cv.string, 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_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): """Set up the Plex sensor.""" 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_port = config.get(CONF_PORT) plex_token = config.get(CONF_TOKEN) + verify_ssl = config.get(CONF_VERIFY_SSL) plex_url = "{}://{}:{}".format( "https" if config.get(CONF_SSL) else "http", plex_host, plex_port ) - import plexapi.exceptions - try: - add_entities( - [ - PlexSensor( - name, - plex_url, - plex_user, - plex_password, - plex_server, - plex_token, - config.get(CONF_VERIFY_SSL), - ) - ], - True, + plex_server = PlexServer( + {CONF_URL: plex_url, CONF_TOKEN: plex_token, CONF_VERIFY_SSL: verify_ssl} ) + plex_server.connect() except ( plexapi.exceptions.BadRequest, plexapi.exceptions.Unauthorized, @@ -84,43 +65,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): _LOGGER.error(error) return + add_entities([PlexSensor(name, plex_server)], True) + class PlexSensor(Entity): """Representation of a Plex now playing sensor.""" - def __init__( - self, - name, - plex_url, - plex_user, - plex_password, - plex_server, - plex_token, - verify_ssl, - ): + def __init__(self, name, plex_server): """Initialize the sensor.""" - from plexapi.myplex import MyPlexAccount - from plexapi.server import PlexServer - from requests import Session - self._name = name - self._state = 0 + self._state = None self._now_playing = [] - - 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) + self._server = plex_server @property def name(self): @@ -145,7 +101,19 @@ class PlexSensor(Entity): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """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 = [] for sess in sessions: user = sess.usernames[0] diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py new file mode 100644 index 00000000000..6647b81714f --- /dev/null +++ b/homeassistant/components/plex/server.py @@ -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