diff --git a/custom_components/fitdays/config_flow.py b/custom_components/fitdays/config_flow.py index 1d869bf..4587e32 100644 --- a/custom_components/fitdays/config_flow.py +++ b/custom_components/fitdays/config_flow.py @@ -26,18 +26,26 @@ try: from fitdays import FitdaysClient - from fitdays.exceptions import FitdaysAuthError, FitdaysError, FitdaysNetworkError + from fitdays.exceptions import ( + FitdaysAuthError, + FitdaysError, + FitdaysNetworkError, + FitdaysValidationError, + ) except ImportError as err: # pragma: no cover - handled by manifest requirements _LOGGER.error("Failed to import the fitdays package: %s", err) raise -STEP_USER_SCHEMA = vol.Schema( - { - vol.Required(CONF_EMAIL): str, - vol.Required(CONF_PASSWORD): _SECRET, - vol.Optional(CONF_COUNTRY, default=DEFAULT_COUNTRY): str, - } -) + +def _credentials_schema(email: str = "", country: str = DEFAULT_COUNTRY) -> vol.Schema: + """Build the email/password/country form, prefilled with what we know.""" + return vol.Schema( + { + vol.Required(CONF_EMAIL, default=email): str, + vol.Required(CONF_PASSWORD): _SECRET, + vol.Optional(CONF_COUNTRY, default=country or DEFAULT_COUNTRY): str, + } + ) class FitdaysConfigFlow(ConfigFlow, domain=DOMAIN): @@ -68,6 +76,10 @@ async def _async_try_login( return None, "invalid_auth" except FitdaysNetworkError: return None, "cannot_connect" + except FitdaysValidationError: + # The client rejects blank input before it calls the cloud. That is + # a form problem, not an outage, so say so instead of "unknown". + return None, "invalid_auth" except FitdaysError as err: _LOGGER.error("Unexpected Fitdays error during login: %s", err) return None, "unknown" @@ -95,7 +107,7 @@ async def async_step_user( return self.async_create_entry(title=email, data=entry_data) return self.async_show_form( - step_id="user", data_schema=STEP_USER_SCHEMA, errors=errors + step_id="user", data_schema=_credentials_schema(), errors=errors ) async def async_step_reauth( @@ -108,16 +120,17 @@ async def async_step_reauth( async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: - """Ask for the password again and refresh the stored session.""" + """Ask for the credentials again and refresh the stored session.""" errors: dict[str, str] = {} existing = self._reauth_entry_data or {} email = existing.get(CONF_EMAIL) or "" if user_input is not None: + email = user_input[CONF_EMAIL].strip() new_data, error = await self._async_try_login( email, user_input[CONF_PASSWORD], - existing.get(CONF_COUNTRY) or DEFAULT_COUNTRY, + user_input.get(CONF_COUNTRY) or DEFAULT_COUNTRY, ) if error: errors["base"] = error @@ -127,9 +140,49 @@ async def async_step_reauth_confirm( data={**existing, **new_data}, ) + # The email is asked for rather than taken from the entry, because an + # entry written before the email was stored has none, and a login with + # a blank email fails in the client before it reaches the cloud. return self.async_show_form( step_id="reauth_confirm", - data_schema=vol.Schema({vol.Required(CONF_PASSWORD): _SECRET}), - description_placeholders={"email": email}, + data_schema=_credentials_schema( + email, existing.get(CONF_COUNTRY) or DEFAULT_COUNTRY + ), + description_placeholders={"email": email or "this account"}, + errors=errors, + ) + + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Let the user point the entry at fresh credentials.""" + errors: dict[str, str] = {} + entry = self._get_reconfigure_entry() + email = entry.data.get(CONF_EMAIL) or "" + country = entry.data.get(CONF_COUNTRY) or DEFAULT_COUNTRY + + if user_input is not None: + email = user_input[CONF_EMAIL].strip() + country = user_input.get(CONF_COUNTRY) or DEFAULT_COUNTRY + new_data, error = await self._async_try_login( + email, user_input[CONF_PASSWORD], country + ) + if error: + errors["base"] = error + elif str(new_data.get("uid")) != str(entry.unique_id): + # Reconfigure repairs one account's entry. A different account + # belongs in its own entry, or its sensors would silently + # change meaning. + return self.async_abort(reason="account_mismatch") + else: + return self.async_update_reload_and_abort( + entry, + data={**entry.data, **new_data}, + title=email, + ) + + return self.async_show_form( + step_id="reconfigure", + data_schema=_credentials_schema(email, country), errors=errors, ) diff --git a/custom_components/fitdays/strings.json b/custom_components/fitdays/strings.json index b9fb919..cbff263 100644 --- a/custom_components/fitdays/strings.json +++ b/custom_components/fitdays/strings.json @@ -15,9 +15,26 @@ }, "reauth_confirm": { "title": "Re-authenticate Fitdays", - "description": "The stored session for {email} is no longer valid. Enter the password again to reconnect.", + "description": "The stored session for {email} is no longer valid. Enter the credentials again to reconnect.", "data": { - "password": "Password" + "email": "Email", + "password": "Password", + "country": "Country code" + }, + "data_description": { + "country": "Two-letter code of the country your Fitdays account is registered in, for example NL." + } + }, + "reconfigure": { + "title": "Reconfigure Fitdays", + "description": "Sign in again to replace the stored session. The credentials must belong to the same Fitdays account as this entry.", + "data": { + "email": "Email", + "password": "Password", + "country": "Country code" + }, + "data_description": { + "country": "Two-letter code of the country your Fitdays account is registered in, for example NL." } } }, @@ -28,7 +45,9 @@ }, "abort": { "already_configured": "This Fitdays account is already set up.", - "reauth_successful": "Re-authentication was successful." + "reauth_successful": "Re-authentication was successful.", + "reconfigure_successful": "Fitdays was reconfigured.", + "account_mismatch": "Those credentials are for a different Fitdays account. Add that account as a new entry instead." } }, "entity": { diff --git a/custom_components/fitdays/translations/en.json b/custom_components/fitdays/translations/en.json index b9fb919..cbff263 100644 --- a/custom_components/fitdays/translations/en.json +++ b/custom_components/fitdays/translations/en.json @@ -15,9 +15,26 @@ }, "reauth_confirm": { "title": "Re-authenticate Fitdays", - "description": "The stored session for {email} is no longer valid. Enter the password again to reconnect.", + "description": "The stored session for {email} is no longer valid. Enter the credentials again to reconnect.", "data": { - "password": "Password" + "email": "Email", + "password": "Password", + "country": "Country code" + }, + "data_description": { + "country": "Two-letter code of the country your Fitdays account is registered in, for example NL." + } + }, + "reconfigure": { + "title": "Reconfigure Fitdays", + "description": "Sign in again to replace the stored session. The credentials must belong to the same Fitdays account as this entry.", + "data": { + "email": "Email", + "password": "Password", + "country": "Country code" + }, + "data_description": { + "country": "Two-letter code of the country your Fitdays account is registered in, for example NL." } } }, @@ -28,7 +45,9 @@ }, "abort": { "already_configured": "This Fitdays account is already set up.", - "reauth_successful": "Re-authentication was successful." + "reauth_successful": "Re-authentication was successful.", + "reconfigure_successful": "Fitdays was reconfigured.", + "account_mismatch": "Those credentials are for a different Fitdays account. Add that account as a new entry instead." } }, "entity": { diff --git a/custom_components/fitdays/translations/nl.json b/custom_components/fitdays/translations/nl.json index d003a05..d9ecac9 100644 --- a/custom_components/fitdays/translations/nl.json +++ b/custom_components/fitdays/translations/nl.json @@ -15,9 +15,26 @@ }, "reauth_confirm": { "title": "Fitdays opnieuw verbinden", - "description": "De opgeslagen sessie voor {email} is niet meer geldig. Voer het wachtwoord opnieuw in om te herverbinden.", + "description": "De opgeslagen sessie voor {email} is niet meer geldig. Voer de inloggegevens opnieuw in om te herverbinden.", "data": { - "password": "Wachtwoord" + "email": "E-mailadres", + "password": "Wachtwoord", + "country": "Landcode" + }, + "data_description": { + "country": "Tweeletterige code van het land waar je Fitdays-account geregistreerd is, bijvoorbeeld NL." + } + }, + "reconfigure": { + "title": "Fitdays opnieuw instellen", + "description": "Log opnieuw in om de opgeslagen sessie te vervangen. De inloggegevens moeten bij hetzelfde Fitdays-account horen als deze invoer.", + "data": { + "email": "E-mailadres", + "password": "Wachtwoord", + "country": "Landcode" + }, + "data_description": { + "country": "Tweeletterige code van het land waar je Fitdays-account geregistreerd is, bijvoorbeeld NL." } } }, @@ -28,7 +45,9 @@ }, "abort": { "already_configured": "Dit Fitdays-account is al ingesteld.", - "reauth_successful": "Opnieuw verbinden is gelukt." + "reauth_successful": "Opnieuw verbinden is gelukt.", + "reconfigure_successful": "Fitdays is opnieuw ingesteld.", + "account_mismatch": "Deze inloggegevens horen bij een ander Fitdays-account. Voeg dat account toe als een nieuwe invoer." } }, "entity": {