Add support to reauthorize expired Plex tokens (#40469)
This commit is contained in:
parent
eda68f127c
commit
3596eb39f2
5 changed files with 116 additions and 18 deletions
|
@ -14,8 +14,10 @@ from homeassistant.components.media_player.const import (
|
||||||
ATTR_MEDIA_CONTENT_ID,
|
ATTR_MEDIA_CONTENT_ID,
|
||||||
ATTR_MEDIA_CONTENT_TYPE,
|
ATTR_MEDIA_CONTENT_TYPE,
|
||||||
)
|
)
|
||||||
|
from homeassistant.config_entries import SOURCE_REAUTH
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID,
|
ATTR_ENTITY_ID,
|
||||||
|
CONF_SOURCE,
|
||||||
CONF_URL,
|
CONF_URL,
|
||||||
CONF_VERIFY_SSL,
|
CONF_VERIFY_SSL,
|
||||||
EVENT_HOMEASSISTANT_STOP,
|
EVENT_HOMEASSISTANT_STOP,
|
||||||
|
@ -75,7 +77,11 @@ async def async_setup_entry(hass, entry):
|
||||||
hass.config_entries.async_update_entry(entry, options=options)
|
hass.config_entries.async_update_entry(entry, options=options)
|
||||||
|
|
||||||
plex_server = PlexServer(
|
plex_server = PlexServer(
|
||||||
hass, server_config, entry.data[CONF_SERVER_IDENTIFIER], entry.options
|
hass,
|
||||||
|
server_config,
|
||||||
|
entry.data[CONF_SERVER_IDENTIFIER],
|
||||||
|
entry.options,
|
||||||
|
entry.entry_id,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
await hass.async_add_executor_job(plex_server.connect)
|
await hass.async_add_executor_job(plex_server.connect)
|
||||||
|
@ -95,9 +101,21 @@ async def async_setup_entry(hass, entry):
|
||||||
error,
|
error,
|
||||||
)
|
)
|
||||||
raise ConfigEntryNotReady from error
|
raise ConfigEntryNotReady from error
|
||||||
|
except plexapi.exceptions.Unauthorized:
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.flow.async_init(
|
||||||
|
PLEX_DOMAIN,
|
||||||
|
context={CONF_SOURCE: SOURCE_REAUTH},
|
||||||
|
data={**entry.data, "config_entry_id": entry.entry_id},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_LOGGER.error(
|
||||||
|
"Token not accepted, please reauthenticate Plex server '%s'",
|
||||||
|
entry.data[CONF_SERVER],
|
||||||
|
)
|
||||||
|
return False
|
||||||
except (
|
except (
|
||||||
plexapi.exceptions.BadRequest,
|
plexapi.exceptions.BadRequest,
|
||||||
plexapi.exceptions.Unauthorized,
|
|
||||||
plexapi.exceptions.NotFound,
|
plexapi.exceptions.NotFound,
|
||||||
) as error:
|
) as error:
|
||||||
_LOGGER.error(
|
_LOGGER.error(
|
||||||
|
@ -207,7 +225,10 @@ async def async_unload_entry(hass, entry):
|
||||||
async def async_options_updated(hass, entry):
|
async def async_options_updated(hass, entry):
|
||||||
"""Triggered by config entry options updates."""
|
"""Triggered by config entry options updates."""
|
||||||
server_id = entry.data[CONF_SERVER_IDENTIFIER]
|
server_id = entry.data[CONF_SERVER_IDENTIFIER]
|
||||||
hass.data[PLEX_DOMAIN][SERVERS][server_id].options = entry.options
|
|
||||||
|
# Guard incomplete setup during reauth flows
|
||||||
|
if server_id in hass.data[PLEX_DOMAIN][SERVERS]:
|
||||||
|
hass.data[PLEX_DOMAIN][SERVERS][server_id].options = entry.options
|
||||||
|
|
||||||
|
|
||||||
def play_on_sonos(hass, service_call):
|
def play_on_sonos(hass, service_call):
|
||||||
|
|
|
@ -16,6 +16,7 @@ from homeassistant.const import (
|
||||||
CONF_CLIENT_ID,
|
CONF_CLIENT_ID,
|
||||||
CONF_HOST,
|
CONF_HOST,
|
||||||
CONF_PORT,
|
CONF_PORT,
|
||||||
|
CONF_SOURCE,
|
||||||
CONF_SSL,
|
CONF_SSL,
|
||||||
CONF_TOKEN,
|
CONF_TOKEN,
|
||||||
CONF_URL,
|
CONF_URL,
|
||||||
|
@ -70,7 +71,7 @@ async def async_discover(hass):
|
||||||
for server_data in gdm.entries:
|
for server_data in gdm.entries:
|
||||||
await hass.config_entries.flow.async_init(
|
await hass.config_entries.flow.async_init(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
context={CONF_SOURCE: config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||||
data=server_data,
|
data=server_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -95,6 +96,7 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
self.token = None
|
self.token = None
|
||||||
self.client_id = None
|
self.client_id = None
|
||||||
self._manual = False
|
self._manual = False
|
||||||
|
self._entry_id = None
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
self, user_input=None, errors=None
|
self, user_input=None, errors=None
|
||||||
|
@ -209,10 +211,6 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
return await self.async_step_user(errors=errors)
|
return await self.async_step_user(errors=errors)
|
||||||
|
|
||||||
server_id = plex_server.machine_identifier
|
server_id = plex_server.machine_identifier
|
||||||
|
|
||||||
await self.async_set_unique_id(server_id)
|
|
||||||
self._abort_if_unique_id_configured()
|
|
||||||
|
|
||||||
url = plex_server.url_in_use
|
url = plex_server.url_in_use
|
||||||
token = server_config.get(CONF_TOKEN)
|
token = server_config.get(CONF_TOKEN)
|
||||||
|
|
||||||
|
@ -226,16 +224,28 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL
|
CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
CONF_SERVER: plex_server.friendly_name,
|
||||||
|
CONF_SERVER_IDENTIFIER: server_id,
|
||||||
|
PLEX_SERVER_CONFIG: entry_config,
|
||||||
|
}
|
||||||
|
|
||||||
|
await self.async_set_unique_id(server_id)
|
||||||
|
if (
|
||||||
|
self.context[CONF_SOURCE] # pylint: disable=no-member
|
||||||
|
== config_entries.SOURCE_REAUTH
|
||||||
|
):
|
||||||
|
entry = self.hass.config_entries.async_get_entry(self._entry_id)
|
||||||
|
self.hass.config_entries.async_update_entry(entry, data=data)
|
||||||
|
_LOGGER.debug("Updated config entry for %s", plex_server.friendly_name)
|
||||||
|
await self.hass.config_entries.async_reload(entry.entry_id)
|
||||||
|
return self.async_abort(reason="reauth_successful")
|
||||||
|
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
_LOGGER.debug("Valid config created for %s", plex_server.friendly_name)
|
_LOGGER.debug("Valid config created for %s", plex_server.friendly_name)
|
||||||
|
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(title=plex_server.friendly_name, data=data)
|
||||||
title=plex_server.friendly_name,
|
|
||||||
data={
|
|
||||||
CONF_SERVER: plex_server.friendly_name,
|
|
||||||
CONF_SERVER_IDENTIFIER: server_id,
|
|
||||||
PLEX_SERVER_CONFIG: entry_config,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_step_select_server(self, user_input=None):
|
async def async_step_select_server(self, user_input=None):
|
||||||
"""Use selected Plex server."""
|
"""Use selected Plex server."""
|
||||||
|
@ -316,6 +326,12 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
server_config = {CONF_TOKEN: self.token}
|
server_config = {CONF_TOKEN: self.token}
|
||||||
return await self.async_step_server_validate(server_config)
|
return await self.async_step_server_validate(server_config)
|
||||||
|
|
||||||
|
async def async_step_reauth(self, data):
|
||||||
|
"""Handle a reauthorization flow request."""
|
||||||
|
self.current_login = dict(data)
|
||||||
|
self._entry_id = self.current_login.pop("config_entry_id")
|
||||||
|
return await self.async_step_user()
|
||||||
|
|
||||||
|
|
||||||
class PlexOptionsFlowHandler(config_entries.OptionsFlow):
|
class PlexOptionsFlowHandler(config_entries.OptionsFlow):
|
||||||
"""Handle Plex options."""
|
"""Handle Plex options."""
|
||||||
|
|
|
@ -62,9 +62,12 @@ plexapi.X_PLEX_VERSION = X_PLEX_VERSION
|
||||||
class PlexServer:
|
class PlexServer:
|
||||||
"""Manages a single Plex server connection."""
|
"""Manages a single Plex server connection."""
|
||||||
|
|
||||||
def __init__(self, hass, server_config, known_server_id=None, options=None):
|
def __init__(
|
||||||
|
self, hass, server_config, known_server_id=None, options=None, entry_id=None
|
||||||
|
):
|
||||||
"""Initialize a Plex server instance."""
|
"""Initialize a Plex server instance."""
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
|
self.entry_id = entry_id
|
||||||
self._plex_account = None
|
self._plex_account = None
|
||||||
self._plex_server = None
|
self._plex_server = None
|
||||||
self._created_clients = set()
|
self._created_clients = set()
|
||||||
|
@ -270,6 +273,12 @@ class PlexServer:
|
||||||
devices, sessions, plextv_clients = await self.hass.async_add_executor_job(
|
devices, sessions, plextv_clients = await self.hass.async_add_executor_job(
|
||||||
self._fetch_platform_data
|
self._fetch_platform_data
|
||||||
)
|
)
|
||||||
|
except plexapi.exceptions.Unauthorized:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Token has expired for '%s', reloading integration", self.friendly_name
|
||||||
|
)
|
||||||
|
await self.hass.config_entries.async_reload(self.entry_id)
|
||||||
|
return
|
||||||
except (
|
except (
|
||||||
plexapi.exceptions.BadRequest,
|
plexapi.exceptions.BadRequest,
|
||||||
requests.exceptions.RequestException,
|
requests.exceptions.RequestException,
|
||||||
|
|
|
@ -41,8 +41,9 @@
|
||||||
"all_configured": "All linked servers already configured",
|
"all_configured": "All linked servers already configured",
|
||||||
"already_configured": "This Plex server is already configured",
|
"already_configured": "This Plex server is already configured",
|
||||||
"already_in_progress": "Plex is being configured",
|
"already_in_progress": "Plex is being configured",
|
||||||
|
"reauth_successful": "Successfully reauthenticated",
|
||||||
"token_request_timeout": "Timed out obtaining token",
|
"token_request_timeout": "Timed out obtaining token",
|
||||||
"unknown": "Failed for unknown reason"
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
|
|
|
@ -24,6 +24,7 @@ from homeassistant.config import async_process_ha_core_config
|
||||||
from homeassistant.config_entries import (
|
from homeassistant.config_entries import (
|
||||||
ENTRY_STATE_LOADED,
|
ENTRY_STATE_LOADED,
|
||||||
SOURCE_INTEGRATION_DISCOVERY,
|
SOURCE_INTEGRATION_DISCOVERY,
|
||||||
|
SOURCE_REAUTH,
|
||||||
)
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_HOST,
|
CONF_HOST,
|
||||||
|
@ -723,3 +724,53 @@ async def test_integration_discovery(hass):
|
||||||
== mock_gdm.entries[0]["data"]["Resource-Identifier"]
|
== mock_gdm.entries[0]["data"]["Resource-Identifier"]
|
||||||
)
|
)
|
||||||
assert flow["step_id"] == "user"
|
assert flow["step_id"] == "user"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_trigger_reauth(hass, entry, mock_plex_server, mock_websocket):
|
||||||
|
"""Test setup and reauthorization of a Plex token."""
|
||||||
|
await async_process_ha_core_config(
|
||||||
|
hass,
|
||||||
|
{"internal_url": "http://example.local:8123"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert entry.state == ENTRY_STATE_LOADED
|
||||||
|
|
||||||
|
with patch.object(
|
||||||
|
mock_plex_server, "clients", side_effect=plexapi.exceptions.Unauthorized
|
||||||
|
), patch("plexapi.server.PlexServer", side_effect=plexapi.exceptions.Unauthorized):
|
||||||
|
trigger_plex_update(mock_websocket)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||||
|
assert entry.state != ENTRY_STATE_LOADED
|
||||||
|
|
||||||
|
flows = hass.config_entries.flow.async_progress()
|
||||||
|
assert len(flows) == 1
|
||||||
|
assert flows[0]["context"]["source"] == SOURCE_REAUTH
|
||||||
|
|
||||||
|
flow_id = flows[0]["flow_id"]
|
||||||
|
|
||||||
|
with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()), patch(
|
||||||
|
"plexapi.server.PlexServer", return_value=mock_plex_server
|
||||||
|
), patch("plexauth.PlexAuth.initiate_auth"), patch(
|
||||||
|
"plexauth.PlexAuth.token", return_value="BRAND_NEW_TOKEN"
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_configure(flow_id, user_input={})
|
||||||
|
assert result["type"] == "external"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||||
|
assert result["type"] == "external_done"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||||
|
assert result["type"] == "abort"
|
||||||
|
assert result["reason"] == "reauth_successful"
|
||||||
|
assert result["flow_id"] == flow_id
|
||||||
|
|
||||||
|
assert len(hass.config_entries.flow.async_progress()) == 0
|
||||||
|
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||||
|
|
||||||
|
assert entry.state == ENTRY_STATE_LOADED
|
||||||
|
assert entry.data[CONF_SERVER] == mock_plex_server.friendlyName
|
||||||
|
assert entry.data[CONF_SERVER_IDENTIFIER] == mock_plex_server.machineIdentifier
|
||||||
|
assert entry.data[PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server._baseurl
|
||||||
|
assert entry.data[PLEX_SERVER_CONFIG][CONF_TOKEN] == "BRAND_NEW_TOKEN"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue