diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 4e5abad4f79..fff6dea3bb4 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -14,8 +14,10 @@ from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, ) +from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import ( ATTR_ENTITY_ID, + CONF_SOURCE, CONF_URL, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP, @@ -75,7 +77,11 @@ async def async_setup_entry(hass, entry): hass.config_entries.async_update_entry(entry, options=options) 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: await hass.async_add_executor_job(plex_server.connect) @@ -95,9 +101,21 @@ async def async_setup_entry(hass, entry): 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 ( plexapi.exceptions.BadRequest, - plexapi.exceptions.Unauthorized, plexapi.exceptions.NotFound, ) as error: _LOGGER.error( @@ -207,7 +225,10 @@ async def async_unload_entry(hass, entry): async def async_options_updated(hass, entry): """Triggered by config entry options updates.""" 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): diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index ffadba63d3a..b2bf856402e 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -16,6 +16,7 @@ from homeassistant.const import ( CONF_CLIENT_ID, CONF_HOST, CONF_PORT, + CONF_SOURCE, CONF_SSL, CONF_TOKEN, CONF_URL, @@ -70,7 +71,7 @@ async def async_discover(hass): for server_data in gdm.entries: await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY}, + context={CONF_SOURCE: config_entries.SOURCE_INTEGRATION_DISCOVERY}, data=server_data, ) @@ -95,6 +96,7 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): self.token = None self.client_id = None self._manual = False + self._entry_id = None async def async_step_user( 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) 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 token = server_config.get(CONF_TOKEN) @@ -226,16 +224,28 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): 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) - return self.async_create_entry( - title=plex_server.friendly_name, - data={ - CONF_SERVER: plex_server.friendly_name, - CONF_SERVER_IDENTIFIER: server_id, - PLEX_SERVER_CONFIG: entry_config, - }, - ) + return self.async_create_entry(title=plex_server.friendly_name, data=data) async def async_step_select_server(self, user_input=None): """Use selected Plex server.""" @@ -316,6 +326,12 @@ class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): server_config = {CONF_TOKEN: self.token} 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): """Handle Plex options.""" diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index f8706eadf22..a5ac287328e 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -62,9 +62,12 @@ plexapi.X_PLEX_VERSION = X_PLEX_VERSION class PlexServer: """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.""" self.hass = hass + self.entry_id = entry_id self._plex_account = None self._plex_server = None self._created_clients = set() @@ -270,6 +273,12 @@ class PlexServer: devices, sessions, plextv_clients = await self.hass.async_add_executor_job( 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 ( plexapi.exceptions.BadRequest, requests.exceptions.RequestException, diff --git a/homeassistant/components/plex/strings.json b/homeassistant/components/plex/strings.json index 2f50e2d3090..1f9226ff776 100644 --- a/homeassistant/components/plex/strings.json +++ b/homeassistant/components/plex/strings.json @@ -41,8 +41,9 @@ "all_configured": "All linked servers already configured", "already_configured": "This Plex server is already configured", "already_in_progress": "Plex is being configured", + "reauth_successful": "Successfully reauthenticated", "token_request_timeout": "Timed out obtaining token", - "unknown": "Failed for unknown reason" + "unknown": "[%key:common::config_flow::error::unknown%]" } }, "options": { diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index 6b64b2f8571..476c342f176 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -24,6 +24,7 @@ from homeassistant.config import async_process_ha_core_config from homeassistant.config_entries import ( ENTRY_STATE_LOADED, SOURCE_INTEGRATION_DISCOVERY, + SOURCE_REAUTH, ) from homeassistant.const import ( CONF_HOST, @@ -723,3 +724,53 @@ async def test_integration_discovery(hass): == mock_gdm.entries[0]["data"]["Resource-Identifier"] ) 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"