class AlexaMediaFlowHandler(config_entries.ConfigFlow): """Handle a Alexa Media config flow.""" VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL proxy: AlexaProxy = None proxy_view: "AlexaMediaAuthorizationProxyView" = None def _update_ord_dict(self, old_dict: OrderedDict, new_dict: dict) -> OrderedDict: result: OrderedDict = OrderedDict() for k, v in old_dict.items(): for key, value in new_dict.items(): if k == key: result.update([(key, value)]) break if k not in result: result.update([(k, v)]) return result def __init__(self): """Initialize the config flow.""" if self.hass and not self.hass.data.get(DATA_ALEXAMEDIA): _LOGGER.info(STARTUP) _LOGGER.info("Loaded alexapy==%s", alexapy_version) self.login = None self.securitycode: Optional[str] = None self.automatic_steps: int = 0 self.config = OrderedDict() self.proxy_schema = None self.data_schema = OrderedDict([ (vol.Required(CONF_EMAIL), str), (vol.Required(CONF_PASSWORD), str), (vol.Required(CONF_URL, default="amazon.com"), str), (vol.Optional(CONF_SECURITYCODE), str), (vol.Optional(CONF_OTPSECRET), str), (vol.Optional(CONF_DEBUG, default=False), bool), (vol.Optional(CONF_INCLUDE_DEVICES, default=""), str), (vol.Optional(CONF_EXCLUDE_DEVICES, default=""), str), (vol.Optional(CONF_SCAN_INTERVAL, default=60), int), ]) self.totp_register = OrderedDict([(vol.Optional(CONF_TOTP_REGISTER, default=False), bool)]) async def async_step_import(self, import_config): """Import a config entry from configuration.yaml.""" return await self.async_step_user_legacy(import_config) async def async_step_user(self, user_input=None): """Provide a proxy for login.""" self._save_user_input_to_config(user_input=user_input) try: hass_url: str = get_url(self.hass, prefer_external=True) except NoURLAvailableError: hass_url = "" self.proxy_schema = OrderedDict([ ( vol.Required(CONF_EMAIL, default=self.config.get(CONF_EMAIL, "")), str, ), ( vol.Required(CONF_PASSWORD, default=self.config.get(CONF_PASSWORD, "")), str, ), ( vol.Required(CONF_URL, default=self.config.get(CONF_URL, "amazon.com")), str, ), ( vol.Required( CONF_HASS_URL, default=self.config.get(CONF_HASS_URL, hass_url), ), str, ), ( vol.Optional(CONF_OTPSECRET, default=self.config.get(CONF_OTPSECRET, "")), str, ), ( vol.Optional(CONF_DEBUG, default=self.config.get(CONF_DEBUG, False)), bool, ), ( vol.Optional( CONF_INCLUDE_DEVICES, default=self.config.get(CONF_INCLUDE_DEVICES, ""), ), str, ), ( vol.Optional( CONF_EXCLUDE_DEVICES, default=self.config.get(CONF_EXCLUDE_DEVICES, ""), ), str, ), ( vol.Optional( CONF_SCAN_INTERVAL, default=self.config.get(CONF_SCAN_INTERVAL, 60), ), int, ), ]) if not user_input: return self.async_show_form( step_id="user", data_schema=vol.Schema(self.proxy_schema), description_placeholders={"message": ""}, ) if self.login is None: try: self.login = self.hass.data[DATA_ALEXAMEDIA]["accounts"][ self.config[CONF_EMAIL]].get("login_obj") except KeyError: self.login = None try: if not self.login or self.login.session.closed: _LOGGER.debug("Creating new login") uuid_dict = await calculate_uuid(self.hass, self.config.get(CONF_EMAIL), self.config[CONF_URL]) uuid = uuid_dict["uuid"] self.login = AlexaLogin( url=self.config[CONF_URL], email=self.config.get(CONF_EMAIL, ""), password=self.config.get(CONF_PASSWORD, ""), outputpath=self.hass.config.path, debug=self.config[CONF_DEBUG], otp_secret=self.config.get(CONF_OTPSECRET, ""), oauth=self.config.get(CONF_OAUTH, {}), uuid=uuid, oauth_login=True, ) else: _LOGGER.debug("Using existing login") if self.config.get(CONF_EMAIL): self.login.email = self.config.get(CONF_EMAIL) if self.config.get(CONF_PASSWORD): self.login.password = self.config.get(CONF_PASSWORD) if self.config.get(CONF_OTPSECRET): self.login.set_totp(self.config.get(CONF_OTPSECRET, "")) except AlexapyPyotpInvalidKey: return self.async_show_form( step_id="user", errors={"base": "2fa_key_invalid"}, description_placeholders={"message": ""}, ) hass_url: str = user_input.get(CONF_HASS_URL) hass_url_valid: bool = False async with ClientSession() as session: try: async with session.get(hass_url) as resp: hass_url_valid = resp.status == 200 except ClientConnectionError: hass_url_valid = False if not hass_url_valid: _LOGGER.debug( "Unable to connect to provided Home Assistant url: %s", hass_url) return self.async_show_form( step_id="user", errors={"base": "hass_url_invalid"}, description_placeholders={"message": ""}, ) if not self.proxy: self.proxy = AlexaProxy( self.login, str(URL(hass_url).with_path(AUTH_PROXY_PATH))) # Swap the login object self.proxy.change_login(self.login) if (user_input and user_input.get(CONF_OTPSECRET) and user_input.get(CONF_OTPSECRET).replace(" ", "")): otp: str = self.login.get_totp_token() if otp: _LOGGER.debug("Generating OTP from %s", otp) return self.async_show_form( step_id="totp_register", data_schema=vol.Schema(self.totp_register), errors={}, description_placeholders={ "email": self.login.email, "url": self.login.url, "message": otp, }, ) return await self.async_step_start_proxy(user_input) async def async_step_start_proxy(self, user_input=None): """Start proxy for login.""" _LOGGER.debug( "Starting proxy for %s - %s", hide_email(self.login.email), self.login.url, ) if not self.proxy_view: self.proxy_view = AlexaMediaAuthorizationProxyView( self.proxy.all_handler) else: _LOGGER.debug("Found existing proxy_view") self.proxy_view.handler = self.proxy.all_handler self.hass.http.register_view(AlexaMediaAuthorizationCallbackView()) self.hass.http.register_view(self.proxy_view) callback_url = (URL( self.config["hass_url"]).with_path(AUTH_CALLBACK_PATH).with_query( {"flow_id": self.flow_id})) proxy_url = self.proxy.access_url().with_query({ "config_flow_id": self.flow_id, "callback_url": str(callback_url) }) self.login._session.cookie_jar.clear() return self.async_external_step(step_id="check_proxy", url=str(proxy_url)) async def async_step_check_proxy(self, user_input=None): """Check status of proxy for login.""" _LOGGER.debug( "Checking proxy response for %s - %s", hide_email(self.login.email), self.login.url, ) self.proxy_view.reset() return self.async_external_step_done(next_step_id="finish_proxy") async def async_step_finish_proxy(self, user_input=None): """Finish auth.""" if await self.login.test_loggedin(): await self.login.finalize_login() self.config[CONF_EMAIL] = self.login.email self.config[CONF_PASSWORD] = self.login.password return await self._test_login() return self.async_abort(reason="login_failed") async def async_step_user_legacy(self, user_input=None): """Handle legacy input for the config flow.""" # pylint: disable=too-many-return-statements self._save_user_input_to_config(user_input=user_input) self.data_schema = self._update_schema_defaults() if not user_input: self.automatic_steps = 0 return self.async_show_form( step_id="user", data_schema=vol.Schema(self.data_schema), description_placeholders={"message": ""}, ) if (not self.config.get("reauth") and f"{self.config[CONF_EMAIL]} - {self.config[CONF_URL]}" in configured_instances(self.hass) and not self.hass.data[DATA_ALEXAMEDIA]["config_flows"].get( f"{self.config[CONF_EMAIL]} - {self.config[CONF_URL]}")): _LOGGER.debug("Existing account found") self.automatic_steps = 0 return self.async_show_form( step_id="user", data_schema=vol.Schema(self.data_schema), errors={CONF_EMAIL: "identifier_exists"}, description_placeholders={"message": ""}, ) if self.login is None: try: self.login = self.hass.data[DATA_ALEXAMEDIA]["accounts"][ self.config[CONF_EMAIL]].get("login_obj") except KeyError: self.login = None try: if not self.login or self.login.session.closed: _LOGGER.debug("Creating new login") uuid_dict = await calculate_uuid(self.hass, self.config.get(CONF_EMAIL), self.config[CONF_URL]) uuid = uuid_dict["uuid"] self.login = AlexaLogin( url=self.config[CONF_URL], email=self.config[CONF_EMAIL], password=self.config[CONF_PASSWORD], outputpath=self.hass.config.path, debug=self.config[CONF_DEBUG], otp_secret=self.config.get(CONF_OTPSECRET, ""), uuid=uuid, oauth_login=True, ) else: _LOGGER.debug("Using existing login") if (not self.config.get("reauth") and user_input and user_input.get(CONF_OTPSECRET) and user_input.get(CONF_OTPSECRET).replace(" ", "")): otp: str = self.login.get_totp_token() if otp: _LOGGER.debug("Generating OTP from %s", otp) return self.async_show_form( step_id="totp_register", data_schema=vol.Schema(self.totp_register), errors={}, description_placeholders={ "email": self.login.email, "url": self.login.url, "message": otp, }, ) return self.async_show_form( step_id="user", errors={"base": "2fa_key_invalid"}, description_placeholders={"message": ""}, ) if self.login.status: _LOGGER.debug("Resuming existing flow") return await self._test_login() _LOGGER.debug("Trying to login %s", self.login.status) await self.login.login(data=self.config, ) return await self._test_login() except AlexapyConnectionError: self.automatic_steps = 0 return self.async_show_form( step_id="user_legacy", errors={"base": "connection_error"}, description_placeholders={"message": ""}, ) except AlexapyPyotpInvalidKey: self.automatic_steps = 0 return self.async_show_form( step_id="user_legacy", errors={"base": "2fa_key_invalid"}, description_placeholders={"message": ""}, ) except BaseException as ex: # pylyint: disable=broad-except _LOGGER.warning("Unknown error: %s", ex) if self.config[CONF_DEBUG]: raise self.automatic_steps = 0 return self.async_show_form( step_id="user_legacy", errors={"base": "unknown_error"}, description_placeholders={"message": ""}, ) async def async_step_totp_register(self, user_input=None): """Handle the input processing of the config flow.""" self._save_user_input_to_config(user_input=user_input) if user_input and user_input.get("registered") is False: _LOGGER.debug("Not registered, regenerating") otp: str = self.login.get_totp_token() if otp: _LOGGER.debug("Generating OTP from %s", otp) return self.async_show_form( step_id="totp_register", data_schema=vol.Schema(self.totp_register), errors={}, description_placeholders={ "email": self.login.email, "url": self.login.url, "message": otp, }, ) return await self.async_step_start_proxy(user_input) async def async_step_process(self, step_id, user_input=None): """Handle the input processing of the config flow.""" _LOGGER.debug( "Processing input for %s: %s", step_id, obfuscate(user_input), ) self._save_user_input_to_config(user_input=user_input) if user_input: return await self.async_step_user(user_input=None) return await self._test_login() async def async_step_reauth(self, user_input=None): """Handle reauth processing for the config flow.""" self._save_user_input_to_config(user_input) self.config["reauth"] = True reauth_schema = self._update_schema_defaults() _LOGGER.debug( "Creating reauth form with %s", obfuscate(self.config), ) self.automatic_steps = 0 if self.login is None: try: self.login = self.hass.data[DATA_ALEXAMEDIA]["accounts"][ self.config[CONF_EMAIL]].get("login_obj") except KeyError: self.login = None seconds_since_login: int = (( datetime.datetime.now() - self.login.stats["login_timestamp"]).seconds if self.login else 60) if seconds_since_login < 60: _LOGGER.debug( "Relogin requested within %s seconds; manual login required", seconds_since_login, ) return self.async_show_form( step_id="user", data_schema=vol.Schema(reauth_schema), description_placeholders={"message": "REAUTH"}, ) _LOGGER.debug("Attempting automatic relogin") await sleep(15) return await self.async_step_user_legacy(self.config) async def _test_login(self): # pylint: disable=too-many-statements, too-many-return-statements login = self.login email = login.email _LOGGER.debug("Testing login status: %s", login.status) if login.status and login.status.get("login_successful"): existing_entry = await self.async_set_unique_id( f"{email} - {login.url}") if self.config.get("reauth"): self.config.pop("reauth") if self.config.get(CONF_SECURITYCODE): self.config.pop(CONF_SECURITYCODE) if self.config.get("hass_url"): self.config.pop("hass_url") self.config[CONF_OAUTH] = { "access_token": login.access_token, "refresh_token": login.refresh_token, "expires_in": login.expires_in, "mac_dms": login.mac_dms } self.hass.data.setdefault( DATA_ALEXAMEDIA, { "accounts": {}, "config_flows": {}, "notify_service": None }, ) self.hass.data[DATA_ALEXAMEDIA].setdefault("accounts", {}) self.hass.data[DATA_ALEXAMEDIA].setdefault("config_flows", {}) if existing_entry: self.hass.config_entries.async_update_entry(existing_entry, data=self.config) _LOGGER.debug("Reauth successful for %s", hide_email(email)) self.hass.bus.async_fire( "alexa_media_relogin_success", event_data={ "email": hide_email(email), "url": login.url }, ) self.hass.components.persistent_notification.async_dismiss( f"alexa_media_{slugify(email)}{slugify(login.url[7:])}") if not self.hass.data[DATA_ALEXAMEDIA]["accounts"].get( self.config[CONF_EMAIL]): self.hass.data[DATA_ALEXAMEDIA]["accounts"][ self.config[CONF_EMAIL]] = {} self.hass.data[DATA_ALEXAMEDIA]["accounts"][ self.config[CONF_EMAIL]]["login_obj"] = self.login self.hass.data[DATA_ALEXAMEDIA]["config_flows"][ f"{email} - {login.url}"] = None return self.async_abort(reason="reauth_successful") _LOGGER.debug("Setting up Alexa devices with %s", dict(obfuscate(self.config))) self._abort_if_unique_id_configured(self.config) return self.async_create_entry( title=f"{login.email} - {login.url}", data=self.config) if login.status and login.status.get("securitycode_required"): _LOGGER.debug( "Creating config_flow to request 2FA. Saved security code %s", self.securitycode, ) generated_securitycode: str = login.get_totp_token() if (self.securitycode or generated_securitycode) and self.automatic_steps < 2: if self.securitycode: _LOGGER.debug("Automatically submitting securitycode %s", self.securitycode) else: _LOGGER.debug( "Automatically submitting generated securitycode %s", generated_securitycode, ) self.automatic_steps += 1 await sleep(5) if generated_securitycode: return await self.async_step_twofactor( user_input={CONF_SECURITYCODE: generated_securitycode}) return await self.async_step_twofactor( user_input={CONF_SECURITYCODE: self.securitycode}) if login.status and (login.status.get("login_failed")): _LOGGER.debug("Login failed: %s", login.status.get("login_failed")) await login.close() self.hass.components.persistent_notification.async_dismiss( f"alexa_media_{slugify(email)}{slugify(login.url[7:])}") return self.async_abort(reason="login_failed") new_schema = self._update_schema_defaults() if login.status and login.status.get("error_message"): _LOGGER.debug("Login error detected: %s", login.status.get("error_message")) if (login.status.get("error_message") in { "There was a problem\n Enter a valid email or mobile number\n " } and self.automatic_steps < 2): _LOGGER.debug( "Trying automatic resubmission %s for error_message 'valid email'", self.automatic_steps, ) self.automatic_steps += 1 await sleep(5) return await self.async_step_user_legacy(user_input=self.config ) _LOGGER.debug( "Done with automatic resubmission for error_message 'valid email'; returning error message", ) self.automatic_steps = 0 return self.async_show_form( step_id="user", data_schema=vol.Schema(new_schema), description_placeholders={ "message": f" \n> {login.status.get('error_message','')}" }, ) def _save_user_input_to_config(self, user_input=None) -> None: """Process user_input to save to self.config. user_input can be a dictionary of strings or an internally saved config_entry data entry. This function will convert all to internal strings. """ if user_input is None: return if CONF_HASS_URL in user_input: self.config[CONF_HASS_URL] = user_input[CONF_HASS_URL] self.securitycode = user_input.get(CONF_SECURITYCODE) if self.securitycode is not None: self.config[CONF_SECURITYCODE] = self.securitycode elif CONF_SECURITYCODE in self.config: self.config.pop(CONF_SECURITYCODE) if user_input.get(CONF_OTPSECRET) and user_input.get( CONF_OTPSECRET).replace(" ", ""): self.config[CONF_OTPSECRET] = user_input[CONF_OTPSECRET].replace( " ", "") elif user_input.get(CONF_OTPSECRET): # a blank line self.config.pop(CONF_OTPSECRET) if CONF_EMAIL in user_input: self.config[CONF_EMAIL] = user_input[CONF_EMAIL] if CONF_PASSWORD in user_input: self.config[CONF_PASSWORD] = user_input[CONF_PASSWORD] if CONF_URL in user_input: self.config[CONF_URL] = user_input[CONF_URL] if CONF_DEBUG in user_input: self.config[CONF_DEBUG] = user_input[CONF_DEBUG] if CONF_SCAN_INTERVAL in user_input: self.config[CONF_SCAN_INTERVAL] = ( user_input[CONF_SCAN_INTERVAL] if not isinstance(user_input[CONF_SCAN_INTERVAL], timedelta) else user_input[CONF_SCAN_INTERVAL].total_seconds()) if CONF_INCLUDE_DEVICES in user_input: if isinstance(user_input[CONF_INCLUDE_DEVICES], list): self.config[CONF_INCLUDE_DEVICES] = ( reduce(lambda x, y: f"{x},{y}", user_input[CONF_INCLUDE_DEVICES]) if user_input[CONF_INCLUDE_DEVICES] else "") else: self.config[CONF_INCLUDE_DEVICES] = user_input[ CONF_INCLUDE_DEVICES] if CONF_EXCLUDE_DEVICES in user_input: if isinstance(user_input[CONF_EXCLUDE_DEVICES], list): self.config[CONF_EXCLUDE_DEVICES] = ( reduce(lambda x, y: f"{x},{y}", user_input[CONF_EXCLUDE_DEVICES]) if user_input[CONF_EXCLUDE_DEVICES] else "") else: self.config[CONF_EXCLUDE_DEVICES] = user_input[ CONF_EXCLUDE_DEVICES] def _update_schema_defaults(self) -> Any: new_schema = self._update_ord_dict( self.data_schema, { vol.Required(CONF_EMAIL, default=self.config.get(CONF_EMAIL, "")): str, vol.Required(CONF_PASSWORD, default=self.config.get(CONF_PASSWORD, "")): str, vol.Optional( CONF_SECURITYCODE, default=self.securitycode if self.securitycode else "", ): str, vol.Optional( CONF_OTPSECRET, default=self.config.get(CONF_OTPSECRET, ""), ): str, vol.Required(CONF_URL, default=self.config.get(CONF_URL, "amazon.com")): str, vol.Optional(CONF_DEBUG, default=bool(self.config.get(CONF_DEBUG, False))): bool, vol.Optional( CONF_INCLUDE_DEVICES, default=self.config.get(CONF_INCLUDE_DEVICES, ""), ): str, vol.Optional( CONF_EXCLUDE_DEVICES, default=self.config.get(CONF_EXCLUDE_DEVICES, ""), ): str, vol.Optional(CONF_SCAN_INTERVAL, default=self.config.get(CONF_SCAN_INTERVAL, 60)): int, }, ) return new_schema @staticmethod @callback def async_get_options_flow(config_entry): """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry)
class AlexaMediaFlowHandler(config_entries.ConfigFlow): """Handle a Alexa Media config flow.""" VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL def _update_ord_dict(self, old_dict: OrderedDict, new_dict: dict) -> OrderedDict: result: OrderedDict = OrderedDict() for k, v in old_dict.items(): for key, value in new_dict.items(): if k == key: result.update([(key, value)]) break if k not in result: result.update([(k, v)]) return result def __init__(self): """Initialize the config flow.""" if self.hass and not self.hass.data.get(DATA_ALEXAMEDIA): _LOGGER.info(STARTUP) _LOGGER.info("Loaded alexapy==%s", alexapy_version) self.login = None self.securitycode: Optional[Text] = None self.automatic_steps: int = 0 self.config = OrderedDict() self.proxy_schema = None self.data_schema = OrderedDict( [ (vol.Optional(CONF_PROXY, default=False), bool), (vol.Required(CONF_EMAIL), str), (vol.Required(CONF_PASSWORD), str), (vol.Required(CONF_URL, default="amazon.com"), str), (vol.Optional(CONF_SECURITYCODE), str), (vol.Optional(CONF_OTPSECRET), str), (vol.Optional(CONF_DEBUG, default=False), bool), (vol.Optional(CONF_INCLUDE_DEVICES, default=""), str), (vol.Optional(CONF_EXCLUDE_DEVICES, default=""), str), (vol.Optional(CONF_SCAN_INTERVAL, default=60), int), (vol.Optional(CONF_COOKIES_TXT, default=""), str), (vol.Optional(CONF_OAUTH_LOGIN, default=True), bool), ] ) self.captcha_schema = OrderedDict( [ (vol.Optional(CONF_PROXY, default=False), bool), (vol.Required(CONF_PASSWORD), str), ( vol.Optional( CONF_SECURITYCODE, default=self.securitycode if self.securitycode else "", ), str, ), (vol.Required("captcha"), str), ] ) self.twofactor_schema = OrderedDict( [ (vol.Optional(CONF_PROXY, default=False), bool), ( vol.Required( CONF_SECURITYCODE, default=self.securitycode if self.securitycode else "", ), str, ), ] ) self.claimspicker_schema = OrderedDict( [ (vol.Optional(CONF_PROXY, default=False), bool), ( vol.Required("claimsoption", default=0), vol.All(cv.positive_int, vol.Clamp(min=0)), ), ] ) self.authselect_schema = OrderedDict( [ (vol.Optional(CONF_PROXY, default=False), bool), ( vol.Required("authselectoption", default=0), vol.All(cv.positive_int, vol.Clamp(min=0)), ), ] ) self.verificationcode_schema = OrderedDict( [ (vol.Optional(CONF_PROXY, default=False), bool), (vol.Required("verificationcode"), str), ] ) self.totp_register = OrderedDict( [(vol.Optional(CONF_TOTP_REGISTER, default=False), bool)] ) self.proxy = None async def async_step_import(self, import_config): """Import a config entry from configuration.yaml.""" return await self.async_step_user_legacy(import_config) async def async_step_user(self, user_input=None): """Provide a proxy for login.""" self._save_user_input_to_config(user_input=user_input) self.proxy_schema = OrderedDict( [ ( vol.Required(CONF_EMAIL, default=self.config.get(CONF_EMAIL, "")), str, ), ( vol.Required( CONF_PASSWORD, default=self.config.get(CONF_PASSWORD, "") ), str, ), ( vol.Required( CONF_URL, default=self.config.get(CONF_URL, "amazon.com") ), str, ), ( vol.Required( CONF_HASS_URL, default=self.config.get(CONF_HASS_URL, get_url(self.hass)), ), str, ), ( vol.Optional( CONF_OTPSECRET, default=self.config.get(CONF_OTPSECRET, "") ), str, ), ( vol.Optional( CONF_DEBUG, default=self.config.get(CONF_DEBUG, False) ), bool, ), ( vol.Optional( CONF_INCLUDE_DEVICES, default=self.config.get(CONF_INCLUDE_DEVICES, ""), ), str, ), ( vol.Optional( CONF_EXCLUDE_DEVICES, default=self.config.get(CONF_EXCLUDE_DEVICES, ""), ), str, ), ( vol.Optional( CONF_SCAN_INTERVAL, default=self.config.get(CONF_SCAN_INTERVAL, 60), ), int, ), ( vol.Optional(CONF_PROXY, default=self.config.get(CONF_PROXY, True)), bool, ), ( vol.Optional( CONF_OAUTH_LOGIN, default=self.config.get(CONF_OAUTH_LOGIN, True), ), bool, ), ] ) if not user_input: return self.async_show_form( step_id="user", data_schema=vol.Schema(self.proxy_schema), description_placeholders={"message": ""}, ) if user_input and not user_input.get(CONF_PROXY): return self.async_show_form( step_id="user_legacy", data_schema=vol.Schema(self._update_schema_defaults()), description_placeholders={"message": ""}, ) if self.login is None: try: self.login = self.hass.data[DATA_ALEXAMEDIA]["accounts"][ self.config[CONF_EMAIL] ].get("login_obj") except KeyError: self.login = None if not self.login or self.login.session.closed: _LOGGER.debug("Creating new login") uuid_dict = await calculate_uuid( self.hass, self.config.get(CONF_EMAIL), self.config[CONF_URL] ) uuid = uuid_dict["uuid"] self.login = AlexaLogin( url=self.config[CONF_URL], email=self.config.get(CONF_EMAIL, ""), password=self.config.get(CONF_PASSWORD, ""), outputpath=self.hass.config.path, debug=self.config[CONF_DEBUG], otp_secret=self.config.get(CONF_OTPSECRET, ""), uuid=uuid, oauth_login=self.config.get(CONF_OAUTH_LOGIN, True), ) else: _LOGGER.debug("Using existing login") if self.config.get(CONF_EMAIL): self.login.email = self.config.get(CONF_EMAIL) if self.config.get(CONF_PASSWORD): self.login.password = self.config.get(CONF_PASSWORD) if self.config.get(CONF_OTPSECRET): self.login.set_totp(self.config.get(CONF_OTPSECRET, "")) hass_url: Text = user_input.get(CONF_HASS_URL) self.proxy = AlexaProxy(self.login, hass_url) await self.proxy.start_proxy() self.hass.http.register_view(AlexaMediaAuthorizationCallbackView) callback_url = f"{hass_url}{AUTH_CALLBACK_PATH}?flow_id={self.flow_id}" proxy_url = f"{self.proxy.access_url()}?config_flow_id={self.flow_id}&callback_url={callback_url}" if self.login.lastreq: proxy_url = f"{self.proxy.access_url()}/resume?config_flow_id={self.flow_id}&callback_url={callback_url}" return self.async_external_step(step_id="check_proxy", url=proxy_url) async def async_step_check_proxy(self, user_input=None): """Check status of proxy for login.""" _LOGGER.debug( "Checking proxy response for %s - %s", hide_email(self.login.email), self.login.url, ) if self.proxy: await self.proxy.stop_proxy() if await self.login.test_loggedin(): await self.login.finalize_login() return self.async_external_step_done(next_step_id="finish_proxy") return self.async_abort(reason=self.login.status.get("login_failed")) async def async_step_finish_proxy(self, user_input=None): """Finish auth.""" self.config[CONF_EMAIL] = self.login.email self.config[CONF_PASSWORD] = self.login.password return await self._test_login() async def async_step_user_legacy(self, user_input=None): """Handle legacy input for the config flow.""" # pylint: disable=too-many-return-statements self._save_user_input_to_config(user_input=user_input) self.data_schema = self._update_schema_defaults() if not user_input: self.automatic_steps = 0 return self.async_show_form( step_id="user_legacy", data_schema=vol.Schema(self.data_schema), description_placeholders={"message": ""}, ) if ( not self.config.get("reauth") and f"{self.config[CONF_EMAIL]} - {self.config[CONF_URL]}" in configured_instances(self.hass) and not self.hass.data[DATA_ALEXAMEDIA]["config_flows"].get( f"{self.config[CONF_EMAIL]} - {self.config[CONF_URL]}" ) ): _LOGGER.debug("Existing account found") self.automatic_steps = 0 return self.async_show_form( step_id="user_legacy", data_schema=vol.Schema(self.data_schema), errors={CONF_EMAIL: "identifier_exists"}, description_placeholders={"message": ""}, ) if user_input and user_input.get(CONF_PROXY): return await self.async_step_user(user_input=None) if self.login is None: try: self.login = self.hass.data[DATA_ALEXAMEDIA]["accounts"][ self.config[CONF_EMAIL] ].get("login_obj") except KeyError: self.login = None try: if not self.login or self.login.session.closed: _LOGGER.debug("Creating new login") uuid_dict = await calculate_uuid( self.hass, self.config.get(CONF_EMAIL), self.config[CONF_URL] ) uuid = uuid_dict["uuid"] self.login = AlexaLogin( url=self.config[CONF_URL], email=self.config[CONF_EMAIL], password=self.config[CONF_PASSWORD], outputpath=self.hass.config.path, debug=self.config[CONF_DEBUG], otp_secret=self.config.get(CONF_OTPSECRET, ""), uuid=uuid, oauth_login=self.config.get(CONF_OAUTH_LOGIN, True), ) else: _LOGGER.debug("Using existing login") if ( not self.config.get("reauth") and user_input and user_input.get(CONF_OTPSECRET) and user_input.get(CONF_OTPSECRET).replace(" ", "") ): otp: Text = self.login.get_totp_token() if otp: _LOGGER.debug("Generating OTP from %s", otp) return self.async_show_form( step_id="totp_register", data_schema=vol.Schema(self.totp_register), errors={}, description_placeholders={ "email": self.login.email, "url": self.login.url, "message": otp, }, ) return self.async_show_form( step_id="user_legacy", errors={"base": "2fa_key_invalid"}, description_placeholders={"message": ""}, ) if self.login.status: _LOGGER.debug("Resuming existing flow") return await self._test_login() _LOGGER.debug("Trying to login %s", self.login.status) await self.login.login( cookies=await self.login.load_cookie( cookies_txt=self.config.get(CONF_COOKIES_TXT, "") ), data=self.config, ) return await self._test_login() except AlexapyConnectionError: self.automatic_steps = 0 return self.async_show_form( step_id="user_legacy", errors={"base": "connection_error"}, description_placeholders={"message": ""}, ) except AlexapyPyotpInvalidKey: self.automatic_steps = 0 return self.async_show_form( step_id="user_legacy", errors={"base": "2fa_key_invalid"}, description_placeholders={"message": ""}, ) except BaseException as ex: # pylyint: disable=broad-except _LOGGER.warning("Unknown error: %s", ex) if self.config[CONF_DEBUG]: raise self.automatic_steps = 0 return self.async_show_form( step_id="user_legacy", errors={"base": "unknown_error"}, description_placeholders={"message": ""}, ) async def async_step_captcha(self, user_input=None): """Handle the input processing of the config flow.""" return await self.async_step_process("captcha", user_input) async def async_step_twofactor(self, user_input=None): """Handle the input processing of the config flow.""" return await self.async_step_process("two_factor", user_input) async def async_step_totp_register(self, user_input=None): """Handle the input processing of the config flow.""" self._save_user_input_to_config(user_input=user_input) if user_input and user_input.get("registered") is False: _LOGGER.debug("Not registered, regenerating") otp: Text = self.login.get_totp_token() if otp: _LOGGER.debug("Generating OTP from %s", otp) return self.async_show_form( step_id="totp_register", data_schema=vol.Schema(self.totp_register), errors={}, description_placeholders={ "email": self.login.email, "url": self.login.url, "message": otp, }, ) return await self.async_step_process("totp_register", self.config) async def async_step_claimspicker(self, user_input=None): """Handle the input processing of the config flow.""" return await self.async_step_process("claimspicker", user_input) async def async_step_authselect(self, user_input=None): """Handle the input processing of the config flow.""" return await self.async_step_process("authselect", user_input) async def async_step_verificationcode(self, user_input=None): """Handle the input processing of the config flow.""" return await self.async_step_process("verificationcode", user_input) async def async_step_action_required(self, user_input=None): """Handle the input processing of the config flow.""" return await self.async_step_process("action_required", user_input) async def async_step_process(self, step_id, user_input=None): """Handle the input processing of the config flow.""" _LOGGER.debug( "Processing input for %s: %s", step_id, obfuscate(user_input), ) self._save_user_input_to_config(user_input=user_input) if user_input and user_input.get(CONF_PROXY): return await self.async_step_user(user_input=None) if user_input: try: await self.login.login(data=user_input) except AlexapyConnectionError: self.automatic_steps = 0 return self.async_show_form( step_id=step_id, errors={"base": "connection_error"}, description_placeholders={"message": ""}, ) except BaseException as ex: # pylint: disable=broad-except _LOGGER.warning("Unknown error: %s", ex) if self.config[CONF_DEBUG]: raise self.automatic_steps = 0 return self.async_show_form( step_id=step_id, errors={"base": "unknown_error"}, description_placeholders={"message": ""}, ) return await self._test_login() async def async_step_reauth(self, user_input=None): """Handle reauth processing for the config flow.""" self._save_user_input_to_config(user_input) self.config["reauth"] = True reauth_schema = self._update_schema_defaults() _LOGGER.debug( "Creating reauth form with %s", obfuscate(self.config), ) self.automatic_steps = 0 if self.login is None: try: self.login = self.hass.data[DATA_ALEXAMEDIA]["accounts"][ self.config[CONF_EMAIL] ].get("login_obj") except KeyError: self.login = None seconds_since_login: int = ( datetime.datetime.now() - self.login.stats["login_timestamp"] ).seconds if self.login else 60 if seconds_since_login < 60: _LOGGER.debug( "Relogin requested within %s seconds; manual login required", seconds_since_login, ) return self.async_show_form( step_id="user_legacy", data_schema=vol.Schema(reauth_schema), description_placeholders={"message": "REAUTH"}, ) _LOGGER.debug("Attempting automatic relogin") await sleep(15) return await self.async_step_user_legacy(self.config) async def _test_login(self): # pylint: disable=too-many-statements, too-many-return-statements login = self.login email = login.email _LOGGER.debug("Testing login status: %s", login.status) if login.status and login.status.get("login_successful"): existing_entry = await self.async_set_unique_id(f"{email} - {login.url}") if self.config.get("reauth"): self.config.pop("reauth") if self.config.get(CONF_SECURITYCODE): self.config.pop(CONF_SECURITYCODE) if self.config.get(CONF_PROXY): self.config.pop(CONF_PROXY) if self.config.get("hass_url"): self.config.pop("hass_url") self.config[CONF_OAUTH] = { "access_token": login.access_token, "refresh_token": login.refresh_token, "expires_in": login.expires_in, } self.hass.data.setdefault( DATA_ALEXAMEDIA, {"accounts": {}, "config_flows": {}, "lock": Lock()}, ) if existing_entry: self.hass.config_entries.async_update_entry( existing_entry, data=self.config ) _LOGGER.debug("Reauth successful for %s", hide_email(email)) self.hass.bus.async_fire( "alexa_media_relogin_success", event_data={"email": hide_email(email), "url": login.url}, ) self.hass.components.persistent_notification.async_dismiss( f"alexa_media_{slugify(email)}{slugify(login.url[7:])}" ) self.hass.data[DATA_ALEXAMEDIA]["accounts"][self.config[CONF_EMAIL]][ "login_obj" ] = self.login self.hass.data[DATA_ALEXAMEDIA]["config_flows"][ f"{email} - {login.url}" ] = None return self.async_abort(reason="reauth_successful") _LOGGER.debug( "Setting up Alexa devices with %s", dict(obfuscate(self.config)) ) self._abort_if_unique_id_configured(self.config) return self.async_create_entry( title=f"{login.email} - {login.url}", data=self.config ) if login.status and login.status.get("captcha_required"): new_schema = self._update_ord_dict( self.captcha_schema, { vol.Required( CONF_PASSWORD, default=self.config[CONF_PASSWORD] ): str, vol.Optional( CONF_SECURITYCODE, default=self.securitycode if self.securitycode else "", ): str, }, ) _LOGGER.debug("Creating config_flow to request captcha") self.automatic_steps = 0 return self.async_show_form( step_id="captcha", data_schema=vol.Schema(new_schema), errors={}, description_placeholders={ "email": login.email, "url": login.url, "captcha_image": "[![captcha]({0})]({0})".format( login.status["captcha_image_url"] ), "message": f" \n> {login.status.get('error_message','')}", }, ) if login.status and login.status.get("securitycode_required"): _LOGGER.debug( "Creating config_flow to request 2FA. Saved security code %s", self.securitycode, ) generated_securitycode: Text = login.get_totp_token() if ( self.securitycode or generated_securitycode ) and self.automatic_steps < 2: if self.securitycode: _LOGGER.debug( "Automatically submitting securitycode %s", self.securitycode ) else: _LOGGER.debug( "Automatically submitting generated securitycode %s", generated_securitycode, ) self.automatic_steps += 1 await sleep(5) if generated_securitycode: return await self.async_step_twofactor( user_input={CONF_SECURITYCODE: generated_securitycode} ) return await self.async_step_twofactor( user_input={CONF_SECURITYCODE: self.securitycode} ) self.twofactor_schema = OrderedDict( [ (vol.Optional(CONF_PROXY, default=False), bool), ( vol.Required( CONF_SECURITYCODE, default=self.securitycode if self.securitycode else "", ), str, ), ] ) self.automatic_steps = 0 return self.async_show_form( step_id="twofactor", data_schema=vol.Schema(self.twofactor_schema), errors={}, description_placeholders={ "email": login.email, "url": login.url, "message": f" \n> {login.status.get('error_message','')}", }, ) if login.status and login.status.get("claimspicker_required"): error_message = f" \n> {login.status.get('error_message', '')}" _LOGGER.debug("Creating config_flow to select verification method") claimspicker_message = login.status["claimspicker_message"] self.automatic_steps = 0 return self.async_show_form( step_id="claimspicker", data_schema=vol.Schema(self.claimspicker_schema), errors={}, description_placeholders={ "email": login.email, "url": login.url, "message": " \n> {0} \n> {1}".format( claimspicker_message, error_message ), }, ) if login.status and login.status.get("authselect_required"): _LOGGER.debug("Creating config_flow to select OTA method") error_message = login.status.get("error_message", "") authselect_message = login.status["authselect_message"] self.automatic_steps = 0 return self.async_show_form( step_id="authselect", data_schema=vol.Schema(self.authselect_schema), description_placeholders={ "email": login.email, "url": login.url, "message": " \n> {0} \n> {1}".format( authselect_message, error_message ), }, ) if login.status and login.status.get("verificationcode_required"): _LOGGER.debug("Creating config_flow to enter verification code") self.automatic_steps = 0 return self.async_show_form( step_id="verificationcode", data_schema=vol.Schema(self.verificationcode_schema), ) if ( login.status and login.status.get("force_get") and not login.status.get("ap_error_href") ): _LOGGER.debug("Creating config_flow to wait for user action") self.automatic_steps = 0 return self.async_show_form( step_id="action_required", data_schema=vol.Schema( OrderedDict([(vol.Optional(CONF_PROXY, default=False), bool)]) ), description_placeholders={ "email": login.email, "url": login.url, "message": f" \n>{login.status.get('message','')} \n", }, ) if login.status and (login.status.get("login_failed")): if login.oauth_login: _LOGGER.debug("Trying non-oauth login") await login.reset() login.oauth_login = False await login.login() return await self._test_login() _LOGGER.debug("Login failed: %s", login.status.get("login_failed")) await login.close() self.hass.components.persistent_notification.async_dismiss( f"alexa_media_{slugify(email)}{slugify(login.url[7:])}" ) return self.async_abort(reason=login.status.get("login_failed")) new_schema = self._update_schema_defaults() if login.status and login.status.get("error_message"): _LOGGER.debug("Login error detected: %s", login.status.get("error_message")) if ( login.status.get("error_message") in { "There was a problem\n Enter a valid email or mobile number\n " } and self.automatic_steps < 2 ): _LOGGER.debug( "Trying automatic resubmission %s for error_message 'valid email'", self.automatic_steps, ) self.automatic_steps += 1 await sleep(5) return await self.async_step_user_legacy(user_input=self.config) _LOGGER.debug( "Done with automatic resubmission for error_message 'valid email'; returning error message", ) self.automatic_steps = 0 return self.async_show_form( step_id="user_legacy", data_schema=vol.Schema(new_schema), description_placeholders={ "message": f" \n> {login.status.get('error_message','')}" }, ) self.automatic_steps = 0 return self.async_show_form( step_id="user_legacy", data_schema=vol.Schema(new_schema), description_placeholders={ "message": f" \n> {login.status.get('error_message','')}" }, ) def _save_user_input_to_config(self, user_input=None) -> None: """Process user_input to save to self.config. user_input can be a dictionary of strings or an internally saved config_entry data entry. This function will convert all to internal strings. """ if user_input is None: return if CONF_PROXY in user_input: self.config[CONF_PROXY] = user_input[CONF_PROXY] if CONF_OAUTH_LOGIN in user_input: self.config[CONF_OAUTH_LOGIN] = user_input[CONF_OAUTH_LOGIN] if CONF_HASS_URL in user_input: self.config[CONF_HASS_URL] = user_input[CONF_HASS_URL] self.securitycode = user_input.get(CONF_SECURITYCODE) if self.securitycode is not None: self.config[CONF_SECURITYCODE] = self.securitycode elif CONF_SECURITYCODE in self.config: self.config.pop(CONF_SECURITYCODE) if user_input.get(CONF_OTPSECRET) and user_input.get(CONF_OTPSECRET).replace( " ", "" ): self.config[CONF_OTPSECRET] = user_input[CONF_OTPSECRET].replace(" ", "") elif user_input.get(CONF_OTPSECRET): # a blank line self.config.pop(CONF_OTPSECRET) if CONF_EMAIL in user_input: self.config[CONF_EMAIL] = user_input[CONF_EMAIL] if CONF_PASSWORD in user_input: self.config[CONF_PASSWORD] = user_input[CONF_PASSWORD] if CONF_URL in user_input: self.config[CONF_URL] = user_input[CONF_URL] if CONF_DEBUG in user_input: self.config[CONF_DEBUG] = user_input[CONF_DEBUG] if CONF_SCAN_INTERVAL in user_input: self.config[CONF_SCAN_INTERVAL] = ( user_input[CONF_SCAN_INTERVAL] if not isinstance(user_input[CONF_SCAN_INTERVAL], timedelta) else user_input[CONF_SCAN_INTERVAL].total_seconds() ) if CONF_INCLUDE_DEVICES in user_input: if isinstance(user_input[CONF_INCLUDE_DEVICES], list): self.config[CONF_INCLUDE_DEVICES] = ( reduce(lambda x, y: f"{x},{y}", user_input[CONF_INCLUDE_DEVICES]) if user_input[CONF_INCLUDE_DEVICES] else "" ) else: self.config[CONF_INCLUDE_DEVICES] = user_input[CONF_INCLUDE_DEVICES] if CONF_EXCLUDE_DEVICES in user_input: if isinstance(user_input[CONF_EXCLUDE_DEVICES], list): self.config[CONF_EXCLUDE_DEVICES] = ( reduce(lambda x, y: f"{x},{y}", user_input[CONF_EXCLUDE_DEVICES]) if user_input[CONF_EXCLUDE_DEVICES] else "" ) else: self.config[CONF_EXCLUDE_DEVICES] = user_input[CONF_EXCLUDE_DEVICES] if ( user_input.get(CONF_COOKIES_TXT) and f"{HTTP_COOKIE_HEADER}\n" != user_input[CONF_COOKIES_TXT] ): fixed_cookies_txt = re.sub( r" ", r"\n", re.sub( r"#.*\n", r"", re.sub( r"# ((?:.(?!# ))+)$", r"\1", re.sub(r" #", r"\n#", user_input[CONF_COOKIES_TXT]), ), ), ) if not fixed_cookies_txt.startswith(HTTP_COOKIE_HEADER): fixed_cookies_txt = f"{HTTP_COOKIE_HEADER}\n{fixed_cookies_txt}" self.config[CONF_COOKIES_TXT] = fixed_cookies_txt _LOGGER.debug("Setting cookies to:\n%s", fixed_cookies_txt) def _update_schema_defaults(self) -> Any: new_schema = self._update_ord_dict( self.data_schema, { vol.Required(CONF_EMAIL, default=self.config.get(CONF_EMAIL, "")): str, vol.Required( CONF_PASSWORD, default=self.config.get(CONF_PASSWORD, "") ): str, vol.Optional( CONF_SECURITYCODE, default=self.securitycode if self.securitycode else "", ): str, vol.Optional( CONF_OTPSECRET, default=self.config.get(CONF_OTPSECRET, ""), ): str, vol.Required( CONF_URL, default=self.config.get(CONF_URL, "amazon.com") ): str, vol.Optional( CONF_DEBUG, default=bool(self.config.get(CONF_DEBUG, False)) ): bool, vol.Optional( CONF_INCLUDE_DEVICES, default=self.config.get(CONF_INCLUDE_DEVICES, ""), ): str, vol.Optional( CONF_EXCLUDE_DEVICES, default=self.config.get(CONF_EXCLUDE_DEVICES, ""), ): str, vol.Optional( CONF_SCAN_INTERVAL, default=self.config.get(CONF_SCAN_INTERVAL, 60) ): int, vol.Optional( CONF_COOKIES_TXT, default=self.config.get(CONF_COOKIES_TXT, "") ): str, vol.Optional( CONF_OAUTH_LOGIN, default=self.config.get(CONF_OAUTH_LOGIN, True) ): bool, }, ) return new_schema @staticmethod @callback def async_get_options_flow(config_entry): """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry)