From 30fb4ddc9888bdb1c2e4588aaed782577810a817 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 9 Sep 2019 16:28:20 -0500 Subject: [PATCH] Move config and connections to Plex component (#26488) * Move config and connections to component * Separate imports * Set a unique_id on sensor * Set a platforms const * Add SERVERS dict, hardcode to single server * Move to debug * Return false * More debug * Import at top to fix lint * Guard against legacy setup attempts * Refactor to add setup callback * Review comments * Log levels * Return result of callback * Store CONFIGURING in hass.data * Set up discovery if no config data * Use schema to set defaults * Remove media_player options to remove entities * Improve error handling --- .../components/discovery/__init__.py | 3 +- homeassistant/components/plex/__init__.py | 202 +++++++++++++++++- homeassistant/components/plex/const.py | 4 + homeassistant/components/plex/media_player.py | 197 ++--------------- homeassistant/components/plex/sensor.py | 62 ++---- homeassistant/components/plex/server.py | 1 + 6 files changed, 238 insertions(+), 231 deletions(-) diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index 5f1fd335d45..827e05a424b 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -36,6 +36,7 @@ SERVICE_KONNECTED = "konnected" SERVICE_MOBILE_APP = "hass_mobile_app" SERVICE_NETGEAR = "netgear_router" SERVICE_OCTOPRINT = "octoprint" +SERVICE_PLEX = "plex_mediaserver" SERVICE_ROKU = "roku" SERVICE_SABNZBD = "sabnzbd" SERVICE_SAMSUNG_PRINTER = "samsung_printer" @@ -68,7 +69,7 @@ SERVICE_HANDLERS = { SERVICE_FREEBOX: ("freebox", None), SERVICE_YEELIGHT: ("yeelight", None), "panasonic_viera": ("media_player", "panasonic_viera"), - "plex_mediaserver": ("media_player", "plex"), + SERVICE_PLEX: ("plex", None), "yamaha": ("media_player", "yamaha"), "logitech_mediaserver": ("media_player", "squeezebox"), "directv": ("media_player", "directv"), diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 6e4e02026ab..846f3e3f53c 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -1 +1,201 @@ -"""The plex component.""" +"""Support to embed Plex.""" +import logging + +import plexapi.exceptions +import requests.exceptions +import voluptuous as vol + +from homeassistant.components.discovery import SERVICE_PLEX +from homeassistant.components.media_player import DOMAIN as MP_DOMAIN +from homeassistant.const import ( + CONF_HOST, + CONF_PORT, + CONF_SSL, + CONF_TOKEN, + CONF_URL, + CONF_VERIFY_SSL, +) +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import discovery +from homeassistant.util.json import load_json, save_json + +from .const import ( + CONF_USE_EPISODE_ART, + CONF_SHOW_ALL_CONTROLS, + DEFAULT_HOST, + DEFAULT_PORT, + DEFAULT_SSL, + DEFAULT_VERIFY_SSL, + DOMAIN as PLEX_DOMAIN, + PLATFORMS, + PLEX_CONFIG_FILE, + PLEX_MEDIA_PLAYER_OPTIONS, + SERVERS, +) +from .server import PlexServer + +MEDIA_PLAYER_SCHEMA = vol.Schema( + { + vol.Optional(CONF_USE_EPISODE_ART, default=False): cv.boolean, + vol.Optional(CONF_SHOW_ALL_CONTROLS, default=False): cv.boolean, + } +) + +SERVER_CONFIG_SCHEMA = vol.Schema( + { + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_TOKEN): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, + vol.Optional(MP_DOMAIN, default={}): MEDIA_PLAYER_SCHEMA, + } +) + +CONFIG_SCHEMA = vol.Schema({PLEX_DOMAIN: SERVER_CONFIG_SCHEMA}, extra=vol.ALLOW_EXTRA) + +CONFIGURING = "configuring" +_LOGGER = logging.getLogger(__package__) + + +def setup(hass, config): + """Set up the Plex component.""" + + def server_discovered(service, info): + """Pass back discovered Plex server details.""" + if hass.data[PLEX_DOMAIN][SERVERS]: + _LOGGER.debug("Plex server already configured, ignoring discovery.") + return + _LOGGER.debug("Discovered Plex server: %s:%s", info["host"], info["port"]) + setup_plex(discovery_info=info) + + def setup_plex(config=None, discovery_info=None, configurator_info=None): + """Return assembled server_config dict.""" + json_file = hass.config.path(PLEX_CONFIG_FILE) + file_config = load_json(json_file) + + if config: + server_config = config + host_and_port = ( + f"{server_config.pop(CONF_HOST)}:{server_config.pop(CONF_PORT)}" + ) + if MP_DOMAIN in server_config: + hass.data[PLEX_MEDIA_PLAYER_OPTIONS] = server_config.pop(MP_DOMAIN) + elif file_config: + _LOGGER.debug("Loading config from %s", json_file) + host_and_port, server_config = file_config.popitem() + server_config[CONF_VERIFY_SSL] = server_config.pop("verify") + elif discovery_info: + server_config = {} + host_and_port = f"{discovery_info[CONF_HOST]}:{discovery_info[CONF_PORT]}" + elif configurator_info: + server_config = configurator_info + host_and_port = server_config["host_and_port"] + else: + discovery.listen(hass, SERVICE_PLEX, server_discovered) + return True + + use_ssl = server_config.get(CONF_SSL, DEFAULT_SSL) + http_prefix = "https" if use_ssl else "http" + server_config[CONF_URL] = f"{http_prefix}://{host_and_port}" + + plex_server = PlexServer(server_config) + try: + plex_server.connect() + except requests.exceptions.ConnectionError as error: + _LOGGER.error( + "Plex server could not be reached, please verify host and port: [%s]", + error, + ) + return False + except ( + plexapi.exceptions.BadRequest, + plexapi.exceptions.Unauthorized, + plexapi.exceptions.NotFound, + ) as error: + _LOGGER.error( + "Connection to Plex server failed, please verify token and SSL settings: [%s]", + error, + ) + request_configuration(host_and_port) + return False + else: + hass.data[PLEX_DOMAIN][SERVERS][ + plex_server.machine_identifier + ] = plex_server + + if host_and_port in hass.data[PLEX_DOMAIN][CONFIGURING]: + request_id = hass.data[PLEX_DOMAIN][CONFIGURING].pop(host_and_port) + configurator = hass.components.configurator + configurator.request_done(request_id) + _LOGGER.debug("Discovery configuration done") + if configurator_info: + # Write plex.conf if created via discovery/configurator + save_json( + hass.config.path(PLEX_CONFIG_FILE), + { + host_and_port: { + CONF_TOKEN: server_config[CONF_TOKEN], + CONF_SSL: use_ssl, + "verify": server_config[CONF_VERIFY_SSL], + } + }, + ) + + if not hass.data.get(PLEX_MEDIA_PLAYER_OPTIONS): + hass.data[PLEX_MEDIA_PLAYER_OPTIONS] = MEDIA_PLAYER_SCHEMA({}) + + for platform in PLATFORMS: + hass.helpers.discovery.load_platform( + platform, PLEX_DOMAIN, {}, original_config + ) + + return True + + def request_configuration(host_and_port): + """Request configuration steps from the user.""" + configurator = hass.components.configurator + if host_and_port in hass.data[PLEX_DOMAIN][CONFIGURING]: + configurator.notify_errors( + hass.data[PLEX_DOMAIN][CONFIGURING][host_and_port], + "Failed to register, please try again.", + ) + return + + def plex_configuration_callback(data): + """Handle configuration changes.""" + config = { + "host_and_port": host_and_port, + CONF_TOKEN: data.get("token"), + CONF_SSL: cv.boolean(data.get("ssl")), + CONF_VERIFY_SSL: cv.boolean(data.get("verify_ssl")), + } + setup_plex(configurator_info=config) + + hass.data[PLEX_DOMAIN][CONFIGURING][ + host_and_port + ] = configurator.request_config( + "Plex Media Server", + plex_configuration_callback, + description="Enter the X-Plex-Token", + entity_picture="/static/images/logo_plex_mediaserver.png", + submit_caption="Confirm", + fields=[ + {"id": "token", "name": "X-Plex-Token", "type": ""}, + {"id": "ssl", "name": "Use SSL", "type": ""}, + {"id": "verify_ssl", "name": "Verify SSL", "type": ""}, + ], + ) + + # End of inner functions. + + original_config = config + + hass.data.setdefault(PLEX_DOMAIN, {SERVERS: {}, CONFIGURING: {}}) + + if hass.data[PLEX_DOMAIN][SERVERS]: + _LOGGER.debug("Plex server already configured") + return False + + plex_config = config.get(PLEX_DOMAIN, {}) + return setup_plex(config=plex_config) diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index 4495b9a8c83..bf8c2387e4d 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -7,7 +7,11 @@ DEFAULT_PORT = 32400 DEFAULT_SSL = False DEFAULT_VERIFY_SSL = True +PLATFORMS = ["media_player", "sensor"] +SERVERS = "servers" + PLEX_CONFIG_FILE = "plex.conf" +PLEX_MEDIA_PLAYER_OPTIONS = "plex_mp_options" PLEX_SERVER_CONFIG = "server_config" CONF_USE_EPISODE_ART = "use_episode_art" diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 6005321310d..cfc63948bee 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -2,10 +2,13 @@ from datetime import timedelta import json import logging -import requests.exceptions -import voluptuous as vol -from homeassistant.components.media_player import MediaPlayerDevice, PLATFORM_SCHEMA +import plexapi.exceptions +import plexapi.playlist +import plexapi.playqueue +import requests.exceptions + +from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, @@ -20,150 +23,37 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_SET, ) from homeassistant.const import ( - CONF_HOST, - CONF_PORT, - CONF_SSL, - CONF_URL, - CONF_TOKEN, - CONF_VERIFY_SSL, DEVICE_DEFAULT_NAME, STATE_IDLE, STATE_OFF, STATE_PAUSED, STATE_PLAYING, ) -from homeassistant.helpers import config_validation as cv 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, - DEFAULT_HOST, - DEFAULT_PORT, - DEFAULT_SSL, - DEFAULT_VERIFY_SSL, DOMAIN as PLEX_DOMAIN, NAME_FORMAT, - PLEX_CONFIG_FILE, + PLEX_MEDIA_PLAYER_OPTIONS, + SERVERS, ) -from .server import PlexServer SERVER_SETUP = "server_setup" _CONFIGURING = {} _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_TOKEN): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, - vol.Optional(CONF_USE_EPISODE_ART, default=False): cv.boolean, - vol.Optional(CONF_SHOW_ALL_CONTROLS, default=False): cv.boolean, - vol.Optional(CONF_REMOVE_UNAVAILABLE_CLIENTS, default=True): cv.boolean, - vol.Optional( - CONF_CLIENT_REMOVE_INTERVAL, default=timedelta(seconds=600) - ): vol.All(cv.time_period, cv.positive_timedelta), - } -) - def setup_platform(hass, config, add_entities_callback, discovery_info=None): """Set up the Plex platform.""" - plex_data = hass.data.setdefault(PLEX_DOMAIN, {}) - server_setup = plex_data.setdefault(SERVER_SETUP, False) - if server_setup: + if discovery_info is None: return - # get config from plex.conf - file_config = load_json(hass.config.path(PLEX_CONFIG_FILE)) - - if file_config: - # Setup a configured PlexServer - host, host_config = file_config.popitem() - token = host_config["token"] - try: - has_ssl = host_config["ssl"] - except KeyError: - has_ssl = False - try: - verify_ssl = host_config["verify"] - except KeyError: - verify_ssl = True - - # Via discovery - elif discovery_info is not None: - # Parse discovery data - host = discovery_info.get("host") - port = discovery_info.get("port") - host = f"{host}:{port}" - _LOGGER.info("Discovered PLEX server: %s", host) - - if host in _CONFIGURING: - return - token = None - has_ssl = False - verify_ssl = True - else: - host = config[CONF_HOST] - port = config[CONF_PORT] - host = f"{host}:{port}" - token = config.get(CONF_TOKEN) - has_ssl = config[CONF_SSL] - verify_ssl = config[CONF_VERIFY_SSL] - - setup_plexserver( - host, token, has_ssl, verify_ssl, hass, config, add_entities_callback - ) - - -def setup_plexserver( - host, token, has_ssl, verify_ssl, hass, config, add_entities_callback -): - """Set up a plexserver based on host parameter.""" - import plexapi.exceptions - - http_prefix = "https" if has_ssl else "http" - - server_config = { - CONF_URL: f"{http_prefix}://{host}", - CONF_TOKEN: token, - CONF_VERIFY_SSL: verify_ssl, - } - - try: - plexserver = PlexServer(server_config) - plexserver.connect() - except ( - plexapi.exceptions.BadRequest, - plexapi.exceptions.Unauthorized, - plexapi.exceptions.NotFound, - ) as error: - _LOGGER.info(error) - # 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: - request_id = _CONFIGURING.pop(host) - configurator = hass.components.configurator - configurator.request_done(request_id) - _LOGGER.info("Discovery configuration done") - - # Save config - save_json( - hass.config.path(PLEX_CONFIG_FILE), - {host: {"token": token, "ssl": has_ssl, "verify": verify_ssl}}, - ) + plexserver = list(hass.data[PLEX_DOMAIN][SERVERS].values())[0] + config = hass.data[PLEX_MEDIA_PLAYER_OPTIONS] plex_clients = {} plex_sessions = {} @@ -178,7 +68,9 @@ def setup_plexserver( return except requests.exceptions.RequestException as ex: _LOGGER.warning( - "Could not connect to plex server at http://%s (%s)", host, ex + "Could not connect to Plex server: %s (%s)", + plexserver.friendly_name, + ex, ) return @@ -210,7 +102,9 @@ def setup_plexserver( return except requests.exceptions.RequestException as ex: _LOGGER.warning( - "Could not connect to plex server at http://%s (%s)", host, ex + "Could not connect to Plex server: %s (%s)", + plexserver.friendly_name, + ex, ) return @@ -239,7 +133,6 @@ def setup_plexserver( _LOGGER.debug("Refreshing session: %s", machine_identifier) plex_clients[machine_identifier].refresh(None, session) - clients_to_remove = [] for client in plex_clients.values(): # force devices to idle that do not have a valid session if client.session is None: @@ -253,59 +146,10 @@ def setup_plexserver( if client not in new_plex_clients: client.schedule_update_ha_state() - if not config.get(CONF_REMOVE_UNAVAILABLE_CLIENTS) or client.available: - continue - - if (dt_util.utcnow() - client.marked_unavailable) >= ( - config.get(CONF_CLIENT_REMOVE_INTERVAL) - ): - hass.add_job(client.async_remove()) - clients_to_remove.append(client.machine_identifier) - - while clients_to_remove: - del plex_clients[clients_to_remove.pop()] - if new_plex_clients: add_entities_callback(new_plex_clients) -def request_configuration(host, hass, config, add_entities_callback): - """Request configuration steps from the user.""" - configurator = hass.components.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): - """Handle configuration changes.""" - setup_plexserver( - host, - data.get("token"), - cv.boolean(data.get("has_ssl")), - cv.boolean(data.get("do_not_verify_ssl")), - hass, - config, - add_entities_callback, - ) - - _CONFIGURING[host] = configurator.request_config( - "Plex Media Server", - plex_configuration_callback, - description="Enter the X-Plex-Token", - entity_picture="/static/images/logo_plex_mediaserver.png", - submit_caption="Confirm", - fields=[ - {"id": "token", "name": "X-Plex-Token", "type": ""}, - {"id": "has_ssl", "name": "Use SSL", "type": ""}, - {"id": "do_not_verify_ssl", "name": "Do not verify SSL", "type": ""}, - ], - ) - - class PlexClient(MediaPlayerDevice): """Representation of a Plex device.""" @@ -378,9 +222,6 @@ class PlexClient(MediaPlayerDevice): def refresh(self, device, session): """Refresh key device data.""" - import plexapi.exceptions - - # new data refresh self._clear_media_details() if session: # Not being triggered by Chrome or FireTablet Plex App @@ -851,8 +692,6 @@ class PlexClient(MediaPlayerDevice): src["video_name"] ) - import plexapi.playlist - if ( media and media_type == "EPISODE" @@ -918,8 +757,6 @@ class PlexClient(MediaPlayerDevice): _LOGGER.error("Client cannot play media: %s", self.entity_id) return - import plexapi.playqueue - playqueue = plexapi.playqueue.PlayQueue.create( self.device.server, media, **params ) diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index bece6274af6..f469e95da80 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -1,87 +1,51 @@ """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_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 -from .const import DEFAULT_HOST, DEFAULT_PORT, DEFAULT_SSL, DEFAULT_VERIFY_SSL -from .server import PlexServer +from .const import DOMAIN as PLEX_DOMAIN, SERVERS DEFAULT_NAME = "Plex" _LOGGER = logging.getLogger(__name__) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) -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_TOKEN): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, - } -) - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Plex sensor.""" - name = config.get(CONF_NAME) - 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 - ) - - try: - 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, - plexapi.exceptions.NotFound, - ) as error: - _LOGGER.error(error) + if discovery_info is None: return - add_entities([PlexSensor(name, plex_server)], True) + plexserver = list(hass.data[PLEX_DOMAIN][SERVERS].values())[0] + add_entities([PlexSensor(plexserver)], True) class PlexSensor(Entity): """Representation of a Plex now playing sensor.""" - def __init__(self, name, plex_server): + def __init__(self, plex_server): """Initialize the sensor.""" - self._name = name + self._name = DEFAULT_NAME self._state = None self._now_playing = [] self._server = plex_server + self._unique_id = f"sensor-{plex_server.machine_identifier}" @property def name(self): """Return the name of the sensor.""" return self._name + @property + def unique_id(self): + """Return the id of this plex client.""" + return self._unique_id + @property def state(self): """Return the state of the sensor.""" diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 6647b81714f..c778588752a 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -1,5 +1,6 @@ """Shared class to maintain Plex server instances.""" import logging + import plexapi.server from requests import Session