From 74683fe8a380d35c6f32bd63180da3885143c28c Mon Sep 17 00:00:00 2001 From: Jakob Schlyter Date: Sat, 14 Dec 2024 22:53:35 +0100 Subject: [PATCH 1/4] Restructure config flow with better error handling --- custom_components/polestar_api/config_flow.py | 144 +++++++++--------- .../polestar_api/translations/en.json | 11 +- 2 files changed, 83 insertions(+), 72 deletions(-) diff --git a/custom_components/polestar_api/config_flow.py b/custom_components/polestar_api/config_flow.py index 478bdc0..0876c3f 100644 --- a/custom_components/polestar_api/config_flow.py +++ b/custom_components/polestar_api/config_flow.py @@ -1,21 +1,28 @@ """Config flow for the Polestar EV platform.""" -import asyncio import logging import voluptuous as vol -from aiohttp import ClientError from homeassistant import config_entries from homeassistant.config_entries import ConfigFlowResult from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers.httpx_client import get_async_client from .const import CONF_VIN, DOMAIN -from .polestar import PolestarCoordinator -from .pypolestar.exception import PolestarAuthException +from .pypolestar.exception import PolestarApiException, PolestarAuthException +from .pypolestar.polestar import PolestarApi _LOGGER = logging.getLogger(__name__) +class NoCarsFoundException(Exception): + pass + + +class VinNotFoundException(Exception): + pass + + @config_entries.HANDLERS.register(DOMAIN) class FlowHandler(config_entries.ConfigFlow): """Handle a config flow.""" @@ -23,76 +30,73 @@ class FlowHandler(config_entries.ConfigFlow): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL - async def _create_entry( - self, username: str, password: str, vin: str | None - ) -> ConfigFlowResult: - """Register new entry.""" - return self.async_create_entry( - title=f"Polestar EV for {username}", - data={CONF_USERNAME: username, CONF_PASSWORD: password, CONF_VIN: vin}, + async def async_step_user(self, user_input: dict | None = None) -> ConfigFlowResult: + """User initiated config flow.""" + _errors = {} + + if user_input is not None: + username = user_input[CONF_USERNAME] + password = user_input[CONF_PASSWORD] + vin = user_input.get(CONF_VIN) + + try: + await self._test_credentials(username, password, vin) + except NoCarsFoundException as exc: + _LOGGER.error(exc) + _errors["base"] = "no_cars_found" + except VinNotFoundException as exc: + _LOGGER.error(exc) + _errors["base"] = "vin_not_found" + except PolestarAuthException as exc: + _LOGGER.warning(exc) + _errors["base"] = "auth_failed" + except PolestarApiException as exc: + _LOGGER.error(exc) + _errors["base"] = "api" + except Exception as exc: + _LOGGER.error(exc) + _errors["base"] = "unknown" + else: + return self.async_create_entry( + title=f"Polestar EV for {username}", + data={ + CONF_USERNAME: username, + CONF_PASSWORD: password, + CONF_VIN: vin, + }, + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Optional(CONF_VIN): str, + } + ), + errors=_errors, ) - async def _create_device( + async def _test_credentials( self, username: str, password: str, vin: str | None - ) -> ConfigFlowResult: - """Create device.""" - - try: - device = PolestarCoordinator( - hass=self.hass, - username=username, - password=password, - vin=vin, - ) - await device.async_init() - - # check that we found cars - if not len(device.get_cars()): - return self.async_abort(reason="No cars found") - - # check if we have a token, otherwise throw exception - if device.polestar_api.auth.access_token is None: - _LOGGER.exception( - "No token, Could be wrong credentials (invalid email or password))" - ) - return self.async_abort(reason="No API token") + ) -> None: + """Validate credentials and return VINs of found cars.""" - except asyncio.TimeoutError: - return self.async_abort(reason="API timeout") - except ClientError: - _LOGGER.exception("ClientError") - return self.async_abort(reason="API client failure") - except PolestarAuthException: - return self.async_abort(reason="Login failed") - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected error creating device") - return self.async_abort(reason="API unexpected failure") + api_client = PolestarApi( + username=username, + password=password, + client_session=get_async_client(self.hass), + ) + await api_client.async_init() - return await self._create_entry(username, password, vin) + found_vins = api_client.vins + _LOGGER.debug("Found %d cars for %s", len(found_vins), username) - async def async_step_user(self, user_input: dict | None = None) -> ConfigFlowResult: - """User initiated config flow.""" - if user_input is None: - return self.async_show_form( - step_id="user", - data_schema=vol.Schema( - { - vol.Required(CONF_USERNAME): str, - vol.Required(CONF_PASSWORD): str, - vol.Optional(CONF_VIN): str, - } - ), - ) - return await self._create_device( - username=user_input[CONF_USERNAME], - password=user_input[CONF_PASSWORD], - vin=user_input.get(CONF_VIN), - ) + if not found_vins: + _LOGGER.warning("No cars found for %s", username) + raise NoCarsFoundException - async def async_step_import(self, user_input: dict) -> ConfigFlowResult: - """Import a config entry.""" - return await self._create_device( - username=user_input[CONF_USERNAME], - password=user_input[CONF_PASSWORD], - vin=user_input.get(CONF_VIN), - ) + if vin and vin not in found_vins: + _LOGGER.warning("VIN %s not found for %s", vin, username) + raise VinNotFoundException diff --git a/custom_components/polestar_api/translations/en.json b/custom_components/polestar_api/translations/en.json index f9601b6..c1d53b1 100644 --- a/custom_components/polestar_api/translations/en.json +++ b/custom_components/polestar_api/translations/en.json @@ -12,10 +12,17 @@ } }, "abort": { - "api_timeout": "Timeout connecting to the api.", - "api_failed": "Unexpected error creating api.", + "api_timeout": "Timeout connecting to the API.", + "api_failed": "Unexpected error creating API.", "already_configured": "Polestar API is already configured", "no_token": "No token found in response. Please check your credentials." + }, + "error": { + "auth_failed": "Invalid username/password.", + "no_cars_found": "No cars found for Polestar ID.", + "vin_not_found": "Specified VIN not found for Polestar ID.", + "api": "Error connecting to Polestar API.", + "unknown": "Unknown error occurred." } }, "entity": { From 63f919e9572942a21e2620a5a69c1e376a414610 Mon Sep 17 00:00:00 2001 From: Jakob Schlyter Date: Sun, 15 Dec 2024 11:07:26 +0100 Subject: [PATCH 2/4] Let unknown exceptions pass through --- custom_components/polestar_api/config_flow.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/custom_components/polestar_api/config_flow.py b/custom_components/polestar_api/config_flow.py index 0876c3f..e0a995f 100644 --- a/custom_components/polestar_api/config_flow.py +++ b/custom_components/polestar_api/config_flow.py @@ -53,9 +53,6 @@ async def async_step_user(self, user_input: dict | None = None) -> ConfigFlowRes except PolestarApiException as exc: _LOGGER.error(exc) _errors["base"] = "api" - except Exception as exc: - _LOGGER.error(exc) - _errors["base"] = "unknown" else: return self.async_create_entry( title=f"Polestar EV for {username}", From 10cc6526d2c34c346621e2392ea17be731eb4dae Mon Sep 17 00:00:00 2001 From: Jakob Schlyter Date: Sun, 15 Dec 2024 11:10:13 +0100 Subject: [PATCH 3/4] update translations --- custom_components/polestar_api/translations/en.json | 7 +++---- custom_components/polestar_api/translations/sv.json | 6 ++++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/custom_components/polestar_api/translations/en.json b/custom_components/polestar_api/translations/en.json index c1d53b1..1f4f1e3 100644 --- a/custom_components/polestar_api/translations/en.json +++ b/custom_components/polestar_api/translations/en.json @@ -19,10 +19,9 @@ }, "error": { "auth_failed": "Invalid username/password.", - "no_cars_found": "No cars found for Polestar ID.", - "vin_not_found": "Specified VIN not found for Polestar ID.", - "api": "Error connecting to Polestar API.", - "unknown": "Unknown error occurred." + "no_cars_found": "No cars found for your Polestar ID.", + "vin_not_found": "Specified VIN not found for your Polestar ID.", + "api": "Error connecting to Polestar API." } }, "entity": { diff --git a/custom_components/polestar_api/translations/sv.json b/custom_components/polestar_api/translations/sv.json index cee0889..546171c 100644 --- a/custom_components/polestar_api/translations/sv.json +++ b/custom_components/polestar_api/translations/sv.json @@ -17,6 +17,12 @@ "api_failed": "Oväntat fel vid skapande av API.", "already_configured": "Polestar API är redan konfigurerat", "no_token": "Ingen token hittades i svaret. Vänligen kontrollera dina uppgifter." + }, + "error": { + "auth_failed": "Felaktigt användarnamn/lösenord.", + "no_cars_found": "Inga fordon hittades för ditt Polestar ID.", + "vin_not_found": "Specificerad VIN hittades inte för ditt Polestar ID.", + "api": "Fel vid anslutning till Polestar API." } }, "entity": { From 72fbbb04b26073d62541fd9e9597b30bffe842c3 Mon Sep 17 00:00:00 2001 From: Jakob Schlyter Date: Sun, 15 Dec 2024 16:40:40 +0100 Subject: [PATCH 4/4] clean up logging for found VINs --- custom_components/polestar_api/config_flow.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/custom_components/polestar_api/config_flow.py b/custom_components/polestar_api/config_flow.py index e0a995f..298ce4e 100644 --- a/custom_components/polestar_api/config_flow.py +++ b/custom_components/polestar_api/config_flow.py @@ -87,11 +87,10 @@ async def _test_credentials( ) await api_client.async_init() - found_vins = api_client.vins - _LOGGER.debug("Found %d cars for %s", len(found_vins), username) - - if not found_vins: - _LOGGER.warning("No cars found for %s", username) + if found_vins := api_client.vins: + _LOGGER.debug("Found %d VINs for %s", len(found_vins), username) + else: + _LOGGER.warning("No VINs found for %s", username) raise NoCarsFoundException if vin and vin not in found_vins: