Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial implementation of reauthentication flow #124

Closed
wants to merge 10 commits into from
88 changes: 77 additions & 11 deletions custom_components/myskoda/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
AuthorizationFailedError,
)
from myskoda.myskoda import TRACE_CONFIG
from myskoda.auth.authorization import CSRFError, TermsAndConditionsError

from myskoda.auth.authorization import (
CSRFError,
TermsAndConditionsError,
)

from .const import COORDINATORS, DOMAIN
from .coordinator import MySkodaDataUpdateCoordinator
Expand All @@ -39,11 +41,26 @@
]


async def async_setup_entry(hass: HomeAssistant, config: ConfigEntry) -> bool:
async def async_connect_myskoda(
hass: HomeAssistant, entry: ConfigEntry, mqtt_enabled: bool = True
) -> MySkoda:
"""Connect to MySkoda."""

trace_configs = []
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(), 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."""

trace_configs = []
if config.options.get("tracing"):
if entry.options.get("tracing"):
trace_configs.append(TRACE_CONFIG)

session = async_create_clientsession(
Expand All @@ -52,15 +69,15 @@ async def async_setup_entry(hass: HomeAssistant, config: ConfigEntry) -> bool:
myskoda = MySkoda(session, get_default_context())

try:
await myskoda.connect(config.data["email"], config.data["password"])
await myskoda.connect(entry.data["email"], entry.data["password"])
except AuthorizationFailedError as exc:
_LOGGER.debug("Authorization with MySkoda failed.")
raise ConfigEntryAuthFailed from exc
except TermsAndConditionsError as exc:
_LOGGER.error(
"New terms and conditions detected while logging in. Please log into the MySkoda app (may require a logout first) to access the new Terms and Conditions. This HomeAssistant integration currently can not continue."
)
async_create_tnc_issue(hass, config.entry_id)
async_create_tnc_issue(hass, entry.entry_id)
raise ConfigEntryNotReady from exc
except (CSRFError, InvalidUrlClientError) as exc:
_LOGGER.debug("An error occurred during login.")
Expand All @@ -69,20 +86,20 @@ async def async_setup_entry(hass: HomeAssistant, config: ConfigEntry) -> bool:
_LOGGER.exception("Login with MySkoda failed for an unknown reason.")
return False

async_delete_tnc_issue(hass, config.entry_id)
async_delete_tnc_issue(hass, entry.entry_id)

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

Expand All @@ -100,3 +117,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
56 changes: 49 additions & 7 deletions custom_components/myskoda/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@
from __future__ import annotations

import logging
from collections.abc import Mapping
from typing import Any

import voluptuous as vol

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
Expand Down Expand Up @@ -58,8 +60,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"])


Expand All @@ -85,10 +88,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
Expand All @@ -102,15 +118,25 @@ 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:
errors["base"] = "invalid_auth"
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.
Expand All @@ -126,6 +152,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."""
Expand Down
Loading