class PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Plex config flow.""" VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL @staticmethod @callback def async_get_options_flow(config_entry): """Get the options flow for this handler.""" return PlexOptionsFlowHandler(config_entry) def __init__(self): """Initialize the Plex flow.""" self.current_login = {} self.available_servers = None self.plexauth = None self.token = None self.client_id = None async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" return self.async_show_form(step_id="start_website_auth") async def async_step_start_website_auth(self, user_input=None): """Show a form before starting external authentication.""" return await self.async_step_plex_website_auth() async def async_step_server_validate(self, server_config): """Validate a provided configuration.""" errors = {} self.current_login = server_config plex_server = PlexServer(self.hass, server_config) try: await self.hass.async_add_executor_job(plex_server.connect) except NoServersFound: errors["base"] = "no_servers" except (plexapi.exceptions.BadRequest, plexapi.exceptions.Unauthorized): _LOGGER.error("Invalid credentials provided, config not created") errors["base"] = "faulty_credentials" except (plexapi.exceptions.NotFound, requests.exceptions.ConnectionError): server_identifier = (server_config.get(CONF_URL) or plex_server.server_choice or "Unknown") _LOGGER.error("Plex server could not be reached: %s", server_identifier) errors["base"] = "not_found" except ServerNotSpecified as available_servers: self.available_servers = available_servers.args[0] return await self.async_step_select_server() except Exception as error: # pylint: disable=broad-except _LOGGER.exception("Unknown error connecting to Plex server: %s", error) return self.async_abort(reason="unknown") if errors: return self.async_show_form(step_id="start_website_auth", errors=errors) server_id = plex_server.machine_identifier for entry in self._async_current_entries(): if entry.data[CONF_SERVER_IDENTIFIER] == server_id: return self.async_abort(reason="already_configured") url = plex_server.url_in_use token = server_config.get(CONF_TOKEN) entry_config = {CONF_URL: url} if self.client_id: entry_config[CONF_CLIENT_IDENTIFIER] = self.client_id if token: entry_config[CONF_TOKEN] = token if url.startswith("https"): entry_config[CONF_VERIFY_SSL] = server_config.get( CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL) _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, }, ) async def async_step_select_server(self, user_input=None): """Use selected Plex server.""" config = dict(self.current_login) if user_input is not None: config[CONF_SERVER] = user_input[CONF_SERVER] return await self.async_step_server_validate(config) configured = configured_servers(self.hass) available_servers = [ name for (name, server_id) in self.available_servers if server_id not in configured ] if not available_servers: return self.async_abort(reason="all_configured") if len(available_servers) == 1: config[CONF_SERVER] = available_servers[0] return await self.async_step_server_validate(config) return self.async_show_form( step_id="select_server", data_schema=vol.Schema( {vol.Required(CONF_SERVER): vol.In(available_servers)}), errors={}, ) async def async_step_discovery(self, discovery_info): """Set default host and port from discovery.""" if self._async_current_entries() or self._async_in_progress(): # Skip discovery if a config already exists or is in progress. return self.async_abort(reason="already_configured") json_file = self.hass.config.path(PLEX_CONFIG_FILE) file_config = await self.hass.async_add_executor_job( load_json, json_file) if file_config: host_and_port, host_config = file_config.popitem() prefix = "https" if host_config[CONF_SSL] else "http" server_config = { CONF_URL: f"{prefix}://{host_and_port}", CONF_TOKEN: host_config[CONF_TOKEN], CONF_VERIFY_SSL: host_config["verify"], } _LOGGER.info("Imported legacy config, file can be removed: %s", json_file) return await self.async_step_server_validate(server_config) return self.async_abort(reason="discovery_no_file") async def async_step_import(self, import_config): """Import from Plex configuration.""" _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 = 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 self.client_id = self.plexauth.client_identifier 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 PlexFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Plex config flow.""" VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH @staticmethod @callback def async_get_options_flow(config_entry): """Get the options flow for this handler.""" return PlexOptionsFlowHandler(config_entry) def __init__(self): """Initialize the Plex flow.""" self.current_login = {} self.available_servers = None self.plexauth = None 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): # pylint: disable=arguments-differ """Handle a flow initialized by the user.""" if user_input is not None: return await self.async_step_plex_website_auth() if self.show_advanced_options: return await self.async_step_user_advanced(errors=errors) return self.async_show_form(step_id="user", errors=errors) async def async_step_user_advanced(self, user_input=None, errors=None): """Handle an advanced mode flow initialized by the user.""" if user_input is not None: if user_input.get("setup_method") == MANUAL_SETUP_STRING: self._manual = True return await self.async_step_manual_setup() return await self.async_step_plex_website_auth() data_schema = vol.Schema({ vol.Required("setup_method", default=AUTOMATIC_SETUP_STRING): vol.In([AUTOMATIC_SETUP_STRING, MANUAL_SETUP_STRING]) }) return self.async_show_form(step_id="user_advanced", data_schema=data_schema, errors=errors) async def async_step_manual_setup(self, user_input=None, errors=None): """Begin manual configuration.""" if user_input is not None and errors is None: user_input.pop(CONF_URL, None) host = user_input.get(CONF_HOST) if host: port = user_input[CONF_PORT] prefix = "https" if user_input.get(CONF_SSL) else "http" user_input[CONF_URL] = f"{prefix}://{host}:{port}" elif CONF_TOKEN not in user_input: return await self.async_step_manual_setup( user_input=user_input, errors={"base": "host_or_token"}) return await self.async_step_server_validate(user_input) previous_input = user_input or {} data_schema = vol.Schema({ vol.Optional( CONF_HOST, description={"suggested_value": previous_input.get(CONF_HOST)}, ): str, vol.Required(CONF_PORT, default=previous_input.get(CONF_PORT, DEFAULT_PORT)): int, vol.Required(CONF_SSL, default=previous_input.get(CONF_SSL, DEFAULT_SSL)): bool, vol.Required( CONF_VERIFY_SSL, default=previous_input.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL), ): bool, vol.Optional( CONF_TOKEN, description={ "suggested_value": previous_input.get(CONF_TOKEN) }, ): str, }) return self.async_show_form(step_id="manual_setup", data_schema=data_schema, errors=errors) async def async_step_server_validate(self, server_config): """Validate a provided configuration.""" errors = {} self.current_login = server_config plex_server = PlexServer(self.hass, server_config) try: await self.hass.async_add_executor_job(plex_server.connect) except NoServersFound: _LOGGER.error("No servers linked to Plex account") errors["base"] = "no_servers" except (plexapi.exceptions.BadRequest, plexapi.exceptions.Unauthorized): _LOGGER.error("Invalid credentials provided, config not created") errors[CONF_TOKEN] = "faulty_credentials" except requests.exceptions.SSLError as error: _LOGGER.error("SSL certificate error: [%s]", error) errors["base"] = "ssl_error" except (plexapi.exceptions.NotFound, requests.exceptions.ConnectionError): server_identifier = (server_config.get(CONF_URL) or plex_server.server_choice or "Unknown") _LOGGER.error("Plex server could not be reached: %s", server_identifier) errors[CONF_HOST] = "not_found" except ServerNotSpecified as available_servers: self.available_servers = available_servers.args[0] return await self.async_step_select_server() except Exception as error: # pylint: disable=broad-except _LOGGER.exception("Unknown error connecting to Plex server: %s", error) return self.async_abort(reason="unknown") if errors: if self._manual: return await self.async_step_manual_setup( user_input=server_config, errors=errors) return await self.async_step_user(errors=errors) server_id = plex_server.machine_identifier url = plex_server.url_in_use token = server_config.get(CONF_TOKEN) entry_config = {CONF_URL: url} if self.client_id: entry_config[CONF_CLIENT_ID] = self.client_id if token: entry_config[CONF_TOKEN] = token if url.startswith("https"): entry_config[CONF_VERIFY_SSL] = server_config.get( 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=data) async def async_step_select_server(self, user_input=None): """Use selected Plex server.""" config = dict(self.current_login) if user_input is not None: config[CONF_SERVER] = user_input[CONF_SERVER] return await self.async_step_server_validate(config) configured = configured_servers(self.hass) available_servers = [ name for (name, server_id) in self.available_servers if server_id not in configured ] if not available_servers: return self.async_abort(reason="all_configured") if len(available_servers) == 1: config[CONF_SERVER] = available_servers[0] return await self.async_step_server_validate(config) return self.async_show_form( step_id="select_server", data_schema=vol.Schema( {vol.Required(CONF_SERVER): vol.In(available_servers)}), errors={}, ) async def async_step_integration_discovery(self, discovery_info): """Handle GDM discovery.""" machine_identifier = discovery_info["data"]["Resource-Identifier"] await self.async_set_unique_id(machine_identifier) self._abort_if_unique_id_configured() host = f"{discovery_info['from'][0]}:{discovery_info['data']['Port']}" name = discovery_info["data"]["Name"] self.context["title_placeholders"] = { # pylint: disable=no-member "host": host, "name": name, } return await self.async_step_user() 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"{get_url(self.hass)}{AUTH_CALLBACK_PATH}?flow_id={self.flow_id}" auth_url = 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 self.client_id = self.plexauth.client_identifier 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) 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()