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
This commit is contained in:
jjlawren 2019-09-09 16:28:20 -05:00 committed by Martin Hjelmare
parent 3c629db096
commit 30fb4ddc98
6 changed files with 238 additions and 231 deletions

View File

@ -36,6 +36,7 @@ SERVICE_KONNECTED = "konnected"
SERVICE_MOBILE_APP = "hass_mobile_app" SERVICE_MOBILE_APP = "hass_mobile_app"
SERVICE_NETGEAR = "netgear_router" SERVICE_NETGEAR = "netgear_router"
SERVICE_OCTOPRINT = "octoprint" SERVICE_OCTOPRINT = "octoprint"
SERVICE_PLEX = "plex_mediaserver"
SERVICE_ROKU = "roku" SERVICE_ROKU = "roku"
SERVICE_SABNZBD = "sabnzbd" SERVICE_SABNZBD = "sabnzbd"
SERVICE_SAMSUNG_PRINTER = "samsung_printer" SERVICE_SAMSUNG_PRINTER = "samsung_printer"
@ -68,7 +69,7 @@ SERVICE_HANDLERS = {
SERVICE_FREEBOX: ("freebox", None), SERVICE_FREEBOX: ("freebox", None),
SERVICE_YEELIGHT: ("yeelight", None), SERVICE_YEELIGHT: ("yeelight", None),
"panasonic_viera": ("media_player", "panasonic_viera"), "panasonic_viera": ("media_player", "panasonic_viera"),
"plex_mediaserver": ("media_player", "plex"), SERVICE_PLEX: ("plex", None),
"yamaha": ("media_player", "yamaha"), "yamaha": ("media_player", "yamaha"),
"logitech_mediaserver": ("media_player", "squeezebox"), "logitech_mediaserver": ("media_player", "squeezebox"),
"directv": ("media_player", "directv"), "directv": ("media_player", "directv"),

View File

@ -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)

View File

@ -7,7 +7,11 @@ DEFAULT_PORT = 32400
DEFAULT_SSL = False DEFAULT_SSL = False
DEFAULT_VERIFY_SSL = True DEFAULT_VERIFY_SSL = True
PLATFORMS = ["media_player", "sensor"]
SERVERS = "servers"
PLEX_CONFIG_FILE = "plex.conf" PLEX_CONFIG_FILE = "plex.conf"
PLEX_MEDIA_PLAYER_OPTIONS = "plex_mp_options"
PLEX_SERVER_CONFIG = "server_config" PLEX_SERVER_CONFIG = "server_config"
CONF_USE_EPISODE_ART = "use_episode_art" CONF_USE_EPISODE_ART = "use_episode_art"

View File

@ -2,10 +2,13 @@
from datetime import timedelta from datetime import timedelta
import json import json
import logging 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 ( from homeassistant.components.media_player.const import (
MEDIA_TYPE_MOVIE, MEDIA_TYPE_MOVIE,
MEDIA_TYPE_MUSIC, MEDIA_TYPE_MUSIC,
@ -20,150 +23,37 @@ from homeassistant.components.media_player.const import (
SUPPORT_VOLUME_SET, SUPPORT_VOLUME_SET,
) )
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST,
CONF_PORT,
CONF_SSL,
CONF_URL,
CONF_TOKEN,
CONF_VERIFY_SSL,
DEVICE_DEFAULT_NAME, DEVICE_DEFAULT_NAME,
STATE_IDLE, STATE_IDLE,
STATE_OFF, STATE_OFF,
STATE_PAUSED, STATE_PAUSED,
STATE_PLAYING, STATE_PLAYING,
) )
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.event import track_time_interval 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 .const import ( from .const import (
CONF_USE_EPISODE_ART, CONF_USE_EPISODE_ART,
CONF_SHOW_ALL_CONTROLS, 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, DOMAIN as PLEX_DOMAIN,
NAME_FORMAT, NAME_FORMAT,
PLEX_CONFIG_FILE, PLEX_MEDIA_PLAYER_OPTIONS,
SERVERS,
) )
from .server import PlexServer
SERVER_SETUP = "server_setup" SERVER_SETUP = "server_setup"
_CONFIGURING = {} _CONFIGURING = {}
_LOGGER = logging.getLogger(__name__) _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): def setup_platform(hass, config, add_entities_callback, discovery_info=None):
"""Set up the Plex platform.""" """Set up the Plex platform."""
plex_data = hass.data.setdefault(PLEX_DOMAIN, {}) if discovery_info is None:
server_setup = plex_data.setdefault(SERVER_SETUP, False)
if server_setup:
return return
# get config from plex.conf plexserver = list(hass.data[PLEX_DOMAIN][SERVERS].values())[0]
file_config = load_json(hass.config.path(PLEX_CONFIG_FILE)) config = hass.data[PLEX_MEDIA_PLAYER_OPTIONS]
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}},
)
plex_clients = {} plex_clients = {}
plex_sessions = {} plex_sessions = {}
@ -178,7 +68,9 @@ def setup_plexserver(
return return
except requests.exceptions.RequestException as ex: except requests.exceptions.RequestException as ex:
_LOGGER.warning( _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 return
@ -210,7 +102,9 @@ def setup_plexserver(
return return
except requests.exceptions.RequestException as ex: except requests.exceptions.RequestException as ex:
_LOGGER.warning( _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 return
@ -239,7 +133,6 @@ def setup_plexserver(
_LOGGER.debug("Refreshing session: %s", machine_identifier) _LOGGER.debug("Refreshing session: %s", machine_identifier)
plex_clients[machine_identifier].refresh(None, session) plex_clients[machine_identifier].refresh(None, session)
clients_to_remove = []
for client in plex_clients.values(): for client in plex_clients.values():
# force devices to idle that do not have a valid session # force devices to idle that do not have a valid session
if client.session is None: if client.session is None:
@ -253,59 +146,10 @@ def setup_plexserver(
if client not in new_plex_clients: if client not in new_plex_clients:
client.schedule_update_ha_state() 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: if new_plex_clients:
add_entities_callback(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): class PlexClient(MediaPlayerDevice):
"""Representation of a Plex device.""" """Representation of a Plex device."""
@ -378,9 +222,6 @@ class PlexClient(MediaPlayerDevice):
def refresh(self, device, session): def refresh(self, device, session):
"""Refresh key device data.""" """Refresh key device data."""
import plexapi.exceptions
# new data refresh
self._clear_media_details() self._clear_media_details()
if session: # Not being triggered by Chrome or FireTablet Plex App if session: # Not being triggered by Chrome or FireTablet Plex App
@ -851,8 +692,6 @@ class PlexClient(MediaPlayerDevice):
src["video_name"] src["video_name"]
) )
import plexapi.playlist
if ( if (
media media
and media_type == "EPISODE" and media_type == "EPISODE"
@ -918,8 +757,6 @@ class PlexClient(MediaPlayerDevice):
_LOGGER.error("Client cannot play media: %s", self.entity_id) _LOGGER.error("Client cannot play media: %s", self.entity_id)
return return
import plexapi.playqueue
playqueue = plexapi.playqueue.PlayQueue.create( playqueue = plexapi.playqueue.PlayQueue.create(
self.device.server, media, **params self.device.server, media, **params
) )

View File

@ -1,87 +1,51 @@
"""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 plexapi.exceptions
import requests.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.helpers.entity import Entity
from homeassistant.util import Throttle 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 .const import DOMAIN as PLEX_DOMAIN, SERVERS
from .server import PlexServer
DEFAULT_NAME = "Plex" DEFAULT_NAME = "Plex"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1) 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): 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) if discovery_info is None:
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)
return 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): class PlexSensor(Entity):
"""Representation of a Plex now playing sensor.""" """Representation of a Plex now playing sensor."""
def __init__(self, name, plex_server): def __init__(self, plex_server):
"""Initialize the sensor.""" """Initialize the sensor."""
self._name = name self._name = DEFAULT_NAME
self._state = None self._state = None
self._now_playing = [] self._now_playing = []
self._server = plex_server self._server = plex_server
self._unique_id = f"sensor-{plex_server.machine_identifier}"
@property @property
def name(self): def name(self):
"""Return the name of the sensor.""" """Return the name of the sensor."""
return self._name return self._name
@property
def unique_id(self):
"""Return the id of this plex client."""
return self._unique_id
@property @property
def state(self): def state(self):
"""Return the state of the sensor.""" """Return the state of the sensor."""

View File

@ -1,5 +1,6 @@
"""Shared class to maintain Plex server instances.""" """Shared class to maintain Plex server instances."""
import logging import logging
import plexapi.server import plexapi.server
from requests import Session from requests import Session