From 6d2812a3b87e0e61b33aa2ccf493708753eebff5 Mon Sep 17 00:00:00 2001 From: tr4nt0r Date: Tue, 30 Apr 2024 16:44:01 +0000 Subject: [PATCH 01/16] add login/password authentication --- .../components/habitica/config_flow.py | 89 +++++++++++++++++-- .../components/habitica/strings.json | 12 +++ 2 files changed, 95 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/habitica/config_flow.py b/homeassistant/components/habitica/config_flow.py index a40261c09021f..5be8afcf8418b 100644 --- a/homeassistant/components/habitica/config_flow.py +++ b/homeassistant/components/habitica/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations +from http import HTTPStatus import logging from typing import Any @@ -10,7 +11,13 @@ import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_URL +from homeassistant.const import ( + CONF_API_KEY, + CONF_NAME, + CONF_PASSWORD, + CONF_URL, + CONF_USERNAME, +) from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -18,7 +25,7 @@ from .const import CONF_API_USER, DEFAULT_URL, DOMAIN -DATA_SCHEMA = vol.Schema( +STEP_API_CREDENTIALS_DATA_SCHEMA = vol.Schema( { vol.Required(CONF_API_USER): str, vol.Required(CONF_API_KEY): str, @@ -27,6 +34,13 @@ } ) +STEP_LOGIN_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } +) + _LOGGER = logging.getLogger(__name__) @@ -49,7 +63,9 @@ async def validate_input(hass: HomeAssistant, data: dict[str, str]) -> dict[str, CONF_API_USER: data[CONF_API_USER], } except ClientResponseError as ex: - raise InvalidAuth from ex + if ex.status == HTTPStatus.UNAUTHORIZED: + raise InvalidAuth from ex + raise CannotConnect from ex class HabiticaConfigFlow(ConfigFlow, domain=DOMAIN): @@ -66,6 +82,8 @@ async def async_step_user( if user_input is not None: try: info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" except InvalidAuth: errors = {"base": "invalid_credentials"} except Exception: @@ -75,11 +93,66 @@ async def async_step_user( await self.async_set_unique_id(info[CONF_API_USER]) self._abort_if_unique_id_configured() return self.async_create_entry(title=info["title"], data=user_input) - return self.async_show_form( + return self.async_show_menu( step_id="user", - data_schema=DATA_SCHEMA, + menu_options=["login", "api_credentials"], + ) + # return self.async_show_form( + # step_id="user", + # data_schema=DATA_SCHEMA, + # errors=errors, + # description_placeholders={}, + # ) + + async def async_step_login( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Fetch API credentials by authenticating with login and password.""" + errors: dict[str, str] = {} + + if user_input is not None: + try: + session = async_get_clientsession(self.hass) + api = HabitipyAsync( + conf={ + "login": "", + "password": "", + "url": DEFAULT_URL, + } + ) + login_response = await api.user.auth.local.login.post( + session=session, + username=user_input[CONF_USERNAME], + password=user_input[CONF_PASSWORD], + ) + + except ClientResponseError as ex: + if ex.status == HTTPStatus.UNAUTHORIZED: + errors = {"base": "invalid_credentials"} + else: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors = {"base": "unknown"} + else: + await self.async_set_unique_id(login_response["id"]) + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=login_response["username"], + data={ + CONF_API_USER: login_response["id"], + CONF_API_KEY: login_response["apiToken"], + CONF_USERNAME: login_response["username"], + CONF_URL: DEFAULT_URL, + }, + ) + + return self.async_show_form( + step_id="login", + data_schema=self.add_suggested_values_to_schema( + data_schema=STEP_LOGIN_DATA_SCHEMA, suggested_values=user_input + ), errors=errors, - description_placeholders={}, ) async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult: @@ -103,3 +176,7 @@ async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResu class InvalidAuth(HomeAssistantError): """Error to indicate there is invalid auth.""" + + +class CannotConnect(HomeAssistantError): + """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/habitica/strings.json b/homeassistant/components/habitica/strings.json index 21d2622245c83..cd3b8b1120908 100644 --- a/homeassistant/components/habitica/strings.json +++ b/homeassistant/components/habitica/strings.json @@ -4,11 +4,16 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" }, "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_credentials": "[%key:common::config_flow::error::invalid_auth%]", "unknown": "[%key:common::config_flow::error::unknown%]" }, "step": { "user": { + "menu_options": { + "login": "Authenticate with your email/username and password", + "api_credentials": "Authenticate with API credentials" + }, "data": { "url": "[%key:common::config_flow::data::url%]", "name": "Override for Habitica’s username. Will be used for actions", @@ -16,6 +21,13 @@ "api_key": "[%key:common::config_flow::data::api_key%]" }, "description": "Connect your Habitica profile to allow monitoring of your user's profile and tasks. Note that api_id and api_key must be gotten from https://habitica.com/user/settings/api" + }, + "login": { + "data": { + "url": "[%key:common::config_flow::data::url%]", + "username": "Email or username (case-sensitive)", + "password": "[%key:common::config_flow::data::password%]" + } } } }, From d6eccbb34bf1d3701e9c1297786a0e80c22b087a Mon Sep 17 00:00:00 2001 From: tr4nt0r Date: Tue, 30 Apr 2024 20:52:44 +0000 Subject: [PATCH 02/16] add advanced config flow --- homeassistant/components/habitica/__init__.py | 5 +- .../components/habitica/config_flow.py | 88 +++++++++++++------ .../components/habitica/strings.json | 22 ++--- 3 files changed, 77 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index 468db8fbc4251..45d147b802c12 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -15,6 +15,7 @@ CONF_NAME, CONF_SENSORS, CONF_URL, + CONF_VERIFY_SSL, Platform, ) from homeassistant.core import HomeAssistant, ServiceCall @@ -147,7 +148,9 @@ async def handle_api_call(call: ServiceCall) -> None: EVENT_API_CALL_SUCCESS, {ATTR_NAME: name, ATTR_PATH: path, ATTR_DATA: data} ) - websession = async_get_clientsession(hass) + websession = async_get_clientsession( + hass, verify_ssl=entry.data.get(CONF_VERIFY_SSL, True) + ) url = config_entry.data[CONF_URL] username = config_entry.data[CONF_API_USER] diff --git a/homeassistant/components/habitica/config_flow.py b/homeassistant/components/habitica/config_flow.py index 5be8afcf8418b..5072d0f38bcde 100644 --- a/homeassistant/components/habitica/config_flow.py +++ b/homeassistant/components/habitica/config_flow.py @@ -13,24 +13,24 @@ from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import ( CONF_API_KEY, - CONF_NAME, CONF_PASSWORD, CONF_URL, CONF_USERNAME, + CONF_VERIFY_SSL, ) -from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from .const import CONF_API_USER, DEFAULT_URL, DOMAIN -STEP_API_CREDENTIALS_DATA_SCHEMA = vol.Schema( +STEP_ADVANCED_DATA_SCHEMA = vol.Schema( { vol.Required(CONF_API_USER): str, vol.Required(CONF_API_KEY): str, - vol.Optional(CONF_NAME): str, vol.Optional(CONF_URL, default=DEFAULT_URL): str, + vol.Required(CONF_VERIFY_SSL, default=True): bool, } ) @@ -78,36 +78,19 @@ async def async_step_user( ) -> ConfigFlowResult: """Handle the initial step.""" - errors = {} - if user_input is not None: - try: - info = await validate_input(self.hass, user_input) - except CannotConnect: - errors["base"] = "cannot_connect" - except InvalidAuth: - errors = {"base": "invalid_credentials"} - except Exception: - _LOGGER.exception("Unexpected exception") - errors = {"base": "unknown"} - else: - await self.async_set_unique_id(info[CONF_API_USER]) - self._abort_if_unique_id_configured() - return self.async_create_entry(title=info["title"], data=user_input) return self.async_show_menu( step_id="user", - menu_options=["login", "api_credentials"], + menu_options=["login", "advanced"], ) - # return self.async_show_form( - # step_id="user", - # data_schema=DATA_SCHEMA, - # errors=errors, - # description_placeholders={}, - # ) async def async_step_login( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: - """Fetch API credentials by authenticating with login and password.""" + """Config flow with username/password. + + Simplified configuration setup that retrieves API credentials + from Habitica.com by authenticating with login and password. + """ errors: dict[str, str] = {} if user_input is not None: @@ -144,6 +127,7 @@ async def async_step_login( CONF_API_KEY: login_response["apiToken"], CONF_USERNAME: login_response["username"], CONF_URL: DEFAULT_URL, + CONF_VERIFY_SSL: True, }, ) @@ -155,6 +139,56 @@ async def async_step_login( errors=errors, ) + async def async_step_advanced( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Advanced configuration with User Id and API Token. + + Advanced configuration allows connecting to Habitica instances + hosted on different domains or to self-hosted instances. + """ + + errors: dict[str, str] = {} + if user_input is not None: + try: + session = async_get_clientsession( + self.hass, verify_ssl=user_input.get(CONF_VERIFY_SSL, True) + ) + api = HabitipyAsync( + conf={ + "login": user_input[CONF_API_USER], + "password": user_input[CONF_API_KEY], + "url": user_input.get(CONF_URL, DEFAULT_URL), + } + ) + api_response = await api.user.get( + session=session, + userFields="auth", + ) + except ClientResponseError as ex: + if ex.status == HTTPStatus.UNAUTHORIZED: + errors = {"base": "invalid_credentials"} + else: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors = {"base": "unknown"} + else: + await self.async_set_unique_id(user_input[CONF_API_USER]) + self._abort_if_unique_id_configured() + user_input[CONF_USERNAME] = api_response["auth"]["local"]["username"] + return self.async_create_entry( + title=user_input[CONF_USERNAME], data=user_input + ) + + return self.async_show_form( + step_id="advanced", + data_schema=self.add_suggested_values_to_schema( + data_schema=STEP_ADVANCED_DATA_SCHEMA, suggested_values=user_input + ), + errors=errors, + ) + async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult: """Import habitica config from configuration.yaml.""" diff --git a/homeassistant/components/habitica/strings.json b/homeassistant/components/habitica/strings.json index cd3b8b1120908..1a19b70bec39c 100644 --- a/homeassistant/components/habitica/strings.json +++ b/homeassistant/components/habitica/strings.json @@ -11,23 +11,25 @@ "step": { "user": { "menu_options": { - "login": "Authenticate with your email/username and password", - "api_credentials": "Authenticate with API credentials" + "login": "Login to Habitica", + "advanced": "Advanced Configuration" }, - "data": { - "url": "[%key:common::config_flow::data::url%]", - "name": "Override for Habitica’s username. Will be used for actions", - "api_user": "Habitica’s API user ID", - "api_key": "[%key:common::config_flow::data::api_key%]" - }, - "description": "Connect your Habitica profile to allow monitoring of your user's profile and tasks. Note that api_id and api_key must be gotten from https://habitica.com/user/settings/api" + "description": "Connect your Habitica profile to allow monitoring of your user's profile and tasks." }, "login": { "data": { - "url": "[%key:common::config_flow::data::url%]", "username": "Email or username (case-sensitive)", "password": "[%key:common::config_flow::data::password%]" } + }, + "advanced": { + "data": { + "url": "[%key:common::config_flow::data::url%]", + "api_user": "User ID", + "api_key": "API Token", + "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" + }, + "description": "You can retrieve your Habitica `User ID` and `API Token` from here https://habitica.com/user/settings/api" } } }, From 350f0d2f9614e3a3c5998aff98095012a3a8f317 Mon Sep 17 00:00:00 2001 From: tr4nt0r Date: Tue, 30 Apr 2024 21:06:17 +0000 Subject: [PATCH 03/16] remove unused exception classes, fix errors --- .../components/habitica/config_flow.py | 19 ++++--------------- .../components/habitica/strings.json | 2 +- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/habitica/config_flow.py b/homeassistant/components/habitica/config_flow.py index 5072d0f38bcde..dfce962e91c26 100644 --- a/homeassistant/components/habitica/config_flow.py +++ b/homeassistant/components/habitica/config_flow.py @@ -19,7 +19,6 @@ CONF_VERIFY_SSL, ) from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue @@ -92,7 +91,6 @@ async def async_step_login( from Habitica.com by authenticating with login and password. """ errors: dict[str, str] = {} - if user_input is not None: try: session = async_get_clientsession(self.hass) @@ -111,12 +109,12 @@ async def async_step_login( except ClientResponseError as ex: if ex.status == HTTPStatus.UNAUTHORIZED: - errors = {"base": "invalid_credentials"} + errors["base"] = "invalid_auth" else: errors["base"] = "cannot_connect" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") - errors = {"base": "unknown"} + errors["base"] = "unknown" else: await self.async_set_unique_id(login_response["id"]) self._abort_if_unique_id_configured() @@ -147,7 +145,6 @@ async def async_step_advanced( Advanced configuration allows connecting to Habitica instances hosted on different domains or to self-hosted instances. """ - errors: dict[str, str] = {} if user_input is not None: try: @@ -167,12 +164,12 @@ async def async_step_advanced( ) except ClientResponseError as ex: if ex.status == HTTPStatus.UNAUTHORIZED: - errors = {"base": "invalid_credentials"} + errors["base"] = "invalid_auth" else: errors["base"] = "cannot_connect" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") - errors = {"base": "unknown"} + errors["base"] = "unknown" else: await self.async_set_unique_id(user_input[CONF_API_USER]) self._abort_if_unique_id_configured() @@ -206,11 +203,3 @@ async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResu }, ) return await self.async_step_user(import_data) - - -class InvalidAuth(HomeAssistantError): - """Error to indicate there is invalid auth.""" - - -class CannotConnect(HomeAssistantError): - """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/habitica/strings.json b/homeassistant/components/habitica/strings.json index 1a19b70bec39c..0055bf59bee70 100644 --- a/homeassistant/components/habitica/strings.json +++ b/homeassistant/components/habitica/strings.json @@ -5,7 +5,7 @@ }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", - "invalid_credentials": "[%key:common::config_flow::error::invalid_auth%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "unknown": "[%key:common::config_flow::error::unknown%]" }, "step": { From fdb9236aaef9023139c92d1cba9ab7e92cf6ca8a Mon Sep 17 00:00:00 2001 From: tr4nt0r Date: Tue, 30 Apr 2024 21:17:50 +0000 Subject: [PATCH 04/16] update username in init --- homeassistant/components/habitica/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index 45d147b802c12..708f4133009a5 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -149,7 +149,7 @@ async def handle_api_call(call: ServiceCall) -> None: ) websession = async_get_clientsession( - hass, verify_ssl=entry.data.get(CONF_VERIFY_SSL, True) + hass, verify_ssl=config_entry.data.get(CONF_VERIFY_SSL, True) ) url = config_entry.data[CONF_URL] @@ -165,7 +165,7 @@ async def handle_api_call(call: ServiceCall) -> None: }, ) try: - user = await api.user.get(userFields="profile") + user = await api.user.get(userFields="auth") except ClientResponseError as e: if e.status == HTTPStatus.TOO_MANY_REQUESTS: raise ConfigEntryNotReady( From 3d7d97b590c3c93cf05fe85fce9338b310dc0bc4 Mon Sep 17 00:00:00 2001 From: tr4nt0r Date: Wed, 1 May 2024 16:53:51 +0000 Subject: [PATCH 05/16] update tests --- .../components/habitica/config_flow.py | 2 +- tests/components/habitica/test_config_flow.py | 152 ++++++++++++++---- tests/components/habitica/test_init.py | 1 + 3 files changed, 120 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/habitica/config_flow.py b/homeassistant/components/habitica/config_flow.py index dfce962e91c26..6c5ceaa6bbb9f 100644 --- a/homeassistant/components/habitica/config_flow.py +++ b/homeassistant/components/habitica/config_flow.py @@ -202,4 +202,4 @@ async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResu "integration_title": "Habitica", }, ) - return await self.async_step_user(import_data) + return await self.async_step_advanced(import_data) diff --git a/tests/components/habitica/test_config_flow.py b/tests/components/habitica/test_config_flow.py index 4dfc696daf299..73d9e2de2fc32 100644 --- a/tests/components/habitica/test_config_flow.py +++ b/tests/components/habitica/test_config_flow.py @@ -3,27 +3,61 @@ from unittest.mock import AsyncMock, MagicMock, patch from aiohttp import ClientResponseError +import pytest from homeassistant import config_entries -from homeassistant.components.habitica.const import DEFAULT_URL, DOMAIN +from homeassistant.components.habitica.const import CONF_API_USER, DEFAULT_URL, DOMAIN +from homeassistant.const import ( + CONF_API_KEY, + CONF_PASSWORD, + CONF_URL, + CONF_USERNAME, + CONF_VERIFY_SSL, +) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry +MOCK_DATA_LOGIN_STEP = { + CONF_USERNAME: "test-email@example.com", + CONF_PASSWORD: "test-password", +} +MOCK_DATA_ADVANCED_STEP = { + CONF_API_USER: "test-api-user", + CONF_API_KEY: "test-api-key", + CONF_URL: DEFAULT_URL, + CONF_VERIFY_SSL: True, +} -async def test_form(hass: HomeAssistant) -> None: - """Test we get the form.""" +async def test_user_menu(hass: HomeAssistant) -> None: + """Test if we get the menu selection.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) + + assert result["type"] is FlowResultType.MENU + assert result["menu_options"] == ["login", "advanced"] + assert result["step_id"] == "user" + + +async def test_form_login(hass: HomeAssistant) -> None: + """Test we get the login form.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "login"} + ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {} mock_obj = MagicMock() - mock_obj.user.get = AsyncMock() - + mock_obj.user.auth.local.login.post = AsyncMock() + mock_obj.user.auth.local.login.post.return_value = { + "id": "test-api-user", + "apiToken": "test-api-key", + "username": "test-username", + } with ( patch( "homeassistant.components.habitica.config_flow.HabitipyAsync", @@ -37,57 +71,111 @@ async def test_form(hass: HomeAssistant) -> None: return_value=True, ) as mock_setup_entry, ): - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], - {"api_user": "test-api-user", "api_key": "test-api-key"}, + user_input=MOCK_DATA_LOGIN_STEP, ) await hass.async_block_till_done() - assert result2["type"] is FlowResultType.CREATE_ENTRY - assert result2["title"] == "Default username" - assert result2["data"] == { - "url": DEFAULT_URL, - "api_user": "test-api-user", - "api_key": "test-api-key", + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == "test-username" + assert result["data"] == { + **MOCK_DATA_ADVANCED_STEP, + CONF_USERNAME: "test-username", } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_invalid_credentials(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("raise_error", "text_error"), + [ + (ClientResponseError(MagicMock(), (), status=400), "cannot_connect"), + (ClientResponseError(MagicMock(), (), status=401), "invalid_auth"), + (IndexError(), "unknown"), + ], +) +async def test_form_login_errors(hass: HomeAssistant, raise_error, text_error) -> None: """Test we handle invalid credentials error.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": "login"} ) mock_obj = MagicMock() - mock_obj.user.get = AsyncMock(side_effect=ClientResponseError(MagicMock(), ())) - + mock_obj.user.auth.local.login.post = AsyncMock(side_effect=raise_error) with patch( "homeassistant.components.habitica.config_flow.HabitipyAsync", return_value=mock_obj, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - { - "url": DEFAULT_URL, - "api_user": "test-api-user", - "api_key": "test-api-key", - }, + user_input=MOCK_DATA_LOGIN_STEP, ) assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"base": "invalid_credentials"} + assert result2["errors"] == {"base": text_error} + +async def test_form_advanced(hass: HomeAssistant) -> None: + """Test we get the form.""" -async def test_form_unexpected_exception(hass: HomeAssistant) -> None: - """Test we handle unexpected exception error.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": "advanced"} ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} mock_obj = MagicMock() - mock_obj.user.get = AsyncMock(side_effect=Exception) + mock_obj.user.get = AsyncMock() + mock_obj.user.get.return_value = {"auth": {"local": {"username": "test-username"}}} + + with ( + patch( + "homeassistant.components.habitica.config_flow.HabitipyAsync", + return_value=mock_obj, + ), + patch( + "homeassistant.components.habitica.async_setup", return_value=True + ) as mock_setup, + patch( + "homeassistant.components.habitica.async_setup_entry", + return_value=True, + ) as mock_setup_entry, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=MOCK_DATA_ADVANCED_STEP, + ) + await hass.async_block_till_done() + + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["title"] == "test-username" + assert result2["data"] == { + **MOCK_DATA_ADVANCED_STEP, + CONF_USERNAME: "test-username", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + ("raise_error", "text_error"), + [ + (ClientResponseError(MagicMock(), (), status=400), "cannot_connect"), + (ClientResponseError(MagicMock(), (), status=401), "invalid_auth"), + (IndexError(), "unknown"), + ], +) +async def test_form_advanced_errors( + hass: HomeAssistant, raise_error, text_error +) -> None: + """Test we handle invalid credentials error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "advanced"} + ) + + mock_obj = MagicMock() + mock_obj.user.get = AsyncMock(side_effect=raise_error) with patch( "homeassistant.components.habitica.config_flow.HabitipyAsync", @@ -95,15 +183,11 @@ async def test_form_unexpected_exception(hass: HomeAssistant) -> None: ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - { - "url": DEFAULT_URL, - "api_user": "test-api-user", - "api_key": "test-api-key", - }, + user_input=MOCK_DATA_ADVANCED_STEP, ) assert result2["type"] is FlowResultType.FORM - assert result2["errors"] == {"base": "unknown"} + assert result2["errors"] == {"base": text_error} async def test_manual_flow_config_exist(hass: HomeAssistant) -> None: @@ -119,7 +203,7 @@ async def test_manual_flow_config_exist(hass: HomeAssistant) -> None: ) assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "user" + assert result["step_id"] == "advanced" mock_obj = MagicMock() mock_obj.user.get = AsyncMock(return_value={"api_user": "test-api-user"}) diff --git a/tests/components/habitica/test_init.py b/tests/components/habitica/test_init.py index 4c2b1e2aae61c..56f17bc98893c 100644 --- a/tests/components/habitica/test_init.py +++ b/tests/components/habitica/test_init.py @@ -52,6 +52,7 @@ def common_requests(aioclient_mock: AiohttpClientMocker) -> AiohttpClientMocker: "https://habitica.com/api/v3/user", json={ "data": { + "auth": {"local": {"username": TEST_USER_NAME}}, "api_user": "test-api-user", "profile": {"name": TEST_USER_NAME}, "stats": { From f8b3bd9df4a85c3db522f1c6cf8c75667727e086 Mon Sep 17 00:00:00 2001 From: tr4nt0r Date: Wed, 1 May 2024 17:57:41 +0000 Subject: [PATCH 06/16] update strings --- homeassistant/components/habitica/strings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/habitica/strings.json b/homeassistant/components/habitica/strings.json index 0055bf59bee70..c5a54d254cc23 100644 --- a/homeassistant/components/habitica/strings.json +++ b/homeassistant/components/habitica/strings.json @@ -12,7 +12,7 @@ "user": { "menu_options": { "login": "Login to Habitica", - "advanced": "Advanced Configuration" + "advanced": "Login to other instances" }, "description": "Connect your Habitica profile to allow monitoring of your user's profile and tasks." }, @@ -29,7 +29,7 @@ "api_key": "API Token", "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" }, - "description": "You can retrieve your Habitica `User ID` and `API Token` from here https://habitica.com/user/settings/api" + "description": "You can retrieve your `User ID` and `API Token` from **Settings -> Site Data** on Habitica or the instance you want to connect to" } } }, From 7aac660fb89d408d61a6901225262e6d03ba1fa4 Mon Sep 17 00:00:00 2001 From: tr4nt0r Date: Wed, 1 May 2024 19:00:57 +0000 Subject: [PATCH 07/16] combine steps with menu --- tests/components/habitica/test_config_flow.py | 43 ++++++++++++++++--- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/tests/components/habitica/test_config_flow.py b/tests/components/habitica/test_config_flow.py index 73d9e2de2fc32..09cda3fbb0a00 100644 --- a/tests/components/habitica/test_config_flow.py +++ b/tests/components/habitica/test_config_flow.py @@ -31,25 +31,23 @@ } -async def test_user_menu(hass: HomeAssistant) -> None: - """Test if we get the menu selection.""" +async def test_form_login(hass: HomeAssistant) -> None: + """Test we get the login form.""" + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.MENU - assert result["menu_options"] == ["login", "advanced"] + assert "login" in result["menu_options"] assert result["step_id"] == "user" - -async def test_form_login(hass: HomeAssistant) -> None: - """Test we get the login form.""" - result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "login"} ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {} + assert result["step_id"] == "login" mock_obj = MagicMock() mock_obj.user.auth.local.login.post = AsyncMock() @@ -97,6 +95,14 @@ async def test_form_login(hass: HomeAssistant) -> None: ) async def test_form_login_errors(hass: HomeAssistant, raise_error, text_error) -> None: """Test we handle invalid credentials error.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "user" + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "login"} ) @@ -119,6 +125,21 @@ async def test_form_login_errors(hass: HomeAssistant, raise_error, text_error) - async def test_form_advanced(hass: HomeAssistant) -> None: """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] is FlowResultType.MENU + assert "advanced" in result["menu_options"] + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "advanced"} + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {} + assert result["step_id"] == "advanced" + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "advanced"} ) @@ -170,6 +191,14 @@ async def test_form_advanced_errors( hass: HomeAssistant, raise_error, text_error ) -> None: """Test we handle invalid credentials error.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] is FlowResultType.MENU + assert result["step_id"] == "user" + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "advanced"} ) From ae580ea6fc54b7e7a54cc28cd450420ffca90ec5 Mon Sep 17 00:00:00 2001 From: tr4nt0r Date: Wed, 1 May 2024 19:56:49 +0000 Subject: [PATCH 08/16] remove username from entry --- homeassistant/components/habitica/__init__.py | 5 ++++- homeassistant/components/habitica/config_flow.py | 7 ++----- homeassistant/components/habitica/const.py | 2 +- homeassistant/components/habitica/services.yaml | 6 +++--- homeassistant/components/habitica/strings.json | 6 +++--- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index 708f4133009a5..ac460b2bc0d87 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -22,10 +22,12 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.selector import ConfigEntrySelector from homeassistant.helpers.typing import ConfigType from .const import ( ATTR_ARGS, + ATTR_CONFIG_ENTRY, ATTR_DATA, ATTR_PATH, CONF_API_USER, @@ -87,7 +89,7 @@ def has_all_unique_users_names(value): SERVICE_API_CALL_SCHEMA = vol.Schema( { - vol.Required(ATTR_NAME): str, + vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector(), vol.Required(ATTR_PATH): vol.All(cv.ensure_list, [str]), vol.Optional(ATTR_ARGS): dict, } @@ -126,6 +128,7 @@ async def handle_api_call(call: ServiceCall) -> None: name = call.data[ATTR_NAME] path = call.data[ATTR_PATH] entries = hass.config_entries.async_entries(DOMAIN) + api = None for entry in entries: if entry.data[CONF_NAME] == name: diff --git a/homeassistant/components/habitica/config_flow.py b/homeassistant/components/habitica/config_flow.py index 6c5ceaa6bbb9f..92eb29750707d 100644 --- a/homeassistant/components/habitica/config_flow.py +++ b/homeassistant/components/habitica/config_flow.py @@ -123,7 +123,6 @@ async def async_step_login( data={ CONF_API_USER: login_response["id"], CONF_API_KEY: login_response["apiToken"], - CONF_USERNAME: login_response["username"], CONF_URL: DEFAULT_URL, CONF_VERIFY_SSL: True, }, @@ -173,10 +172,8 @@ async def async_step_advanced( else: await self.async_set_unique_id(user_input[CONF_API_USER]) self._abort_if_unique_id_configured() - user_input[CONF_USERNAME] = api_response["auth"]["local"]["username"] - return self.async_create_entry( - title=user_input[CONF_USERNAME], data=user_input - ) + title = api_response["auth"]["local"]["username"] + return self.async_create_entry(title=title, data=user_input) return self.async_show_form( step_id="advanced", diff --git a/homeassistant/components/habitica/const.py b/homeassistant/components/habitica/const.py index 4b10e9a705b53..2e3ec9421f29d 100644 --- a/homeassistant/components/habitica/const.py +++ b/homeassistant/components/habitica/const.py @@ -12,7 +12,7 @@ SERVICE_API_CALL = "api_call" ATTR_PATH = CONF_PATH ATTR_ARGS = "args" - +ATTR_CONFIG_ENTRY = "config_entry" # event constants EVENT_API_CALL_SUCCESS = f"{DOMAIN}_{SERVICE_API_CALL}_success" ATTR_DATA = "data" diff --git a/homeassistant/components/habitica/services.yaml b/homeassistant/components/habitica/services.yaml index a7ef39eb5299f..d1cbf02a03f58 100644 --- a/homeassistant/components/habitica/services.yaml +++ b/homeassistant/components/habitica/services.yaml @@ -1,11 +1,11 @@ # Describes the format for Habitica service api_call: fields: - name: + config_entry: required: true - example: "xxxNotAValidNickxxx" selector: - text: + config_entry: + integration: habitica path: required: true example: '["tasks", "user", "post"]' diff --git a/homeassistant/components/habitica/strings.json b/homeassistant/components/habitica/strings.json index c5a54d254cc23..84884d2c13447 100644 --- a/homeassistant/components/habitica/strings.json +++ b/homeassistant/components/habitica/strings.json @@ -161,9 +161,9 @@ "name": "API name", "description": "Calls Habitica API.", "fields": { - "name": { - "name": "[%key:common::config_flow::data::name%]", - "description": "Habitica's username to call for." + "config_entry": { + "name": "Habitica user", + "description": "The Habitica user to call for" }, "path": { "name": "[%key:common::config_flow::data::path%]", From 4f2469b0ac76c92c545c65812c5c31580cfa4a95 Mon Sep 17 00:00:00 2001 From: tr4nt0r Date: Wed, 1 May 2024 20:34:57 +0000 Subject: [PATCH 09/16] update tests --- homeassistant/components/habitica/__init__.py | 6 ++++-- homeassistant/components/habitica/sensor.py | 2 ++ tests/components/habitica/test_config_flow.py | 11 +++-------- tests/components/habitica/test_init.py | 9 +++++++-- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index ac460b2bc0d87..64ca8885143fb 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -2,6 +2,7 @@ from http import HTTPStatus import logging +from typing import TYPE_CHECKING from aiohttp import ClientResponseError from habitipy.aio import HabitipyAsync @@ -135,7 +136,7 @@ async def handle_api_call(call: ServiceCall) -> None: api = entry.runtime_data.api break if api is None: - _LOGGER.error("API_CALL: User '%s' not configured", name) + _LOGGER.error("API_CALL: User '%s' not configured", entry.title) return try: for element in path: @@ -148,7 +149,8 @@ async def handle_api_call(call: ServiceCall) -> None: kwargs = call.data.get(ATTR_ARGS, {}) data = await api(**kwargs) hass.bus.async_fire( - EVENT_API_CALL_SUCCESS, {ATTR_NAME: name, ATTR_PATH: path, ATTR_DATA: data} + EVENT_API_CALL_SUCCESS, + {ATTR_NAME: entry.title, ATTR_PATH: path, ATTR_DATA: data}, ) websession = async_get_clientsession( diff --git a/homeassistant/components/habitica/sensor.py b/homeassistant/components/habitica/sensor.py index 8762345b597d8..83b8b3b94c289 100644 --- a/homeassistant/components/habitica/sensor.py +++ b/homeassistant/components/habitica/sensor.py @@ -14,6 +14,8 @@ SensorEntity, SensorEntityDescription, ) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback diff --git a/tests/components/habitica/test_config_flow.py b/tests/components/habitica/test_config_flow.py index 09cda3fbb0a00..bef8d4a1d2f4a 100644 --- a/tests/components/habitica/test_config_flow.py +++ b/tests/components/habitica/test_config_flow.py @@ -77,10 +77,8 @@ async def test_form_login(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test-username" - assert result["data"] == { - **MOCK_DATA_ADVANCED_STEP, - CONF_USERNAME: "test-username", - } + assert result["data"] == MOCK_DATA_ADVANCED_STEP + assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -171,10 +169,7 @@ async def test_form_advanced(hass: HomeAssistant) -> None: assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "test-username" - assert result2["data"] == { - **MOCK_DATA_ADVANCED_STEP, - CONF_USERNAME: "test-username", - } + assert result2["data"] == MOCK_DATA_ADVANCED_STEP assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/habitica/test_init.py b/tests/components/habitica/test_init.py index 56f17bc98893c..5ecd4488aee66 100644 --- a/tests/components/habitica/test_init.py +++ b/tests/components/habitica/test_init.py @@ -6,6 +6,7 @@ from homeassistant.components.habitica.const import ( ATTR_ARGS, + ATTR_CONFIG_ENTRY, ATTR_DATA, ATTR_PATH, DEFAULT_URL, @@ -143,7 +144,7 @@ async def test_service_call( assert len(capture_api_call_success) == 0 TEST_SERVICE_DATA = { - ATTR_NAME: "test_user", + ATTR_CONFIG_ENTRY: habitica_entry.entry_id, ATTR_PATH: ["tasks", "user", "post"], ATTR_ARGS: TEST_API_CALL_ARGS, } @@ -155,7 +156,11 @@ async def test_service_call( captured_data = capture_api_call_success[0].data captured_data[ATTR_ARGS] = captured_data[ATTR_DATA] del captured_data[ATTR_DATA] - assert captured_data == TEST_SERVICE_DATA + assert captured_data == { + "name": habitica_entry.title, + ATTR_PATH: ["tasks", "user", "post"], + ATTR_ARGS: TEST_API_CALL_ARGS, + } assert await hass.config_entries.async_unload(habitica_entry.entry_id) From 1b60c7c50a19ef14ec0e237a26b9969fbb2e89f4 Mon Sep 17 00:00:00 2001 From: tr4nt0r Date: Thu, 2 May 2024 13:24:21 +0000 Subject: [PATCH 10/16] Revert "update tests" This reverts commit 6ac8ad6a26547b623e217db817ec4d0cf8c91f1d. --- homeassistant/components/habitica/__init__.py | 6 ++---- homeassistant/components/habitica/sensor.py | 2 +- tests/components/habitica/test_config_flow.py | 11 ++++++++--- tests/components/habitica/test_init.py | 9 ++------- 4 files changed, 13 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index 64ca8885143fb..ac460b2bc0d87 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -2,7 +2,6 @@ from http import HTTPStatus import logging -from typing import TYPE_CHECKING from aiohttp import ClientResponseError from habitipy.aio import HabitipyAsync @@ -136,7 +135,7 @@ async def handle_api_call(call: ServiceCall) -> None: api = entry.runtime_data.api break if api is None: - _LOGGER.error("API_CALL: User '%s' not configured", entry.title) + _LOGGER.error("API_CALL: User '%s' not configured", name) return try: for element in path: @@ -149,8 +148,7 @@ async def handle_api_call(call: ServiceCall) -> None: kwargs = call.data.get(ATTR_ARGS, {}) data = await api(**kwargs) hass.bus.async_fire( - EVENT_API_CALL_SUCCESS, - {ATTR_NAME: entry.title, ATTR_PATH: path, ATTR_DATA: data}, + EVENT_API_CALL_SUCCESS, {ATTR_NAME: name, ATTR_PATH: path, ATTR_DATA: data} ) websession = async_get_clientsession( diff --git a/homeassistant/components/habitica/sensor.py b/homeassistant/components/habitica/sensor.py index 83b8b3b94c289..9789d7fdc15d0 100644 --- a/homeassistant/components/habitica/sensor.py +++ b/homeassistant/components/habitica/sensor.py @@ -15,7 +15,7 @@ SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_URL +from homeassistant.const import CONF_NAME, CONF_URL from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback diff --git a/tests/components/habitica/test_config_flow.py b/tests/components/habitica/test_config_flow.py index bef8d4a1d2f4a..09cda3fbb0a00 100644 --- a/tests/components/habitica/test_config_flow.py +++ b/tests/components/habitica/test_config_flow.py @@ -77,8 +77,10 @@ async def test_form_login(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test-username" - assert result["data"] == MOCK_DATA_ADVANCED_STEP - + assert result["data"] == { + **MOCK_DATA_ADVANCED_STEP, + CONF_USERNAME: "test-username", + } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -169,7 +171,10 @@ async def test_form_advanced(hass: HomeAssistant) -> None: assert result2["type"] is FlowResultType.CREATE_ENTRY assert result2["title"] == "test-username" - assert result2["data"] == MOCK_DATA_ADVANCED_STEP + assert result2["data"] == { + **MOCK_DATA_ADVANCED_STEP, + CONF_USERNAME: "test-username", + } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/habitica/test_init.py b/tests/components/habitica/test_init.py index 5ecd4488aee66..56f17bc98893c 100644 --- a/tests/components/habitica/test_init.py +++ b/tests/components/habitica/test_init.py @@ -6,7 +6,6 @@ from homeassistant.components.habitica.const import ( ATTR_ARGS, - ATTR_CONFIG_ENTRY, ATTR_DATA, ATTR_PATH, DEFAULT_URL, @@ -144,7 +143,7 @@ async def test_service_call( assert len(capture_api_call_success) == 0 TEST_SERVICE_DATA = { - ATTR_CONFIG_ENTRY: habitica_entry.entry_id, + ATTR_NAME: "test_user", ATTR_PATH: ["tasks", "user", "post"], ATTR_ARGS: TEST_API_CALL_ARGS, } @@ -156,11 +155,7 @@ async def test_service_call( captured_data = capture_api_call_success[0].data captured_data[ATTR_ARGS] = captured_data[ATTR_DATA] del captured_data[ATTR_DATA] - assert captured_data == { - "name": habitica_entry.title, - ATTR_PATH: ["tasks", "user", "post"], - ATTR_ARGS: TEST_API_CALL_ARGS, - } + assert captured_data == TEST_SERVICE_DATA assert await hass.config_entries.async_unload(habitica_entry.entry_id) From 961683c5b999130fa3d891ba29e721688232f66d Mon Sep 17 00:00:00 2001 From: tr4nt0r Date: Thu, 2 May 2024 13:24:40 +0000 Subject: [PATCH 11/16] Revert "remove username from entry" This reverts commit d9323fb72df3f9d41be0a53bb0cbe16be718d005. --- homeassistant/components/habitica/__init__.py | 4 +--- homeassistant/components/habitica/config_flow.py | 7 +++++-- homeassistant/components/habitica/const.py | 2 +- homeassistant/components/habitica/services.yaml | 6 +++--- homeassistant/components/habitica/strings.json | 6 +++--- 5 files changed, 13 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index ac460b2bc0d87..850a4a63a1ed5 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -22,12 +22,10 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.selector import ConfigEntrySelector from homeassistant.helpers.typing import ConfigType from .const import ( ATTR_ARGS, - ATTR_CONFIG_ENTRY, ATTR_DATA, ATTR_PATH, CONF_API_USER, @@ -89,7 +87,7 @@ def has_all_unique_users_names(value): SERVICE_API_CALL_SCHEMA = vol.Schema( { - vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector(), + vol.Required(ATTR_NAME): str, vol.Required(ATTR_PATH): vol.All(cv.ensure_list, [str]), vol.Optional(ATTR_ARGS): dict, } diff --git a/homeassistant/components/habitica/config_flow.py b/homeassistant/components/habitica/config_flow.py index 92eb29750707d..6c5ceaa6bbb9f 100644 --- a/homeassistant/components/habitica/config_flow.py +++ b/homeassistant/components/habitica/config_flow.py @@ -123,6 +123,7 @@ async def async_step_login( data={ CONF_API_USER: login_response["id"], CONF_API_KEY: login_response["apiToken"], + CONF_USERNAME: login_response["username"], CONF_URL: DEFAULT_URL, CONF_VERIFY_SSL: True, }, @@ -172,8 +173,10 @@ async def async_step_advanced( else: await self.async_set_unique_id(user_input[CONF_API_USER]) self._abort_if_unique_id_configured() - title = api_response["auth"]["local"]["username"] - return self.async_create_entry(title=title, data=user_input) + user_input[CONF_USERNAME] = api_response["auth"]["local"]["username"] + return self.async_create_entry( + title=user_input[CONF_USERNAME], data=user_input + ) return self.async_show_form( step_id="advanced", diff --git a/homeassistant/components/habitica/const.py b/homeassistant/components/habitica/const.py index 2e3ec9421f29d..4b10e9a705b53 100644 --- a/homeassistant/components/habitica/const.py +++ b/homeassistant/components/habitica/const.py @@ -12,7 +12,7 @@ SERVICE_API_CALL = "api_call" ATTR_PATH = CONF_PATH ATTR_ARGS = "args" -ATTR_CONFIG_ENTRY = "config_entry" + # event constants EVENT_API_CALL_SUCCESS = f"{DOMAIN}_{SERVICE_API_CALL}_success" ATTR_DATA = "data" diff --git a/homeassistant/components/habitica/services.yaml b/homeassistant/components/habitica/services.yaml index d1cbf02a03f58..a7ef39eb5299f 100644 --- a/homeassistant/components/habitica/services.yaml +++ b/homeassistant/components/habitica/services.yaml @@ -1,11 +1,11 @@ # Describes the format for Habitica service api_call: fields: - config_entry: + name: required: true + example: "xxxNotAValidNickxxx" selector: - config_entry: - integration: habitica + text: path: required: true example: '["tasks", "user", "post"]' diff --git a/homeassistant/components/habitica/strings.json b/homeassistant/components/habitica/strings.json index 84884d2c13447..c5a54d254cc23 100644 --- a/homeassistant/components/habitica/strings.json +++ b/homeassistant/components/habitica/strings.json @@ -161,9 +161,9 @@ "name": "API name", "description": "Calls Habitica API.", "fields": { - "config_entry": { - "name": "Habitica user", - "description": "The Habitica user to call for" + "name": { + "name": "[%key:common::config_flow::data::name%]", + "description": "Habitica's username to call for." }, "path": { "name": "[%key:common::config_flow::data::path%]", From 64c8ff730accb2c331af15aa3251ad614b1e6306 Mon Sep 17 00:00:00 2001 From: tr4nt0r Date: Sun, 5 May 2024 22:59:34 +0000 Subject: [PATCH 12/16] small changes --- homeassistant/components/habitica/__init__.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index 850a4a63a1ed5..bcf8713f9b100 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -153,20 +153,16 @@ async def handle_api_call(call: ServiceCall) -> None: hass, verify_ssl=config_entry.data.get(CONF_VERIFY_SSL, True) ) - url = config_entry.data[CONF_URL] - username = config_entry.data[CONF_API_USER] - password = config_entry.data[CONF_API_KEY] - api = await hass.async_add_executor_job( HAHabitipyAsync, { - "url": url, - "login": username, - "password": password, + "url": config_entry.data[CONF_URL], + "login": config_entry.data[CONF_API_USER], + "password": config_entry.data[CONF_API_KEY], }, ) try: - user = await api.user.get(userFields="auth") + user = await api.user.get(userFields="profile") except ClientResponseError as e: if e.status == HTTPStatus.TOO_MANY_REQUESTS: raise ConfigEntryNotReady( From 2cdc2439501e31755ae3a1908f72c76ae5f97dda Mon Sep 17 00:00:00 2001 From: tr4nt0r Date: Fri, 17 May 2024 09:04:58 +0000 Subject: [PATCH 13/16] remove pylint broad-excep --- homeassistant/components/habitica/config_flow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/habitica/config_flow.py b/homeassistant/components/habitica/config_flow.py index 6c5ceaa6bbb9f..8e6bd2cb8315b 100644 --- a/homeassistant/components/habitica/config_flow.py +++ b/homeassistant/components/habitica/config_flow.py @@ -112,7 +112,7 @@ async def async_step_login( errors["base"] = "invalid_auth" else: errors["base"] = "cannot_connect" - except Exception: # pylint: disable=broad-except + except Exception: _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: @@ -167,7 +167,7 @@ async def async_step_advanced( errors["base"] = "invalid_auth" else: errors["base"] = "cannot_connect" - except Exception: # pylint: disable=broad-except + except Exception: _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: From 2b8f0740e6c09a185f15bf17dd94a7aab85b8b60 Mon Sep 17 00:00:00 2001 From: tr4nt0r Date: Tue, 28 May 2024 15:22:03 +0000 Subject: [PATCH 14/16] run habitipy init in executor --- .../components/habitica/config_flow.py | 42 +++++-------------- 1 file changed, 10 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/habitica/config_flow.py b/homeassistant/components/habitica/config_flow.py index 8e6bd2cb8315b..8f4f7a8965807 100644 --- a/homeassistant/components/habitica/config_flow.py +++ b/homeassistant/components/habitica/config_flow.py @@ -43,30 +43,6 @@ _LOGGER = logging.getLogger(__name__) -async def validate_input(hass: HomeAssistant, data: dict[str, str]) -> dict[str, str]: - """Validate the user input allows us to connect.""" - - websession = async_get_clientsession(hass) - api = await hass.async_add_executor_job( - HabitipyAsync, - { - "login": data[CONF_API_USER], - "password": data[CONF_API_KEY], - "url": data[CONF_URL] or DEFAULT_URL, - }, - ) - try: - await api.user.get(session=websession) - return { - "title": f"{data.get('name', 'Default username')}", - CONF_API_USER: data[CONF_API_USER], - } - except ClientResponseError as ex: - if ex.status == HTTPStatus.UNAUTHORIZED: - raise InvalidAuth from ex - raise CannotConnect from ex - - class HabiticaConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for habitica.""" @@ -94,18 +70,19 @@ async def async_step_login( if user_input is not None: try: session = async_get_clientsession(self.hass) - api = HabitipyAsync( - conf={ + api = await self.hass.async_add_executor_job( + HabitipyAsync, + { "login": "", "password": "", "url": DEFAULT_URL, - } + }, ) login_response = await api.user.auth.local.login.post( session=session, username=user_input[CONF_USERNAME], password=user_input[CONF_PASSWORD], - ) + ) # pyright: ignore[reportGeneralTypeIssues] except ClientResponseError as ex: if ex.status == HTTPStatus.UNAUTHORIZED: @@ -151,17 +128,18 @@ async def async_step_advanced( session = async_get_clientsession( self.hass, verify_ssl=user_input.get(CONF_VERIFY_SSL, True) ) - api = HabitipyAsync( - conf={ + api = await self.hass.async_add_executor_job( + HabitipyAsync, + { "login": user_input[CONF_API_USER], "password": user_input[CONF_API_KEY], "url": user_input.get(CONF_URL, DEFAULT_URL), - } + }, ) api_response = await api.user.get( session=session, userFields="auth", - ) + ) # pyright: ignore[reportGeneralTypeIssues] except ClientResponseError as ex: if ex.status == HTTPStatus.UNAUTHORIZED: errors["base"] = "invalid_auth" From 4e885e6b5545ba68298a0934176b578d7f92aba6 Mon Sep 17 00:00:00 2001 From: tr4nt0r Date: Thu, 6 Jun 2024 10:33:00 +0000 Subject: [PATCH 15/16] Add text selectors --- .../components/habitica/config_flow.py | 19 +++++++++++++++++-- homeassistant/components/habitica/sensor.py | 2 -- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/habitica/config_flow.py b/homeassistant/components/habitica/config_flow.py index 8f4f7a8965807..1dfbcaf3648b6 100644 --- a/homeassistant/components/habitica/config_flow.py +++ b/homeassistant/components/habitica/config_flow.py @@ -21,6 +21,11 @@ from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from homeassistant.helpers.selector import ( + TextSelector, + TextSelectorConfig, + TextSelectorType, +) from .const import CONF_API_USER, DEFAULT_URL, DOMAIN @@ -35,8 +40,18 @@ STEP_LOGIN_DATA_SCHEMA = vol.Schema( { - vol.Required(CONF_USERNAME): str, - vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_USERNAME): TextSelector( + TextSelectorConfig( + type=TextSelectorType.EMAIL, + autocomplete="email", + ) + ), + vol.Required(CONF_PASSWORD): TextSelector( + TextSelectorConfig( + type=TextSelectorType.PASSWORD, + autocomplete="current-password", + ) + ), } ) diff --git a/homeassistant/components/habitica/sensor.py b/homeassistant/components/habitica/sensor.py index 9789d7fdc15d0..8762345b597d8 100644 --- a/homeassistant/components/habitica/sensor.py +++ b/homeassistant/components/habitica/sensor.py @@ -14,8 +14,6 @@ SensorEntity, SensorEntityDescription, ) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_NAME, CONF_URL from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_platform import AddEntitiesCallback From b0860106fcaa0656ccb8d58e9218ac42a45ab2e2 Mon Sep 17 00:00:00 2001 From: tr4nt0r Date: Wed, 21 Aug 2024 16:05:13 +0000 Subject: [PATCH 16/16] changes --- homeassistant/components/habitica/config_flow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/habitica/config_flow.py b/homeassistant/components/habitica/config_flow.py index 1dfbcaf3648b6..2947032c41ef7 100644 --- a/homeassistant/components/habitica/config_flow.py +++ b/homeassistant/components/habitica/config_flow.py @@ -97,7 +97,7 @@ async def async_step_login( session=session, username=user_input[CONF_USERNAME], password=user_input[CONF_PASSWORD], - ) # pyright: ignore[reportGeneralTypeIssues] + ) except ClientResponseError as ex: if ex.status == HTTPStatus.UNAUTHORIZED: @@ -154,7 +154,7 @@ async def async_step_advanced( api_response = await api.user.get( session=session, userFields="auth", - ) # pyright: ignore[reportGeneralTypeIssues] + ) except ClientResponseError as ex: if ex.status == HTTPStatus.UNAUTHORIZED: errors["base"] = "invalid_auth"