diff --git a/custom_components/indego/__init__.py b/custom_components/indego/__init__.py index 80e9875..1515916 100644 --- a/custom_components/indego/__init__.py +++ b/custom_components/indego/__init__.py @@ -3,11 +3,12 @@ import asyncio import logging from datetime import datetime, timedelta +from aiohttp.client_exceptions import ClientResponseError import homeassistant.util.dt import voluptuous as vol from homeassistant.core import HomeAssistant, CoreState -from homeassistant.exceptions import HomeAssistantError +from homeassistant.exceptions import HomeAssistantError, ConfigEntryAuthFailed from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_DEVICE_CLASS, @@ -233,6 +234,15 @@ async def load_platforms(): try: await indego_hub.update_generic_data_and_load_platforms(load_platforms) + + except ClientResponseError as exc: + if 400 <= exc.status < 500: + _LOGGER.debug("Received 401, triggering ConfigEntryAuthFailed in HA...") + raise ConfigEntryAuthFailed from exc + + _LOGGER.warning("Login unsuccessful: %s", str(exc)) + return False + except AttributeError as exc: _LOGGER.warning("Login unsuccessful: %s", str(exc)) return False @@ -547,7 +557,6 @@ async def refresh_state(self): except Exception as exc: update_failed = True _LOGGER.warning("Mower state update failed, reason: %s", str(exc)) - #_LOGGER.exception(exc) self.set_online_state(False) if self._shutdown: diff --git a/custom_components/indego/config_flow.py b/custom_components/indego/config_flow.py index c20f599..a1605a9 100644 --- a/custom_components/indego/config_flow.py +++ b/custom_components/indego/config_flow.py @@ -1,4 +1,5 @@ from typing import Final, Any +from collections.abc import Mapping import logging import voluptuous as vol @@ -8,7 +9,7 @@ from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.components.application_credentials import ClientCredential, async_import_client_credential from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.config_entries import OptionsFlowWithConfigEntry, ConfigEntry +from homeassistant.config_entries import OptionsFlowWithConfigEntry, ConfigEntry, ConfigFlowResult, SOURCE_REAUTH, UnknownEntry from homeassistant.core import callback from pyIndego import IndegoAsyncClient @@ -103,7 +104,7 @@ class IndegoFlowHandler(config_entry_oauth2_flow.AbstractOAuth2FlowHandler, doma DOMAIN = DOMAIN VERSION = 1 - _config = {} + _data = {} _options = {} _mower_serials = None @@ -143,7 +144,10 @@ async def async_step_user( async def async_oauth_create_entry(self, data: dict[str, Any]) -> FlowResult: """Test connection and load the available mowers.""" - self._config = data + if self.source == SOURCE_REAUTH: + self._data.update(data) + else: + self._data = data return await self.async_step_advanced() async def async_step_advanced( @@ -154,7 +158,7 @@ async def async_step_advanced( _LOGGER.debug("Testing API access by retrieving available mowers...") api_client = IndegoAsyncClient( - token=self._config["token"]["access_token"], + token=self._data["token"]["access_token"], session=async_get_clientsession(self.hass), raise_request_exceptions=True ) @@ -162,6 +166,9 @@ async def async_step_advanced( self._options[CONF_USER_AGENT] = user_input[CONF_USER_AGENT] api_client.set_default_header(HTTP_HEADER_USER_AGENT, user_input[CONF_USER_AGENT]) + self._options[CONF_EXPOSE_INDEGO_AS_MOWER] = user_input[CONF_EXPOSE_INDEGO_AS_MOWER] + self._options[CONF_EXPOSE_INDEGO_AS_VACUUM] = user_input[CONF_EXPOSE_INDEGO_AS_VACUUM] + try: self._mower_serials = await api_client.get_mowers() _LOGGER.debug("Found mowers in account: %s", self._mower_serials) @@ -173,6 +180,22 @@ async def async_step_advanced( _LOGGER.error("Error while retrieving mower serial in account! Reason: %s", str(exc)) return self.async_abort(reason="connection_error") + if self.source == SOURCE_REAUTH: + if self._data[CONF_MOWER_SERIAL] not in self._mower_serials: + return self.async_abort(reason="mower_not_found") + + self.async_set_unique_id(self._data[CONF_MOWER_SERIAL]) + self._abort_if_unique_id_mismatch() + + _LOGGER.debug("Reauth entry with data: '%s'", self._data) + _LOGGER.debug("Reauth entry with options: '%s'", self._options) + + return self.async_update_reload_and_abort( + self._get_reauth_entry(), + data_updates=self._data, + options=self._options, + ) + return await self.async_step_mower() schema = vol.Schema( @@ -180,14 +203,14 @@ async def async_step_advanced( vol.Optional( CONF_USER_AGENT, description={ - "suggested_value": HTTP_HEADER_USER_AGENT_DEFAULT + "suggested_value": (self._options[CONF_USER_AGENT] if CONF_USER_AGENT in self._options else HTTP_HEADER_USER_AGENT_DEFAULT) }, ): str, vol.Optional( - CONF_EXPOSE_INDEGO_AS_MOWER, default=False + CONF_EXPOSE_INDEGO_AS_MOWER, default=(self._options[CONF_EXPOSE_INDEGO_AS_MOWER] if CONF_EXPOSE_INDEGO_AS_MOWER in self._options else False) ): bool, vol.Optional( - CONF_EXPOSE_INDEGO_AS_VACUUM, default=False + CONF_EXPOSE_INDEGO_AS_VACUUM, default=(self._options[CONF_EXPOSE_INDEGO_AS_VACUUM] if CONF_EXPOSE_INDEGO_AS_VACUUM in self._options else False) ): bool, } ) @@ -203,18 +226,17 @@ async def async_step_mower( await self.async_set_unique_id(user_input[CONF_MOWER_SERIAL]) self._abort_if_unique_id_configured() - self._config[CONF_MOWER_SERIAL] = user_input[CONF_MOWER_SERIAL] - self._config[CONF_MOWER_NAME] = user_input[CONF_MOWER_NAME] + self._data[CONF_MOWER_SERIAL] = user_input[CONF_MOWER_SERIAL] + self._data[CONF_MOWER_NAME] = user_input[CONF_MOWER_NAME] - _LOGGER.debug("Creating entry with config: '%s'", self._config) + _LOGGER.debug("Creating entry with data: '%s'", self._data) _LOGGER.debug("Creating entry with options: '%s'", self._options) - result = self.async_create_entry( + return self.async_create_entry( title=("%s (%s)" % (user_input[CONF_MOWER_NAME], user_input[CONF_MOWER_SERIAL])), - data=self._config + data=self._data, + options=self._options, ) - result["options"] = self._options - return result return self.async_show_form( step_id="mower", @@ -223,6 +245,31 @@ async def async_step_mower( last_step=True ) + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Perform reauth upon an API authentication error.""" + current_config = self._get_reauth_entry() + + self._data = dict(current_config.data) + self._options = dict(current_config.options) + + _LOGGER.debug("Loaded reauth with data: '%s'", self._data) + _LOGGER.debug("Loaded reauth with options: '%s'", self._options) + + 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 that reauth is required.""" + if user_input is None: + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema({}), + ) + return await self.async_step_user() + def _build_mower_options_schema(self): return vol.Schema( { diff --git a/custom_components/indego/manifest.json b/custom_components/indego/manifest.json index 4db9b3d..5841866 100644 --- a/custom_components/indego/manifest.json +++ b/custom_components/indego/manifest.json @@ -7,6 +7,6 @@ "codeowners": ["@jm-73", "@eavanvalkenburg", "@sander1988"], "requirements": ["pyIndego==3.2.2"], "iot_class": "cloud_push", - "version": "5.7.7", + "version": "5.7.8", "loggers": ["custom_components.indego", "pyIndego"] } diff --git a/custom_components/indego/translations/de.json b/custom_components/indego/translations/de.json index d7ab6eb..36fb033 100644 --- a/custom_components/indego/translations/de.json +++ b/custom_components/indego/translations/de.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Dieser Bosch Indego Mähroboter wurde bereits konfiguriert!", "connection_error": "Die Verbindung zur Bosch Indego API ist fehlgeschlagen! Bitte die Known Issues Seite (https://github.com/sander1988/Indego?tab=readme-ov-file#known-issues) für möglche Lösungen nutzen.", - "no_mowers_found": "In diesem Bosch Indego Account wurden keine Mähroboter gefunden!" + "no_mowers_found": "In diesem Bosch Indego Account wurden keine Mähroboter gefunden!", + "reauth_successful": "Re-Authentifizierung war erfolgreich. Zugang zur Bosch API wurde wiederhergestellt." }, "step": { "advanced": { @@ -20,6 +21,10 @@ "mower_name": "Mähroboter Name" }, "description": "Bitte die Seriennummer des Bosch Mähroboters, der hinzugefügt werden soll, auswählen." + }, + "reauth_confirm": { + "title": "Authentifizierung abgelaufen", + "description": "Die Bosch Indego API Authentifizierung ist abgelaufen. Bitte mit der Bosch SingleKey ID neu Authentifizieren." } } }, diff --git a/custom_components/indego/translations/en.json b/custom_components/indego/translations/en.json index 5af69b5..3a3e065 100644 --- a/custom_components/indego/translations/en.json +++ b/custom_components/indego/translations/en.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "This Bosch Indego mower has already been configured!", "connection_error": "The connection to the Bosch Indego API failed! Please use the known issues page (https://github.com/sander1988/Indego?tab=readme-ov-file#known-issues) for possible solutions.", - "no_mowers_found": "No mowers found in this Bosch Indego account!" + "no_mowers_found": "No mowers found in this Bosch Indego account!", + "reauth_successful": "Re-authentication was successful. Access to the Bosch API has been restored." }, "step": { "advanced": { @@ -20,6 +21,10 @@ "mower_name": "Mower name" }, "description": "Please select the serial of the Bosch Mower your would like to add." + }, + "reauth_confirm": { + "title": "Authentication expired", + "description": "The Bosch Indego API authentication has expired. Please re-authenticate using your Bosch SingleKey ID." } } }, diff --git a/custom_components/indego/translations/fr.json b/custom_components/indego/translations/fr.json index 1a090e6..8b5ee02 100644 --- a/custom_components/indego/translations/fr.json +++ b/custom_components/indego/translations/fr.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Cette tondeuse Bosch Indego a déjà été configurée !", "connection_error": "La connexion à l'API Bosch Indego a échoué ! Regardez la page des problèmes connus (https://github.com/sander1988/Indego?tab=readme-ov-file#known-issues) pour trouver une solution éventuelle.", - "no_mowers_found": "Aucune tondeuse n'a été trouvée sur ce compte Bosch Indego !" + "no_mowers_found": "Aucune tondeuse n'a été trouvée sur ce compte Bosch Indego !", + "reauth_successful": "Ré-authentication réussie. L'accès à l'API Bosch a été rétabli." }, "step": { "advanced": { @@ -20,6 +21,10 @@ "mower_name": "Nom de la tondeuse" }, "description": "Sélectionez le numéro de série de la tondeuse Bosch que vous souhaitez ajouter." + }, + "reauth_confirm": { + "title": "Authentication expirée", + "description": "L'authentification de l'API Bosch Indego a expiré. Re-authentifiez-vous avec votre Bosch SingleKey ID." } } }, diff --git a/custom_components/indego/translations/nl.json b/custom_components/indego/translations/nl.json index c02ee89..562f830 100644 --- a/custom_components/indego/translations/nl.json +++ b/custom_components/indego/translations/nl.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Deze Bosch Indego robotmaaier is al geconfigureerd!", "connection_error": "De verbinding met de Bosch Indego API is mislukt! Gebruik de bekende problemen pagina (https://github.com/sander1988/Indego?tab=readme-ov-file#known-issues) voor mogelijke oplossingen.", - "no_mowers_found": "Geen robotmaaiers gevonden in deze Bosch Indego account!" + "no_mowers_found": "Geen robotmaaiers gevonden in deze Bosch Indego account!", + "reauth_successful": "Herauthenticatie is gelukt. De toegang tot de Bosch API is hersteld." }, "step": { "advanced": { @@ -20,6 +21,10 @@ "mower_name": "Robotmaaier naam" }, "description": "Selecteer het serienummer van de Bosch Indego robotmaaier die je toe wilt voegen." + }, + "reauth_confirm": { + "title": "Authenticatie verlopen", + "description": "De Bosch Indego API authenticatie is verlopen. Log a.u.b. opnieuw in m.b.v. je Bosch SingleKey ID." } } },