From 955830ec2cca7c4142af0a42222fac46f183870d Mon Sep 17 00:00:00 2001 From: Konstantin Pivnov Date: Mon, 11 Nov 2024 16:02:48 +0300 Subject: [PATCH 1/7] removed raising exception --- custom_components/tion/config_flow.py | 38 +++++++++++++-------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/custom_components/tion/config_flow.py b/custom_components/tion/config_flow.py index 504fee5..6a49996 100644 --- a/custom_components/tion/config_flow.py +++ b/custom_components/tion/config_flow.py @@ -15,7 +15,6 @@ from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult -from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_create_clientsession from .client import TionClient @@ -70,25 +69,24 @@ async def async_step_user( if auth_data is None: errors["base"] = "invalid_auth" - raise ConfigEntryAuthFailed - - sha256_hash = hashlib.new("sha256") - sha256_hash.update(user_input[CONF_USERNAME].encode()) - unique_id = f"{sha256_hash.hexdigest()}" - - # Checks that the device is actually unique, otherwise abort - await self.async_set_unique_id(unique_id) - self._abort_if_unique_id_configured() - - return self.async_create_entry( - title=user_input[CONF_USERNAME], - data={ - CONF_USERNAME: user_input[CONF_USERNAME], - CONF_PASSWORD: user_input[CONF_PASSWORD], - CONF_SCAN_INTERVAL: interval, - AUTH_DATA: auth_data, - }, - ) + else: + sha256_hash = hashlib.new("sha256") + sha256_hash.update(user_input[CONF_USERNAME].encode()) + unique_id = f"{sha256_hash.hexdigest()}" + + # Checks that the device is actually unique, otherwise abort + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=user_input[CONF_USERNAME], + data={ + CONF_USERNAME: user_input[CONF_USERNAME], + CONF_PASSWORD: user_input[CONF_PASSWORD], + CONF_SCAN_INTERVAL: interval, + AUTH_DATA: auth_data, + }, + ) return self.async_show_form( step_id="user", From a7522fa8d19a9466a55b8904c1de85fc788ae5fc Mon Sep 17 00:00:00 2001 From: Konstantin Pivnov Date: Tue, 12 Nov 2024 12:59:08 +0300 Subject: [PATCH 2/7] added reauth to config flow --- custom_components/tion/__init__.py | 19 +++--- custom_components/tion/client.py | 68 ++++++++++----------- custom_components/tion/config_flow.py | 62 +++++++++++++------ custom_components/tion/strings.json | 9 +++ custom_components/tion/translations/ru.json | 8 +++ 5 files changed, 105 insertions(+), 61 deletions(-) diff --git a/custom_components/tion/__init__.py b/custom_components/tion/__init__.py index a00e959..5a47245 100644 --- a/custom_components/tion/__init__.py +++ b/custom_components/tion/__init__.py @@ -12,6 +12,7 @@ CONF_USERNAME, ) from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_create_clientsession import homeassistant.helpers.config_validation as cv @@ -39,7 +40,7 @@ vol.Required(CONF_PASSWORD): cv.string, vol.Optional( CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL - ): cv.time_period, + ): cv.positive_timedelta, vol.Optional(CONF_FILE_PATH, default=DEFAULT_AUTH_FILENAME): cv.string, } ) @@ -102,11 +103,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if hass.data.get(DOMAIN) is None: hass.data.setdefault(DOMAIN, {}) - if entry.options: - entry_data = dict(entry.data) - entry_data.update(entry.options) - hass.config_entries.async_update_entry(entry, data=entry_data, options={}) - async def update_auth_data(**kwargs): hass.config_entries.async_update_entry(entry, data=kwargs) @@ -115,13 +111,18 @@ async def update_auth_data(**kwargs): session, username=entry.data.get(CONF_USERNAME), password=entry.data.get(CONF_PASSWORD), - min_update_interval_sec=entry.data.get(CONF_SCAN_INTERVAL), + min_update_interval_sec=entry.options.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL + ), auth=entry.data.get(AUTH_DATA), ) client.add_update_listener(update_auth_data) - assert await client.authorization, "Couldn't get authorisation data!" - _LOGGER.info("Api initialized with authorization %s", await client.authorization) + auth_data = await client.authorization + if not auth_data: + raise ConfigEntryAuthFailed("Couldn't get authorisation data.") + + _LOGGER.info("Api initialized with authorization %s", auth_data) hass.data[DOMAIN][entry.entry_id] = client diff --git a/custom_components/tion/client.py b/custom_components/tion/client.py index f1fe1c0..1fc659f 100644 --- a/custom_components/tion/client.py +++ b/custom_components/tion/client.py @@ -154,7 +154,7 @@ async def _headers(self): "Accept": "application/json, text/plain, */*", "Accept-Encoding": "gzip, deflate", "Accept-Language": "ru-RU", - "Authorization": await self.authorization, + "Authorization": self._authorization, "Connection": "Keep-Alive", "Content-Type": "application/json", "Host": "api2.magicair.tion.ru", @@ -170,9 +170,10 @@ async def authorization(self) -> str: if await self._get_authorization(): return self._authorization - return None + elif await self._get_data(): + return self._authorization - return self._authorization + return None def add_update_listener(self, coro): """Add entry data update listener function.""" @@ -214,42 +215,41 @@ async def _get_authorization(self): ) return False - async def get_location_data(self, force=False) -> bool: - """Get locations data from Tion API.""" + async def _get_data(self): + response = await self._session.get( + url=f"{self._API_ENDPOINT}{self._LOCATION_URL}", + headers=await self._headers, + timeout=10, + ) - async def get_data(): - response = await self._session.get( - url=f"{self._API_ENDPOINT}{self._LOCATION_URL}", - headers=await self._headers, - timeout=10, + if response.status == 200: + self._locations = [ + TionLocation(location) for location in await response.json() + ] + self._last_update = time() + + _LOGGER.debug( + "TionClient: location data has been updated (%s)", self._last_update ) + return True - if response.status == 200: - self._locations = [ - TionLocation(location) for location in await response.json() - ] - self._last_update = time() + if response.status == 401: + _LOGGER.info("TionClient: need to get new authorization") + if await self._get_authorization(): + return await self._get_data() - _LOGGER.debug( - "TionClient: location data has been updated (%s)", self._last_update - ) - return True - - if response.status == 401: - _LOGGER.info("TionClient: need to get new authorization") - if await self._get_authorization(): - return await get_data() - - _LOGGER.error("TionClient: authorization failed!") - else: - _LOGGER.error( - "TionClient: response while getting location data: status code: %s, content:\n%s", - response.status, - await response.json(), - ) + _LOGGER.error("TionClient: authorization failed!") + else: + _LOGGER.error( + "TionClient: response while getting location data: status code: %s, content:\n%s", + response.status, + await response.json(), + ) - return False + return False + async def get_location_data(self, force=False) -> bool: + """Get locations data from Tion API.""" async with self._temp_lock: if not force and (time() - self._last_update) < self._min_update_interval: _LOGGER.debug( @@ -257,7 +257,7 @@ async def get_data(): ) return self._locations is not None - return await get_data() + return await self._get_data() async def get_zone(self, guid: str, force=False) -> TionZone | None: """Get zone data by guid from Tion API.""" diff --git a/custom_components/tion/config_flow.py b/custom_components/tion/config_flow.py index 6a49996..9d9df43 100644 --- a/custom_components/tion/config_flow.py +++ b/custom_components/tion/config_flow.py @@ -1,5 +1,6 @@ """Adds config flow (UI flow) for Tion component.""" +from collections.abc import Mapping import hashlib import logging from typing import Any @@ -54,17 +55,10 @@ async def async_step_user( if user_input is not None: self._async_abort_entries_match({CONF_USERNAME: user_input[CONF_USERNAME]}) - try: - interval = int(user_input.get(CONF_SCAN_INTERVAL)) - except ValueError: - interval = DEFAULT_SCAN_INTERVAL - except TypeError: - interval = DEFAULT_SCAN_INTERVAL - auth_data = await self._get_auth_data( user_input[CONF_USERNAME], user_input[CONF_PASSWORD], - interval, + DEFAULT_SCAN_INTERVAL, ) if auth_data is None: @@ -74,7 +68,6 @@ async def async_step_user( sha256_hash.update(user_input[CONF_USERNAME].encode()) unique_id = f"{sha256_hash.hexdigest()}" - # Checks that the device is actually unique, otherwise abort await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured() @@ -83,7 +76,6 @@ async def async_step_user( data={ CONF_USERNAME: user_input[CONF_USERNAME], CONF_PASSWORD: user_input[CONF_PASSWORD], - CONF_SCAN_INTERVAL: interval, AUTH_DATA: auth_data, }, ) @@ -94,9 +86,6 @@ async def async_step_user( { vol.Required(CONF_USERNAME, default=""): str, vol.Required(CONF_PASSWORD, default=""): str, - vol.Required( - CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL - ): vol.Coerce(int), } ), errors=errors, @@ -111,6 +100,47 @@ async def async_step_import( ) return await self.async_step_user(import_config) + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Perform reauth upon an API authentication error.""" + 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.""" + errors: dict[str, str] = {} + if user_input is not None: + auth_data = await self._get_auth_data( + user_input[CONF_USERNAME], + user_input[CONF_PASSWORD], + DEFAULT_SCAN_INTERVAL, + ) + + if auth_data is None: + errors["base"] = "invalid_auth" + else: + return self.async_create_entry( + title=user_input[CONF_USERNAME], + data={ + CONF_USERNAME: user_input[CONF_USERNAME], + CONF_PASSWORD: user_input[CONF_PASSWORD], + AUTH_DATA: auth_data, + }, + ) + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_USERNAME, default=""): str, + vol.Required(CONF_PASSWORD, default=""): str, + } + ), + errors=errors, + ) + class TionOptionsFlow(OptionsFlow): """Tion options flow handler.""" @@ -129,13 +159,9 @@ async def async_step_init(self, user_input=None): step_id="init", data_schema=vol.Schema( { - vol.Required( - CONF_PASSWORD, - default=self.config_entry.data.get(CONF_PASSWORD), - ): str, vol.Required( CONF_SCAN_INTERVAL, - default=self.config_entry.data.get(CONF_SCAN_INTERVAL), + default=self.config_entry.options.get(CONF_SCAN_INTERVAL), ): vol.Coerce(int), } ), diff --git a/custom_components/tion/strings.json b/custom_components/tion/strings.json index 388885a..1a73580 100644 --- a/custom_components/tion/strings.json +++ b/custom_components/tion/strings.json @@ -16,6 +16,15 @@ } } }, + "reauth_confirm": { + "title": "Reauthentification required", + "description": "Please re-enter your Tion account credentials", + "data": { + "name": "[%key:common::config_flow::data::name%]", + "username": "Username", + "password": "Password" + } + }, "abort": { "already_configured": "Account is already configured" }, diff --git a/custom_components/tion/translations/ru.json b/custom_components/tion/translations/ru.json index f8e36a7..63d8669 100644 --- a/custom_components/tion/translations/ru.json +++ b/custom_components/tion/translations/ru.json @@ -14,6 +14,14 @@ } } }, + "reauth_confirm": { + "title": "Необходима повторная аутентификация", + "description": "Введите актуальные данные учётной записи Tion", + "data": { + "username": "Логин", + "password": "Пароль" + } + }, "abort": { "already_configured": "Учётная запись уже была добавлена" }, From 6f049b92f279835aa9e9439400426a3aa0d95f21 Mon Sep 17 00:00:00 2001 From: Konstantin Pivnov Date: Tue, 12 Nov 2024 18:32:03 +0300 Subject: [PATCH 3/7] fixed default_interval const --- custom_components/tion/const.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/custom_components/tion/const.py b/custom_components/tion/const.py index 9de6cde..23cde41 100644 --- a/custom_components/tion/const.py +++ b/custom_components/tion/const.py @@ -1,13 +1,12 @@ """Constant variables used by integration.""" -from datetime import timedelta from enum import StrEnum from homeassistant.const import Platform DOMAIN = "tion" DEFAULT_AUTH_FILENAME = "tion_auth" -DEFAULT_SCAN_INTERVAL = timedelta(minutes=1) +DEFAULT_SCAN_INTERVAL = 60 AUTH_DATA = "auth" MANUFACTURER = "Tion" PLATFORMS = [ From d60352b3d4faef83fbef46734530b4f9b4cd5e18 Mon Sep 17 00:00:00 2001 From: Konstantin Pivnov Date: Tue, 12 Nov 2024 18:38:51 +0300 Subject: [PATCH 4/7] fixed strings translations --- custom_components/tion/manifest.json | 2 +- custom_components/tion/strings.json | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/custom_components/tion/manifest.json b/custom_components/tion/manifest.json index a2ff113..28e1953 100644 --- a/custom_components/tion/manifest.json +++ b/custom_components/tion/manifest.json @@ -17,5 +17,5 @@ "iot_class": "cloud_polling", "issue_tracker": "https://github.com/vaproloff/tion_home_assistant/issues", "requirements": [], - "version": "2024.11.2" + "version": "2024.11.3" } \ No newline at end of file diff --git a/custom_components/tion/strings.json b/custom_components/tion/strings.json index 1a73580..914ca05 100644 --- a/custom_components/tion/strings.json +++ b/custom_components/tion/strings.json @@ -20,7 +20,6 @@ "title": "Reauthentification required", "description": "Please re-enter your Tion account credentials", "data": { - "name": "[%key:common::config_flow::data::name%]", "username": "Username", "password": "Password" } From 6a50ae87b05f9718c6d3078a49a9ac66152add75 Mon Sep 17 00:00:00 2001 From: Konstantin Pivnov Date: Tue, 12 Nov 2024 18:45:26 +0300 Subject: [PATCH 5/7] attempted to fix strings translations again --- custom_components/tion/strings.json | 8 +------- custom_components/tion/translations/ru.json | 8 +------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/custom_components/tion/strings.json b/custom_components/tion/strings.json index 914ca05..bdf1af0 100644 --- a/custom_components/tion/strings.json +++ b/custom_components/tion/strings.json @@ -8,16 +8,11 @@ "data": { "name": "[%key:common::config_flow::data::name%]", "username": "Username", - "password": "Password", - "scan_interval": "API poll interval" - }, - "data_description": { - "scan_interval": "Tion devices data minimum update period." + "password": "Password" } } }, "reauth_confirm": { - "title": "Reauthentification required", "description": "Please re-enter your Tion account credentials", "data": { "username": "Username", @@ -36,7 +31,6 @@ "step": { "init": { "data": { - "password": "Password", "scan_interval": "API poll interval" }, "data_description": { diff --git a/custom_components/tion/translations/ru.json b/custom_components/tion/translations/ru.json index 63d8669..7154608 100644 --- a/custom_components/tion/translations/ru.json +++ b/custom_components/tion/translations/ru.json @@ -6,16 +6,11 @@ "description": "Введите данные учётной записи Tion", "data": { "username": "Логин", - "password": "Пароль", - "scan_interval": "Интервал обновления" - }, - "data_description": { - "scan_interval": "Минимальный период обновления данных устройств Tion от API." + "password": "Пароль" } } }, "reauth_confirm": { - "title": "Необходима повторная аутентификация", "description": "Введите актуальные данные учётной записи Tion", "data": { "username": "Логин", @@ -34,7 +29,6 @@ "step": { "init": { "data": { - "password": "Пароль", "scan_interval": "Интервал обновления" }, "data_description": { From a74c8377870dc36740cd8a8985cc9a7a0112f8cb Mon Sep 17 00:00:00 2001 From: Konstantin Pivnov Date: Tue, 12 Nov 2024 18:50:23 +0300 Subject: [PATCH 6/7] attempted to fix strings translations again --- custom_components/tion/strings.json | 1 - custom_components/tion/translations/ru.json | 1 - 2 files changed, 2 deletions(-) diff --git a/custom_components/tion/strings.json b/custom_components/tion/strings.json index bdf1af0..5b43524 100644 --- a/custom_components/tion/strings.json +++ b/custom_components/tion/strings.json @@ -13,7 +13,6 @@ } }, "reauth_confirm": { - "description": "Please re-enter your Tion account credentials", "data": { "username": "Username", "password": "Password" diff --git a/custom_components/tion/translations/ru.json b/custom_components/tion/translations/ru.json index 7154608..bcb2e41 100644 --- a/custom_components/tion/translations/ru.json +++ b/custom_components/tion/translations/ru.json @@ -11,7 +11,6 @@ } }, "reauth_confirm": { - "description": "Введите актуальные данные учётной записи Tion", "data": { "username": "Логин", "password": "Пароль" From 62d318598a276feaf7c1b1dfbc8af1a119ed23e9 Mon Sep 17 00:00:00 2001 From: Konstantin Pivnov Date: Tue, 12 Nov 2024 18:51:55 +0300 Subject: [PATCH 7/7] removed reauth_step strings translations again --- custom_components/tion/strings.json | 6 ------ custom_components/tion/translations/ru.json | 6 ------ 2 files changed, 12 deletions(-) diff --git a/custom_components/tion/strings.json b/custom_components/tion/strings.json index 5b43524..b46aa1c 100644 --- a/custom_components/tion/strings.json +++ b/custom_components/tion/strings.json @@ -12,12 +12,6 @@ } } }, - "reauth_confirm": { - "data": { - "username": "Username", - "password": "Password" - } - }, "abort": { "already_configured": "Account is already configured" }, diff --git a/custom_components/tion/translations/ru.json b/custom_components/tion/translations/ru.json index bcb2e41..b5ad074 100644 --- a/custom_components/tion/translations/ru.json +++ b/custom_components/tion/translations/ru.json @@ -10,12 +10,6 @@ } } }, - "reauth_confirm": { - "data": { - "username": "Логин", - "password": "Пароль" - } - }, "abort": { "already_configured": "Учётная запись уже была добавлена" },