From e7ea00b414e742be83a471b2d78086777db26140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Mandari=C4=87?= <2945713+MislavMandaric@users.noreply.github.com> Date: Mon, 11 Apr 2022 22:03:28 +0200 Subject: [PATCH 1/3] feat: Adds reauth handler in config flow which is triggered by unauthorized error from the API. --- .../vaillant_vsmart/config_flow.py | 74 ++++++++++++++----- custom_components/vaillant_vsmart/entity.py | 10 ++- 2 files changed, 61 insertions(+), 23 deletions(-) diff --git a/custom_components/vaillant_vsmart/config_flow.py b/custom_components/vaillant_vsmart/config_flow.py index 5c71c31..15e8c18 100644 --- a/custom_components/vaillant_vsmart/config_flow.py +++ b/custom_components/vaillant_vsmart/config_flow.py @@ -12,14 +12,12 @@ CONF_TOKEN, CONF_USERNAME, ) -from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.httpx_client import get_async_client from vaillant_netatmo_api import ( ApiException, AuthClient, RequestClientException, - Token, TokenStore, ) import voluptuous as vol @@ -43,8 +41,23 @@ async def async_step_user( ) -> FlowResult: """Handle a flow initialized by the user.""" + return await self._async_step_all("user", "already_configured", user_input) + + async def async_step_reauth( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a reauth flow initialized when token used for API requests is invalid.""" + + return await self._async_step_all("reauth", "reauth_successful", user_input) + + async def _async_step_all( + self, step_id: str, abort_reason: str, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Generic flow step that handles both user and reauth steps.""" + if user_input is None: - return await self._show_config_form() + user_input = self._init_user_input() + return self._show_config_form(step_id, user_input) errors = {} @@ -61,20 +74,26 @@ async def async_step_user( errors["base"] = "unknown" if errors: - return await self._show_config_form(user_input, errors) + return self._show_config_form(step_id, user_input, errors) - await self.async_set_unique_id(user_input[CONF_USERNAME]) - self._abort_if_unique_id_configured() + existing_entry = await self.async_set_unique_id(user_input[CONF_USERNAME]) + if existing_entry: + self.hass.config_entries.async_update_entry(existing_entry, data=data) + await self.hass.config_entries.async_reload(existing_entry.entry_id) + return self.async_abort(reason=abort_reason) return self.async_create_entry(title=user_input[CONF_USERNAME], data=data) - async def _show_config_form( - self, user_input: dict[str, Any] = {}, errors: dict[str, str] = None - ): + def _show_config_form( + self, + step_id: str, + user_input: dict[str, Any], + errors: dict[str, str] = None, + ) -> FlowResult: """Show the configuration form to edit location data.""" return self.async_show_form( - step_id="user", + step_id=step_id, data_schema=vol.Schema( { vol.Required( @@ -101,14 +120,29 @@ async def _show_config_form( errors=errors, ) + def _init_user_input(self) -> dict[str, Any]: + """Initializes user input data from configs init data.""" + + data = self.init_data + + if data is None: + return {} + + return { + CONF_CLIENT_ID: data.get(CONF_CLIENT_ID), + CONF_CLIENT_SECRET: data.get(CONF_CLIENT_SECRET), + CONF_USER_PREFIX: data.get(CONF_USER_PREFIX), + CONF_APP_VERSION: data.get(CONF_APP_VERSION), + } + async def _get_config_storage_data( self, user_input: dict[str, Any] ) -> dict[str, Any]: """Get config storage data from user input form data.""" token_store = TokenStore( - user_input[CONF_CLIENT_ID], - user_input[CONF_CLIENT_SECRET], + user_input.get(CONF_CLIENT_ID), + user_input.get(CONF_CLIENT_SECRET), None, None, ) @@ -119,16 +153,16 @@ async def _get_config_storage_data( ) await client.async_token( - user_input[CONF_USERNAME], - user_input[CONF_PASSWORD], - user_input[CONF_USER_PREFIX], - user_input[CONF_APP_VERSION], + user_input.get(CONF_USERNAME), + user_input.get(CONF_PASSWORD), + user_input.get(CONF_USER_PREFIX), + user_input.get(CONF_APP_VERSION), ) return { - CONF_CLIENT_ID: user_input[CONF_CLIENT_ID], - CONF_CLIENT_SECRET: user_input[CONF_CLIENT_SECRET], - CONF_USER_PREFIX: user_input[CONF_USER_PREFIX], - CONF_APP_VERSION: user_input[CONF_APP_VERSION], + CONF_CLIENT_ID: user_input.get(CONF_CLIENT_ID), + CONF_CLIENT_SECRET: user_input.get(CONF_CLIENT_SECRET), + CONF_USER_PREFIX: user_input.get(CONF_USER_PREFIX), + CONF_APP_VERSION: user_input.get(CONF_APP_VERSION), CONF_TOKEN: token_store.token.serialize(), } diff --git a/custom_components/vaillant_vsmart/entity.py b/custom_components/vaillant_vsmart/entity.py index 0b737da..beab136 100644 --- a/custom_components/vaillant_vsmart/entity.py +++ b/custom_components/vaillant_vsmart/entity.py @@ -5,6 +5,7 @@ from typing import Any from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -19,6 +20,7 @@ MeasurementItem, MeasurementType, MeasurementScale, + RequestUnauthorizedException, ) from .const import DOMAIN @@ -83,9 +85,11 @@ async def _update_method(self): self._debug_log(devices) return VaillantData(self._client, devices) - except ApiException as e: - _LOGGER.exception(e) - raise UpdateFailed(f"Error communicating with API: {e}") from e + except RequestUnauthorizedException as ex: + raise ConfigEntryAuthFailed from ex + except ApiException as ex: + _LOGGER.exception(ex) + raise UpdateFailed(f"Error communicating with API: {ex}") from ex async def _get_temperature_measurements_for_all_devices( self, devices: list[Device] From 7cd0a9bd6670701719e5c99b690b72d55a5f5381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Mandari=C4=87?= <2945713+MislavMandaric@users.noreply.github.com> Date: Mon, 11 Apr 2022 22:04:00 +0200 Subject: [PATCH 2/3] chore: Improves logging. --- custom_components/vaillant_vsmart/climate.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/custom_components/vaillant_vsmart/climate.py b/custom_components/vaillant_vsmart/climate.py index 07a1719..a916886 100644 --- a/custom_components/vaillant_vsmart/climate.py +++ b/custom_components/vaillant_vsmart/climate.py @@ -126,8 +126,8 @@ def hvac_action(self) -> str: try: if self._module.measured.temperature < self._module.measured.setpoint_temp: return CURRENT_HVAC_HEAT - except TypeError as ex: - _LOGGER.exception(ex) + except TypeError: + pass return CURRENT_HVAC_IDLE @@ -155,7 +155,7 @@ def preset_mode(self) -> str: async def async_set_hvac_mode(self, hvac_mode: str) -> None: """Select new HVAC operation mode.""" - _LOGGER.debug(f"Setting HVAC mode to: {hvac_mode}") + _LOGGER.debug("Setting HVAC mode to: %s", hvac_mode) if hvac_mode == HVAC_MODE_OFF: try: @@ -213,7 +213,7 @@ async def async_set_hvac_mode(self, hvac_mode: str) -> None: async def async_set_preset_mode(self, preset_mode: str) -> None: """Select new HVAC preset mode.""" - _LOGGER.debug(f"Setting HVAC preset mode to: {preset_mode}") + _LOGGER.debug("Setting HVAC preset mode to: %s", preset_mode) if self._device.system_mode == SystemMode.FROSTGUARD: return @@ -266,7 +266,7 @@ async def async_set_temperature(self, **kwargs) -> None: if new_temperature is None: return - _LOGGER.debug(f"Setting target temperature to: {new_temperature}") + _LOGGER.debug("Setting target temperature to: %s", new_temperature) endtime = datetime.datetime.now() + datetime.timedelta( minutes=self._device.setpoint_default_duration From da6b9f9c3284713f7bc07cd0a85f8d32f6f64eb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Mandari=C4=87?= <2945713+MislavMandaric@users.noreply.github.com> Date: Mon, 11 Apr 2022 22:04:48 +0200 Subject: [PATCH 3/3] feat: Improves existing translations by using predefined HA Core translations and adds new reauth translations. --- .../vaillant_vsmart/translations/en.json | 22 +++++++++++++++---- .../vaillant_vsmart/translations/hr.json | 22 +++++++++++++++---- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/custom_components/vaillant_vsmart/translations/en.json b/custom_components/vaillant_vsmart/translations/en.json index 7d8cf90..351c058 100644 --- a/custom_components/vaillant_vsmart/translations/en.json +++ b/custom_components/vaillant_vsmart/translations/en.json @@ -2,7 +2,18 @@ "config": { "step": { "user": { - "description": "If you need help with the configuration have a look here: https://github.com/MislavMandaric/home-assistant-vaillant-vsmart", + "description": "If you're setting the component for the first time and need some guidelines, check out the [community page](https://community.home-assistant.io/t/added-support-for-vaillant-thermostat-how-to-integrate-in-official-release/31858). You can find out how to extract client ID and client secret there.", + "data": { + "client_id": "Client ID", + "client_secret": "Client secret", + "username": "Username", + "password": "Password", + "user_prefix": "User prefix", + "app_version": "App version" + } + }, + "reauth": { + "description": "Tokens used by the component expired and you need to re-authenticate with the API again. Since your credentials are never stored, you need to enter them again.", "data": { "client_id": "Client ID", "client_secret": "Client secret", @@ -14,11 +25,14 @@ } }, "error": { - "unknown": "Something went wrong when authenticating with the API." + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_in_progress": "Integration for provided user is already in progress.", - "already_configured": "Integration for provided user is already configured." + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" } } } \ No newline at end of file diff --git a/custom_components/vaillant_vsmart/translations/hr.json b/custom_components/vaillant_vsmart/translations/hr.json index 9dabff6..193e8be 100644 --- a/custom_components/vaillant_vsmart/translations/hr.json +++ b/custom_components/vaillant_vsmart/translations/hr.json @@ -2,7 +2,18 @@ "config": { "step": { "user": { - "description": "Pomoć oko konfiguracije integracije potražite na ovom linku: https://github.com/MislavMandaric/home-assistant-vaillant-vsmart", + "description": "Više informacija o postavkama komponente možete pronaći na [stranicama zajednice](https://community.home-assistant.io/t/added-support-for-vaillant-thermostat-how-to-integrate-in-official-release/31858), uključujući i način na koji možete saznati ID i tajni kod klijenta.", + "data": { + "client_id": "ID klijenta", + "client_secret": "Tajni kod klijenta", + "username": "Korisničko ime", + "password": "Lozinka", + "user_prefix": "Korisnički prefiks", + "app_version": "Verzija aplikacije" + } + }, + "reauth": { + "description": "Tokeni za komunikaciju koje komponenta koristi su istekli i potrebna je ponovna autentikacija. Korisničko ime i lozinka se ne spremaju te ih je potrebno ponovno unijeti.", "data": { "client_id": "ID klijenta", "client_secret": "Tajni kod klijenta", @@ -14,11 +25,14 @@ } }, "error": { - "unknown": "Došlo je do pogreške u komunikaciji sa serverom." + "invalid_auth": "Pogreška prilikom autentikacije", + "cannot_connect": "Pogreška u spajanju", + "unknown": "Neočekivana pogreška" }, "abort": { - "already_in_progress": "Integracija je već u tijeku.", - "already_configured": "Integracija je već postavljena." + "reauth_successful": "Uspješna ponovna autentikacija.", + "already_in_progress": "Konfiguracija je već u tijeku.", + "already_configured": "Račun je otprije konfiguriran." } } } \ No newline at end of file