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_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"),

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_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"

View file

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

View file

@ -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."""

View file

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