Files
home-assistant-garmin_connect/custom_components/garmin_connect/config_flow.py
Ron Klinkien 9cf1b3a3bb Migrate from old config flow to new config flow
Catch authentication errors and show them in the UI
Limit last activity to 5 in attributes
2025-04-19 10:38:01 +00:00

174 lines
6.0 KiB
Python

"""Config flow for Garmin Connect integration."""
import logging
from collections.abc import Mapping
from typing import Any, cast
import requests
from garminconnect import (
Garmin,
GarminConnectAuthenticationError,
GarminConnectConnectionError,
GarminConnectTooManyRequestsError,
)
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_ID, CONF_TOKEN, CONF_PASSWORD, CONF_USERNAME
import voluptuous as vol
import garth
from .const import CONF_MFA, DOMAIN
_LOGGER = logging.getLogger(__name__)
class GarminConnectConfigFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Garmin Connect."""
VERSION = 1
def __init__(self) -> None:
"""Initialize flow."""
self.data_schema = {
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
self.mfa_data_schema = {
vol.Required(CONF_MFA): str,
}
self._api = None
self._login_result1: Any = None
self._login_result2: Any = None
self._mfa_code: str | None = None
self._username: str | None = None
self._password: str | None = None
self._in_china = False
async def _async_garmin_connect_login(self, step_id: str) -> ConfigFlowResult:
"""Handle login with Garmin Connect."""
errors = {}
# Check if the user resides in China
country = self.hass.config.country
if country == "CN":
self._in_china = True
self._api = Garmin(email=self._username,
password=self._password, return_on_mfa=True, is_cn=self._in_china)
try:
self._login_result1, self._login_result2 = await self.hass.async_add_executor_job(self._api.login)
if self._login_result1 == "needs_mfa": # MFA is required
return await self.async_step_mfa()
except GarminConnectConnectionError:
errors = {"base": "cannot_connect"}
except GarminConnectAuthenticationError:
errors = {"base": "invalid_auth"}
except GarminConnectTooManyRequestsError:
errors = {"base": "too_many_requests"}
except requests.exceptions.HTTPError as err:
if err.response.status_code == 403:
errors = {"base": "invalid_auth"}
elif err.response.status_code == 429:
errors = {"base": "too_many_requests"}
else:
errors = {"base": "cannot_connect"}
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors = {"base": "unknown"}
if errors:
return self.async_show_form(
step_id=step_id, data_schema=vol.Schema(self.data_schema), errors=errors
)
return await self._async_create_entry()
async def _async_garmin_connect_mfa_login(self) -> ConfigFlowResult:
"""Handle multi-factor authentication (MFA) login with Garmin Connect."""
try:
await self.hass.async_add_executor_job(self._api.resume_login, self._login_result2, self._mfa_code)
except garth.exc.GarthException as err:
_LOGGER.error(f"Error during MFA login: {err}")
return self.async_show_form(
step_id="mfa",
data_schema=vol.Schema(self.mfa_data_schema),
errors={"base": "invalid_mfa_code"},
)
return await self._async_create_entry()
async def _async_create_entry(self) -> ConfigFlowResult:
"""Create the config entry."""
config_data = {
CONF_ID: self._username,
CONF_USERNAME: self._username,
CONF_TOKEN: self._api.garth.dumps(),
}
existing_entry = await self.async_set_unique_id(self._username)
if existing_entry:
return self.async_update_reload_and_abort(existing_entry, data=config_data)
return self.async_create_entry(
title=cast(str, self._username), data=config_data
)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a flow initialized by the user."""
if user_input is None:
return self.async_show_form(
step_id="user", data_schema=vol.Schema(self.data_schema)
)
self._username = user_input[CONF_USERNAME]
self._password = user_input[CONF_PASSWORD]
return await self._async_garmin_connect_login(step_id="user")
async def async_step_mfa(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a multi-factor authentication (MFA) flow."""
if user_input is None:
return self.async_show_form(
step_id="mfa", data_schema=vol.Schema(self.mfa_data_schema)
)
self._mfa_code = user_input[CONF_MFA]
_LOGGER.info(f"MFA CODE: {self._mfa_code}")
return await self._async_garmin_connect_mfa_login()
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle reauthorization request from Garmin Connect."""
self._username = entry_data[CONF_USERNAME]
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reauthorization flow."""
if user_input is None:
return self.async_show_form(
step_id="reauth_confirm",
data_schema=vol.Schema(
{
vol.Required(CONF_USERNAME, default=self._username): str,
vol.Required(CONF_PASSWORD): str,
}
),
)
self._username = user_input[CONF_USERNAME]
self._password = user_input[CONF_PASSWORD]
return await self._async_garmin_connect_login(step_id="reauth_confirm")