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

Added reauth config flow #19

Merged
merged 8 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions custom_components/tion/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
}
)
Expand Down Expand Up @@ -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)

Expand All @@ -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

Expand Down
68 changes: 34 additions & 34 deletions custom_components/tion/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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."""
Expand Down Expand Up @@ -214,50 +215,49 @@ 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(
"TionClient: location data already updated recently. Skipping request"
)
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."""
Expand Down
62 changes: 44 additions & 18 deletions custom_components/tion/config_flow.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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()

Expand All @@ -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,
},
)
Expand All @@ -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,
Expand All @@ -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."""
Expand All @@ -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),
}
),
Expand Down
3 changes: 1 addition & 2 deletions custom_components/tion/const.py
Original file line number Diff line number Diff line change
@@ -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 = [
Expand Down
2 changes: 1 addition & 1 deletion custom_components/tion/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
7 changes: 1 addition & 6 deletions custom_components/tion/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,7 @@
"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"
}
}
},
Expand All @@ -28,7 +24,6 @@
"step": {
"init": {
"data": {
"password": "Password",
"scan_interval": "API poll interval"
},
"data_description": {
Expand Down
7 changes: 1 addition & 6 deletions custom_components/tion/translations/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,7 @@
"description": "Введите данные учётной записи Tion",
"data": {
"username": "Логин",
"password": "Пароль",
"scan_interval": "Интервал обновления"
},
"data_description": {
"scan_interval": "Минимальный период обновления данных устройств Tion от API."
"password": "Пароль"
}
}
},
Expand All @@ -26,7 +22,6 @@
"step": {
"init": {
"data": {
"password": "Пароль",
"scan_interval": "Интервал обновления"
},
"data_description": {
Expand Down