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

Add option to login with username/email and password in Habitica integration #117622

Merged
merged 16 commits into from
Aug 30, 2024
16 changes: 8 additions & 8 deletions homeassistant/components/habitica/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
CONF_NAME,
CONF_SENSORS,
CONF_URL,
CONF_VERIFY_SSL,
Platform,
)
from homeassistant.core import HomeAssistant, ServiceCall
Expand Down Expand Up @@ -125,6 +126,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:
Expand All @@ -147,18 +149,16 @@ 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)

url = config_entry.data[CONF_URL]
username = config_entry.data[CONF_API_USER]
password = config_entry.data[CONF_API_KEY]
websession = async_get_clientsession(
hass, verify_ssl=config_entry.data.get(CONF_VERIFY_SSL, True)
)

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:
Expand Down
177 changes: 135 additions & 42 deletions homeassistant/components/habitica/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

from http import HTTPStatus
import logging
from typing import Any

Expand All @@ -10,46 +11,51 @@
import voluptuous as vol

from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_URL
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.const import (
CONF_API_KEY,
CONF_PASSWORD,
CONF_URL,
CONF_USERNAME,
CONF_VERIFY_SSL,
)
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

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,
}
)

_LOGGER = logging.getLogger(__name__)

STEP_LOGIN_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_USERNAME): TextSelector(
TextSelectorConfig(
type=TextSelectorType.EMAIL,
autocomplete="email",
)
),
vol.Required(CONF_PASSWORD): TextSelector(
TextSelectorConfig(
type=TextSelectorType.PASSWORD,
autocomplete="current-password",
)
),
}
)

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:
raise InvalidAuth from ex
_LOGGER = logging.getLogger(__name__)


class HabiticaConfigFlow(ConfigFlow, domain=DOMAIN):
Expand All @@ -62,24 +68,115 @@ async def async_step_user(
) -> ConfigFlowResult:
"""Handle the initial step."""

errors = {}
return self.async_show_menu(
step_id="user",
menu_options=["login", "advanced"],
)

async def async_step_login(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""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:
try:
info = await validate_input(self.hass, user_input)
except InvalidAuth:
errors = {"base": "invalid_credentials"}
session = async_get_clientsession(self.hass)
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],
)

except ClientResponseError as ex:
if ex.status == HTTPStatus.UNAUTHORIZED:
errors["base"] = "invalid_auth"
else:
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unexpected exception")
errors = {"base": "unknown"}
errors["base"] = "unknown"
else:
await self.async_set_unique_id(info[CONF_API_USER])
await self.async_set_unique_id(login_response["id"])
self._abort_if_unique_id_configured()
return self.async_create_entry(title=info["title"], data=user_input)
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,
CONF_VERIFY_SSL: True,
},
)

return self.async_show_form(
step_id="user",
data_schema=DATA_SCHEMA,
step_id="login",
data_schema=self.add_suggested_values_to_schema(
data_schema=STEP_LOGIN_DATA_SCHEMA, suggested_values=user_input
),
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 = 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",
)
except ClientResponseError as ex:
if ex.status == HTTPStatus.UNAUTHORIZED:
errors["base"] = "invalid_auth"
else:
errors["base"] = "cannot_connect"
except Exception:
_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,
description_placeholders={},
)

async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
Expand All @@ -98,8 +195,4 @@ async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResu
"integration_title": "Habitica",
},
)
return await self.async_step_user(import_data)


class InvalidAuth(HomeAssistantError):
"""Error to indicate there is invalid auth."""
return await self.async_step_advanced(import_data)
24 changes: 19 additions & 5 deletions homeassistant/components/habitica/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,32 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
},
"error": {
"invalid_credentials": "[%key:common::config_flow::error::invalid_auth%]",
"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%]"
},
"step": {
"user": {
"menu_options": {
"login": "Login to Habitica",
"advanced": "Login to other instances"
},
"description": "Connect your Habitica profile to allow monitoring of your user's profile and tasks."
},
"login": {
"data": {
"username": "Email or username (case-sensitive)",
"password": "[%key:common::config_flow::data::password%]"
}
},
"advanced": {
"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%]"
"api_user": "User ID",
"api_key": "API Token",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have a common key for this right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed this because that is how it is called by Habitica.

Screenshot 2024-08-21 175559

"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
},
"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": "You can retrieve your `User ID` and `API Token` from **Settings -> Site Data** on Habitica or the instance you want to connect to"
}
}
},
Expand Down
Loading