From c11a5d0fed315bdb05a47563adefa008ffed6c24 Mon Sep 17 00:00:00 2001 From: Nils Vogels Date: Tue, 22 Oct 2024 16:07:38 +0200 Subject: [PATCH 1/2] Implement reauth flow --- custom_components/myskoda/__init__.py | 4 ++++ custom_components/myskoda/config_flow.py | 17 +++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/custom_components/myskoda/__init__.py b/custom_components/myskoda/__init__.py index bb2d2d3..7b7db25 100644 --- a/custom_components/myskoda/__init__.py +++ b/custom_components/myskoda/__init__.py @@ -7,10 +7,12 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_create_clientsession from homeassistant.util.ssl import get_default_context from myskoda import MySkoda from myskoda.myskoda import TRACE_CONFIG +from myskoda.auth.authorization import AuthorizationFailedError from .const import COORDINATORS, DOMAIN from .coordinator import MySkodaDataUpdateCoordinator @@ -38,6 +40,8 @@ async def async_setup_entry(hass: HomeAssistant, config: ConfigEntry) -> bool: try: await myskoda.connect(config.data["email"], config.data["password"]) + except AuthorizationFailedError: + raise ConfigEntryAuthFailed("Log in failed for MySkoda") except Exception: _LOGGER.exception("Login with MySkoda failed.") return False diff --git a/custom_components/myskoda/config_flow.py b/custom_components/myskoda/config_flow.py index a3c08f9..7f36cbd 100644 --- a/custom_components/myskoda/config_flow.py +++ b/custom_components/myskoda/config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +from collections.abc import Mapping from typing import Any import voluptuous as vol @@ -92,6 +93,22 @@ def async_get_options_flow( """Create the options flow.""" return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW) + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Handle reauthentication.""" + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Dialog that informs the user reauthentication is needed.""" + if user_input is None: + return self.async_show_form( + step_id="reauth_confirm", data_schema=STEP_USER_DATA_SCHEMA + ) + return await self.async_step_user() + class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" From e4992998444d6b497899ad0e911e703a196955e2 Mon Sep 17 00:00:00 2001 From: Nils Vogels Date: Thu, 14 Nov 2024 15:36:14 +0100 Subject: [PATCH 2/2] Add unique_id to config entry Implement migration flow Enhance reauth flow --- custom_components/myskoda/__init__.py | 85 ++++++++++++++++++++---- custom_components/myskoda/config_flow.py | 39 +++++++++-- 2 files changed, 104 insertions(+), 20 deletions(-) diff --git a/custom_components/myskoda/__init__.py b/custom_components/myskoda/__init__.py index 7af3874..28fa651 100644 --- a/custom_components/myskoda/__init__.py +++ b/custom_components/myskoda/__init__.py @@ -30,36 +30,46 @@ ] -async def async_setup_entry(hass: HomeAssistant, config: ConfigEntry) -> bool: - """Set up MySkoda integration from a config entry.""" +async def async_connect_myskoda( + hass: HomeAssistant, entry: ConfigEntry, mqtt_enabled: bool = True +) -> MySkoda: + """Connect to MySkoda.""" trace_configs = [] - if config.options.get("tracing"): + if entry.options.get("tracing"): trace_configs.append(TRACE_CONFIG) session = async_create_clientsession(hass, trace_configs=trace_configs) - myskoda = MySkoda(session, get_default_context()) + myskoda = MySkoda(session, get_default_context(), mqtt_enabled=mqtt_enabled) + await myskoda.connect(entry.data["email"], entry.data["password"]) + return myskoda + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up MySkoda integration from a config entry.""" try: - await myskoda.connect(config.data["email"], config.data["password"]) - except AuthorizationFailedError: - raise ConfigEntryAuthFailed("Log in failed for MySkoda") - except Exception: - _LOGGER.exception("Login with MySkoda failed.") + myskoda = await async_connect_myskoda(hass, entry) + except AuthorizationFailedError as exc: + raise ConfigEntryAuthFailed("Log in failed for %s: %s", DOMAIN, exc) + except Exception as exc: + _LOGGER.exception( + "Login with %s failed for unknown reason. Details: %s", DOMAIN, exc + ) return False coordinators: dict[str, MySkodaDataUpdateCoordinator] = {} vehicles = await myskoda.list_vehicle_vins() for vin in vehicles: - coordinator = MySkodaDataUpdateCoordinator(hass, config, myskoda, vin) + coordinator = MySkodaDataUpdateCoordinator(hass, entry, myskoda, vin) await coordinator.async_config_entry_first_refresh() coordinators[vin] = coordinator hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][config.entry_id] = {COORDINATORS: coordinators} + hass.data[DOMAIN][entry.entry_id] = {COORDINATORS: coordinators} - await hass.config_entries.async_forward_entry_setups(config, PLATFORMS) - config.async_on_unload(config.add_update_listener(_async_update_listener)) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(_async_update_listener)) return True @@ -77,3 +87,52 @@ async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): """Handle options update.""" # Do a lazy reload of integration when configuration changed await hass.config_entries.async_reload(entry.entry_id) + + +async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Handle config entry schema migration.""" + _LOGGER.debug( + "Migrating configuration entry %s from v%s.%s", + entry.entry_id, + entry.version, + entry.minor_version, + ) + + if entry.version > 2: + return False + + if entry.version == 1: + # v1 could have a missing unique_id. Bump to 2.1 + if not entry.unique_id or entry.unique_id == "": + _LOGGER.debug("Starting migration of unique_id") + + new_version = 2 + new_minor_version = 1 + try: + myskoda = await async_connect_myskoda(hass, entry, mqtt_enabled=False) + user = await myskoda.get_user() + unique_id = user.id + except AuthorizationFailedError as exc: + raise ConfigEntryAuthFailed("Log in failed for %s: %s", DOMAIN, exc) + except Exception as exc: + _LOGGER.exception( + "Login with %s failed for unknown reason. Details: %s", DOMAIN, exc + ) + return False + _LOGGER.debug("Add unique_id %s to entry %s", unique_id, entry.entry_id) + hass.config_entries.async_update_entry( + entry, + version=new_version, + minor_version=new_minor_version, + unique_id=unique_id, + ) + + if new_entry := hass.config_entries.async_get_entry(entry_id=entry.entry_id): + _LOGGER.debug( + "Migration of %s to v%s.%s successful", + entry.entry_id, + new_entry.version, + new_entry.minor_version, + ) + + return True diff --git a/custom_components/myskoda/config_flow.py b/custom_components/myskoda/config_flow.py index 0e1cbaf..2b838a0 100644 --- a/custom_components/myskoda/config_flow.py +++ b/custom_components/myskoda/config_flow.py @@ -10,10 +10,11 @@ from homeassistant.config_entries import ( ConfigEntry, - ConfigFlow as BaseConfigFlow, + ConfigFlow, ConfigFlowResult, OptionsFlow, callback, + SOURCE_REAUTH, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError @@ -53,8 +54,9 @@ async def validate_options_input( async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None: """Check that the inputs are valid.""" - hub = MySkoda(async_get_clientsession(hass), get_default_context()) - + hub = MySkoda( + async_get_clientsession(hass), get_default_context(), mqtt_enabled=False + ) await hub.connect(data["email"], data["password"]) @@ -78,10 +80,23 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None: } -class ConfigFlow(BaseConfigFlow, domain=DOMAIN): +class MySkodaConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for MySkoda.""" - VERSION = 1 + myskoda: MySkoda | None = None + + VERSION = 2 + MINOR_VERSION = 1 + + async def async_connect_myskoda(self, data: dict[str, Any]) -> MySkoda: + """Verify the connection to MySkoda.""" + hub = MySkoda( + async_get_clientsession(self.hass), + get_default_context(), + mqtt_enabled=False, + ) + await hub.connect(data["email"], data["password"]) + return hub async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -95,7 +110,9 @@ async def async_step_user( errors = {} try: - await validate_input(self.hass, user_input) + myskoda = await self.async_connect_myskoda(user_input) + user = await myskoda.get_user() + await self.async_set_unique_id(user.id) except CannotConnect: errors["base"] = "cannot_connect" except InvalidAuth: @@ -103,7 +120,15 @@ async def async_step_user( except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" - else: + + if not errors: + self._abort_if_unique_id_configured() + + if self.source == SOURCE_REAUTH: + self._abort_if_unique_id_mismatch() + return self.async_update_reload_and_abort(self._get_reauth_entry()) + + self._abort_if_unique_id_configured() return self.async_create_entry(title=user_input["email"], data=user_input) # Only called if there was an error.