Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plex external config flow #26936

Merged
merged 15 commits into from
Oct 1, 2019
80 changes: 74 additions & 6 deletions homeassistant/components/plex/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@
import copy
import logging

from aiohttp import web_response
import plexapi.exceptions
from plexauth import PlexAuth
import requests.exceptions
import voluptuous as vol

from homeassistant.components.http.view import HomeAssistantView
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant import config_entries
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
from homeassistant.const import (
Expand All @@ -20,6 +24,8 @@
from homeassistant.util.json import load_json

from .const import ( # pylint: disable=unused-import
AUTH_CALLBACK_NAME,
AUTH_CALLBACK_PATH,
CONF_SERVER,
CONF_SERVER_IDENTIFIER,
CONF_USE_EPISODE_ART,
Expand All @@ -30,13 +36,15 @@
DOMAIN,
PLEX_CONFIG_FILE,
PLEX_SERVER_CONFIG,
X_PLEX_DEVICE_NAME,
X_PLEX_VERSION,
X_PLEX_PRODUCT,
X_PLEX_PLATFORM,
)
from .errors import NoServersFound, ServerNotSpecified
from .server import PlexServer

USER_SCHEMA = vol.Schema(
{vol.Optional(CONF_TOKEN): str, vol.Optional("manual_setup"): bool}
)
USER_SCHEMA = vol.Schema({vol.Optional("manual_setup"): bool})

_LOGGER = logging.getLogger(__package__)

Expand Down Expand Up @@ -67,16 +75,17 @@ def __init__(self):
self.current_login = {}
self.discovery_info = {}
self.available_servers = None
self.plexauth = None
self.token = None

async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
errors = {}
if user_input is not None:
if user_input.pop("manual_setup", False):
return await self.async_step_manual_setup(user_input)
if CONF_TOKEN in user_input:
return await self.async_step_server_validate(user_input)
errors[CONF_TOKEN] = "no_token"

return await self.async_step_plex_website_auth()

return self.async_show_form(
step_id="user", data_schema=USER_SCHEMA, errors=errors
Expand Down Expand Up @@ -225,6 +234,45 @@ async def async_step_import(self, import_config):
_LOGGER.debug("Imported Plex configuration")
return await self.async_step_server_validate(import_config)

async def async_step_plex_website_auth(self):
"""Begin external auth flow on Plex website."""
self.hass.http.register_view(PlexAuthorizationCallbackView)
payload = {
"X-Plex-Device-Name": X_PLEX_DEVICE_NAME,
"X-Plex-Version": X_PLEX_VERSION,
"X-Plex-Product": X_PLEX_PRODUCT,
"X-Plex-Device": self.hass.config.location_name,
"X-Plex-Platform": X_PLEX_PLATFORM,
"X-Plex-Model": "Plex OAuth",
}
session = async_get_clientsession(self.hass)
self.plexauth = PlexAuth(payload, session)
await self.plexauth.initiate_auth()
forward_url = f"{self.hass.config.api.base_url}{AUTH_CALLBACK_PATH}?flow_id={self.flow_id}"
auth_url = await self.hass.async_add_executor_job(
jjlawren marked this conversation as resolved.
Show resolved Hide resolved
self.plexauth.auth_url, forward_url
)
return self.async_external_step(step_id="obtain_token", url=auth_url)

async def async_step_obtain_token(self, user_input=None):
"""Obtain token after external auth completed."""
token = await self.plexauth.token(10)

if not token:
return self.async_external_step_done(next_step_id="timed_out")

self.token = token
return self.async_external_step_done(next_step_id="use_external_token")

async def async_step_timed_out(self, user_input=None):
"""Abort flow when time expires."""
return self.async_abort(reason="token_request_timeout")

async def async_step_use_external_token(self, user_input=None):
"""Continue server validation with external token."""
server_config = {CONF_TOKEN: self.token}
return await self.async_step_server_validate(server_config)


class PlexOptionsFlowHandler(config_entries.OptionsFlow):
"""Handle Plex options."""
Expand Down Expand Up @@ -263,3 +311,23 @@ async def async_step_plex_mp_settings(self, user_input=None):
}
),
)


class PlexAuthorizationCallbackView(HomeAssistantView):
"""Handle callback from external auth."""

url = AUTH_CALLBACK_PATH
name = AUTH_CALLBACK_NAME
requires_auth = False

async def get(self, request):
"""Receive authorization confirmation."""
hass = request.app["hass"]
await hass.config_entries.flow.async_configure(
flow_id=request.query["flow_id"], user_input=None
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
)

return web_response.Response(
headers={"content-type": "text/html"},
text="<script>window.close()</script>Success! This window can be closed",
)
10 changes: 10 additions & 0 deletions homeassistant/components/plex/const.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
"""Constants for the Plex component."""
from homeassistant.const import __version__

DOMAIN = "plex"
NAME_FORMAT = "Plex {}"

Expand All @@ -18,3 +20,11 @@
CONF_SERVER_IDENTIFIER = "server_id"
CONF_USE_EPISODE_ART = "use_episode_art"
CONF_SHOW_ALL_CONTROLS = "show_all_controls"

AUTH_CALLBACK_PATH = "/auth/plex/callback"
AUTH_CALLBACK_NAME = "auth:plex:callback"

X_PLEX_DEVICE_NAME = "Home Assistant"
X_PLEX_PLATFORM = "Home Assistant"
X_PLEX_PRODUCT = "Home Assistant"
X_PLEX_VERSION = __version__
3 changes: 2 additions & 1 deletion homeassistant/components/plex/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/components/plex",
"requirements": [
"plexapi==3.0.6"
"plexapi==3.0.6",
"plexauth==0.0.4"
],
"dependencies": [],
"codeowners": [
Expand Down
12 changes: 12 additions & 0 deletions homeassistant/components/plex/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,21 @@
CONF_SHOW_ALL_CONTROLS,
CONF_USE_EPISODE_ART,
DEFAULT_VERIFY_SSL,
X_PLEX_DEVICE_NAME,
X_PLEX_PLATFORM,
X_PLEX_PRODUCT,
X_PLEX_VERSION,
)
from .errors import NoServersFound, ServerNotSpecified

# Set default headers sent by plexapi
plexapi.X_PLEX_DEVICE_NAME = X_PLEX_DEVICE_NAME
plexapi.X_PLEX_PLATFORM = X_PLEX_PLATFORM
plexapi.X_PLEX_PRODUCT = X_PLEX_PRODUCT
plexapi.X_PLEX_VERSION = X_PLEX_VERSION
plexapi.myplex.BASE_HEADERS = plexapi.reset_base_headers()
plexapi.server.BASE_HEADERS = plexapi.reset_base_headers()


class PlexServer:
"""Manages a single Plex server connection."""
Expand Down
7 changes: 3 additions & 4 deletions homeassistant/components/plex/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,24 +21,23 @@
},
"user": {
"title": "Connect Plex server",
"description": "Enter a Plex token for automatic setup or manually configure a server.",
"description": "Continue to authorize at plex.tv or manually configure a server.",
"data": {
"token": "Plex token",
"manual_setup": "Manual setup"
}
}
},
"error": {
"faulty_credentials": "Authorization failed",
"no_servers": "No servers linked to account",
"not_found": "Plex server not found",
"no_token": "Provide a token or select manual setup"
"not_found": "Plex server not found"
},
"abort": {
"all_configured": "All linked servers already configured",
"already_configured": "This Plex server is already configured",
"already_in_progress": "Plex is being configured",
"invalid_import": "Imported configuration is invalid",
"token_request_timeout": "Timed out obtaining token",
"unknown": "Failed for unknown reason"
}
},
Expand Down
3 changes: 3 additions & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -961,6 +961,9 @@ pizzapi==0.0.3
# homeassistant.components.plex
plexapi==3.0.6

# homeassistant.components.plex
plexauth==0.0.4

# homeassistant.components.plum_lightpad
plumlightpad==0.0.11

Expand Down
3 changes: 3 additions & 0 deletions requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,9 @@ pillow==6.1.0
# homeassistant.components.plex
plexapi==3.0.6

# homeassistant.components.plex
plexauth==0.0.4

# homeassistant.components.mhz19
# homeassistant.components.serial_pm
pmsensor==0.4
Expand Down
1 change: 1 addition & 0 deletions script/gen_requirements_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@
"pilight",
"pillow",
"plexapi",
"plexauth",
"pmsensor",
"prometheus_client",
"ptvsd",
Expand Down
4 changes: 2 additions & 2 deletions tests/components/plex/mock_classes.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
"""Mock classes used in tests."""

MOCK_HOST_1 = "1.2.3.4"
MOCK_PORT_1 = "32400"
MOCK_PORT_1 = 32400
MOCK_HOST_2 = "4.3.2.1"
MOCK_PORT_2 = "32400"
MOCK_PORT_2 = 32400


class MockAvailableServer: # pylint: disable=too-few-public-methods
Expand Down
Loading