Skip to content

Commit

Permalink
Fix config-flow issue (#104)
Browse files Browse the repository at this point in the history
Connection updates

 * Update configflow to better identify when issues occur
 * Update data collection (configflow + data) to specify a user-defined timeout
  • Loading branch information
Expl0dingBanana authored Sep 29, 2024
1 parent 9a7ebd9 commit 321bd0c
Show file tree
Hide file tree
Showing 9 changed files with 100 additions and 53 deletions.
27 changes: 24 additions & 3 deletions custom_components/hubspace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
from dataclasses import dataclass

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceEntry
from hubspace_async import HubSpaceConnection

from .const import UPDATE_INTERVAL_OBSERVATION
from .const import DEFAULT_TIMEOUT, UPDATE_INTERVAL_OBSERVATION
from .coordinator import HubSpaceDataUpdateCoordinator

_LOGGER = logging.getLogger(__name__)
Expand All @@ -35,7 +35,7 @@ class HubSpaceData:
type HubSpaceConfigEntry = ConfigEntry[HubSpaceData]


async def async_setup_entry(hass: HomeAssistant, entry: HubSpaceData) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up HubSpace as config entry."""
websession = async_get_clientsession(hass)
conn = HubSpaceConnection(
Expand All @@ -45,6 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: HubSpaceData) -> bool:
coordinator_hubspace = HubSpaceDataUpdateCoordinator(
hass,
conn,
entry.data[CONF_TIMEOUT],
[],
[],
UPDATE_INTERVAL_OBSERVATION,
Expand Down Expand Up @@ -75,3 +76,23 @@ async def async_remove_config_entry_device(
if x.device_id == device_id
]
)


async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
_LOGGER.debug(
"Migrating configuration from version %s.%s",
config_entry.version,
config_entry.minor_version,
)
if config_entry.version == 1:
new_data = {**config_entry.data}
new_data[CONF_TIMEOUT] = DEFAULT_TIMEOUT
hass.config_entries.async_update_entry(
config_entry, data=new_data, version=2, minor_version=0
)
_LOGGER.debug(
"Migration to configuration version %s.%s successful",
config_entry.version,
config_entry.minor_version,
)
return True
86 changes: 53 additions & 33 deletions custom_components/hubspace/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,66 +2,86 @@

from __future__ import annotations

import contextlib
import logging
from asyncio import timeout
from typing import Any
from typing import Any, Optional

import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from hubspace_async import HubSpaceConnection
from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME
from hubspace_async import HubSpaceConnection, InvalidAuth, InvalidResponse

from .const import DOMAIN
from .const import DEFAULT_TIMEOUT, DOMAIN
from .const import VERSION_MAJOR as const_maj
from .const import VERSION_MINOR as const_min

_LOGGER = logging.getLogger(__name__)

LOGIN_REQS = {
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
LOGIN_SCHEMA = vol.Schema(LOGIN_REQS)
OPTIONAL = {
vol.Required(CONF_TIMEOUT): int,
}
LOGIN_SCHEMA = vol.Schema(LOGIN_REQS | OPTIONAL)
RECONFIG_SCHEMA = vol.Schema(OPTIONAL)


class HubSpaceConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for HubSpace"""

VERSION = 1
VERSION = const_maj
MINOR_VERSION = const_min
username: str
password: str
conn: HubSpaceConnection

async def perform_auth(
self, step_id: str, user_input: dict[str, Any] | None = None
) -> config_entries.ConfigFlowResult:
async def validate_auth(
self, user_input: dict[str, Any] | None = None
) -> Optional[str]:
"""Validate and save auth"""
err_type = None
try:
async with timeout(user_input[CONF_TIMEOUT] / 1000):
self.conn = HubSpaceConnection(
user_input[CONF_USERNAME],
user_input[CONF_PASSWORD],
)
await self.conn.get_account_id()
except TimeoutError:
err_type = "cannot_connect"
except (InvalidAuth, InvalidResponse):
err_type = "invalid_auth"
except Exception:
_LOGGER.exception("Unexpected exception")
err_type = "unknown"
return err_type

errors = {}
if not user_input:
return self.async_show_form(
step_id=step_id,
data_schema=LOGIN_SCHEMA,
errors=errors,
)
else:
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> config_entries.ConfigFlowResult:
"""Handle a flow initialized by the user."""
errors: dict[str, str] = {}
if user_input is not None:
try:
async with timeout(10):
self.conn = HubSpaceConnection(
user_input[CONF_USERNAME], user_input[CONF_PASSWORD]
)
await self.conn.get_account_id()
except TimeoutError:
errors["base"] = "cannot_connect"
except Exception:
errors["unknown"] = "generic"
else:
user_input[CONF_TIMEOUT] = user_input[CONF_TIMEOUT] or DEFAULT_TIMEOUT
except ValueError:
errors["base"] = "invalid_timeout"
if not (err_type := await self.validate_auth(user_input)):
await self.async_set_unique_id(
await self.conn.account_id, raise_on_progress=False
)
# self._abort_if_unique_id_configured()
await self.conn.client.close()
return self.async_create_entry(title=DOMAIN, data=user_input)

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> config_entries.ConfigFlowResult:
"""Handle a flow initialized by the user."""
return await self.perform_auth("user", user_input)
else:
errors["base"] = err_type
with contextlib.suppress(Exception):
await self.conn.client.close()
return self.async_show_form(
step_id="user",
data_schema=LOGIN_SCHEMA,
errors=errors,
)
4 changes: 4 additions & 0 deletions custom_components/hubspace/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
CONF_DEBUG: Final = "debug"
UPDATE_INTERVAL_OBSERVATION = timedelta(seconds=30)
HUB_IDENTIFIER: Final[str] = "hubspace_debug"
DEFAULT_TIMEOUT: Final[int] = 10000

VERSION_MAJOR: Final[int] = 2
VERSION_MINOR: Final[int] = 0


ENTITY_FAN: Final[str] = "fan"
Expand Down
6 changes: 4 additions & 2 deletions custom_components/hubspace/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,14 @@ def __init__(
self,
hass: HomeAssistant,
conn: hubspace_async.HubSpaceConnection,
timeout: int,
friendly_names: list[str],
room_names: list[str],
update_interval: timedelta,
) -> None:
"""Initialize."""
self.conn = conn
self.timeout = timeout
self.tracked_devices: list[hubspace_async.HubSpaceDevice] = []
self.states: dict[str, list[hubspace_async.HubSpaceState]] = {}
self.friendly_names = friendly_names
Expand All @@ -50,7 +52,7 @@ def __init__(
super().__init__(
hass,
_LOGGER,
name="hubspace",
name=const.DOMAIN,
update_interval=update_interval,
)

Expand Down Expand Up @@ -98,7 +100,7 @@ async def _async_update_data(
async def hs_data_update(self) -> None:
"""Update the data via library."""
try:
async with timeout(10):
async with timeout(self.timeout):
await self.conn.populate_data()
except Exception as error:
raise UpdateFailed(error) from error
Expand Down
2 changes: 1 addition & 1 deletion custom_components/hubspace/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@
"issue_tracker": "https://github.com/jdeath/Hubspace-Homeassistant/issues",
"loggers": ["hubspace_async"],
"requirements": ["hubspace-async==0.4.1", "aiofiles==24.1.0"],
"version": "1.0"
"version": "2.0"
}
14 changes: 6 additions & 8 deletions custom_components/hubspace/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,16 @@
"step": {
"user": {
"data": {
"username": "Username",
"password": "Password"
},
"data_description": {
"username": "Username",
"password": "Password"
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
"timeout": "[%key:common::config_flow::data::password%]"
}
}
},
"error": {
"base": "Connection timed out",
"unknown": "Unable to authentication. Please verify Username and Password"
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
},
"services": {
Expand Down
10 changes: 6 additions & 4 deletions custom_components/hubspace/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,19 @@
"user": {
"data": {
"username": "Username",
"password": "Password"
"password": "Password",
"timeout": "timeout"
},
"data_description": {
"username": "Username",
"password": "Password"
"password": "Timeout in milliseconds for the connection. (Default: 10000)"
}
}
},
"error": {
"base": "Connection timed out",
"unknown": "Unable to authentication. Please verify Username and Password"
"cannot_connect": "Connection timed out when reaching the server.",
"invalid_auth": "Unable to authenticate with the provided credentials",
"unknown": "Unknown error occurred. Please review to the logs"
}
},
"services": {
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
homeassistant
hubspace-async>=0.1.0
hubspace-async>=0.4.1
aiofiles>=24.1.0
2 changes: 1 addition & 1 deletion test-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
pytest
pytest-mock
pytest_asyncio
pytest-asyncio
responses
pre-commit

0 comments on commit 321bd0c

Please sign in to comment.