Add sensors to fetch Habitica tasks (#38910)

* Adding sensors to fetch habitica tasks

* PR changes and rebase

* Fixing pylint

* Fixing failed test dependancy

* Generating requirements

* Apply suggestions from code review

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* PR changes

* Update homeassistant/components/habitica/config_flow.py

Thank you, @MartinHjelmare

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* PR Changes

* Fix failing test

* Update tests/components/habitica/test_config_flow.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Fixing linting and imports

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Ilja Leiko 2021-02-17 09:04:11 +01:00 committed by GitHub
parent 56f32196bd
commit 8c72cb6163
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 549 additions and 96 deletions

View File

@ -361,7 +361,9 @@ omit =
homeassistant/components/guardian/sensor.py
homeassistant/components/guardian/switch.py
homeassistant/components/guardian/util.py
homeassistant/components/habitica/*
homeassistant/components/habitica/__init__.py
homeassistant/components/habitica/const.py
homeassistant/components/habitica/sensor.py
homeassistant/components/hangouts/*
homeassistant/components/hangouts/__init__.py
homeassistant/components/hangouts/const.py

View File

@ -182,6 +182,7 @@ homeassistant/components/griddy/* @bdraco
homeassistant/components/group/* @home-assistant/core
homeassistant/components/growatt_server/* @indykoning
homeassistant/components/guardian/* @bachya
homeassistant/components/habitica/* @ASMfreaK @leikoilja
homeassistant/components/harmony/* @ehendrix23 @bramkragten @bdraco @mkeesey
homeassistant/components/hassio/* @home-assistant/supervisor
homeassistant/components/hdmi_cec/* @newAM

View File

@ -1,55 +1,47 @@
"""Support for Habitica devices."""
from collections import namedtuple
"""The habitica integration."""
import asyncio
import logging
from habitipy.aio import HabitipyAsync
import voluptuous as vol
from homeassistant.const import (
CONF_API_KEY,
CONF_NAME,
CONF_PATH,
CONF_SENSORS,
CONF_URL,
)
from homeassistant.helpers import config_validation as cv, discovery
from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_SENSORS, CONF_URL
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import (
ATTR_ARGS,
ATTR_NAME,
ATTR_PATH,
CONF_API_USER,
DEFAULT_URL,
DOMAIN,
EVENT_API_CALL_SUCCESS,
SERVICE_API_CALL,
)
from .sensor import SENSORS_TYPES
_LOGGER = logging.getLogger(__name__)
CONF_API_USER = "api_user"
DEFAULT_URL = "https://habitica.com"
DOMAIN = "habitica"
ST = SensorType = namedtuple("SensorType", ["name", "icon", "unit", "path"])
SENSORS_TYPES = {
"name": ST("Name", None, "", ["profile", "name"]),
"hp": ST("HP", "mdi:heart", "HP", ["stats", "hp"]),
"maxHealth": ST("max HP", "mdi:heart", "HP", ["stats", "maxHealth"]),
"mp": ST("Mana", "mdi:auto-fix", "MP", ["stats", "mp"]),
"maxMP": ST("max Mana", "mdi:auto-fix", "MP", ["stats", "maxMP"]),
"exp": ST("EXP", "mdi:star", "EXP", ["stats", "exp"]),
"toNextLevel": ST("Next Lvl", "mdi:star", "EXP", ["stats", "toNextLevel"]),
"lvl": ST("Lvl", "mdi:arrow-up-bold-circle-outline", "Lvl", ["stats", "lvl"]),
"gp": ST("Gold", "mdi:currency-usd-circle", "Gold", ["stats", "gp"]),
"class": ST("Class", "mdi:sword", "", ["stats", "class"]),
}
INSTANCE_SCHEMA = vol.Schema(
{
vol.Optional(CONF_URL, default=DEFAULT_URL): cv.url,
vol.Optional(CONF_NAME): cv.string,
vol.Required(CONF_API_USER): cv.string,
vol.Required(CONF_API_KEY): cv.string,
vol.Optional(CONF_SENSORS, default=list(SENSORS_TYPES)): vol.All(
cv.ensure_list, vol.Unique(), [vol.In(list(SENSORS_TYPES))]
),
}
INSTANCE_SCHEMA = vol.All(
cv.deprecated(CONF_SENSORS),
vol.Schema(
{
vol.Optional(CONF_URL, default=DEFAULT_URL): cv.url,
vol.Optional(CONF_NAME): cv.string,
vol.Required(CONF_API_USER): cv.string,
vol.Required(CONF_API_KEY): cv.string,
vol.Optional(CONF_SENSORS, default=list(SENSORS_TYPES)): vol.All(
cv.ensure_list, vol.Unique(), [vol.In(list(SENSORS_TYPES))]
),
}
),
)
has_unique_values = vol.Schema(vol.Unique())
has_unique_values = vol.Schema(vol.Unique()) # pylint: disable=invalid-name
# because we want a handy alias
@ -73,14 +65,9 @@ def has_all_unique_users_names(value):
INSTANCE_LIST_SCHEMA = vol.All(
cv.ensure_list, has_all_unique_users, has_all_unique_users_names, [INSTANCE_SCHEMA]
)
CONFIG_SCHEMA = vol.Schema({DOMAIN: INSTANCE_LIST_SCHEMA}, extra=vol.ALLOW_EXTRA)
SERVICE_API_CALL = "api_call"
ATTR_NAME = CONF_NAME
ATTR_PATH = CONF_PATH
ATTR_ARGS = "args"
EVENT_API_CALL_SUCCESS = f"{DOMAIN}_{SERVICE_API_CALL}_success"
PLATFORMS = ["sensor"]
SERVICE_API_CALL_SCHEMA = vol.Schema(
{
@ -91,12 +78,25 @@ SERVICE_API_CALL_SCHEMA = vol.Schema(
)
async def async_setup(hass, config):
async def async_setup(hass: HomeAssistant, config: dict) -> bool:
"""Set up the Habitica service."""
configs = config.get(DOMAIN, [])
conf = config[DOMAIN]
data = hass.data[DOMAIN] = {}
websession = async_get_clientsession(hass)
for conf in configs:
if conf.get(CONF_URL) is None:
conf[CONF_URL] = DEFAULT_URL
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=conf
)
)
return True
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up habitica from a config entry."""
class HAHabitipyAsync(HabitipyAsync):
"""Closure API class to hold session."""
@ -104,28 +104,6 @@ async def async_setup(hass, config):
def __call__(self, **kwargs):
return super().__call__(websession, **kwargs)
for instance in conf:
url = instance[CONF_URL]
username = instance[CONF_API_USER]
password = instance[CONF_API_KEY]
name = instance.get(CONF_NAME)
config_dict = {"url": url, "login": username, "password": password}
api = HAHabitipyAsync(config_dict)
user = await api.user.get()
if name is None:
name = user["profile"]["name"]
data[name] = api
if CONF_SENSORS in instance:
hass.async_create_task(
discovery.async_load_platform(
hass,
"sensor",
DOMAIN,
{"name": name, "sensors": instance[CONF_SENSORS]},
config,
)
)
async def handle_api_call(call):
name = call.data[ATTR_NAME]
path = call.data[ATTR_PATH]
@ -147,7 +125,50 @@ async def async_setup(hass, config):
EVENT_API_CALL_SUCCESS, {"name": name, "path": path, "data": data}
)
hass.services.async_register(
DOMAIN, SERVICE_API_CALL, handle_api_call, schema=SERVICE_API_CALL_SCHEMA
)
data = hass.data.setdefault(DOMAIN, {})
config = config_entry.data
websession = async_get_clientsession(hass)
url = config[CONF_URL]
username = config[CONF_API_USER]
password = config[CONF_API_KEY]
name = config.get(CONF_NAME)
config_dict = {"url": url, "login": username, "password": password}
api = HAHabitipyAsync(config_dict)
user = await api.user.get()
if name is None:
name = user["profile"]["name"]
hass.config_entries.async_update_entry(
config_entry,
data={**config_entry.data, CONF_NAME: name},
)
data[config_entry.entry_id] = api
for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, component)
)
if not hass.services.has_service(DOMAIN, SERVICE_API_CALL):
hass.services.async_register(
DOMAIN, SERVICE_API_CALL, handle_api_call, schema=SERVICE_API_CALL_SCHEMA
)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, component)
for component in PLATFORMS
]
)
)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
if len(hass.config_entries.async_entries) == 1:
hass.components.webhook.async_unregister(SERVICE_API_CALL)
return unload_ok

View File

@ -0,0 +1,85 @@
"""Config flow for habitica integration."""
import logging
from typing import Dict
from aiohttp import ClientResponseError
from habitipy.aio import HabitipyAsync
import voluptuous as vol
from homeassistant import config_entries, core, exceptions
from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_URL
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import CONF_API_USER, DEFAULT_URL, DOMAIN # pylint: disable=unused-import
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,
}
)
_LOGGER = logging.getLogger(__name__)
async def validate_input(
hass: core.HomeAssistant, data: Dict[str, str]
) -> Dict[str, str]:
"""Validate the user input allows us to connect."""
websession = async_get_clientsession(hass)
api = HabitipyAsync(
conf={
"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
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for habitica."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
errors = {}
if user_input is not None:
try:
info = await validate_input(self.hass, user_input)
except InvalidAuth:
errors = {"base": "invalid_credentials"}
except Exception: # pylint: disable=broad-except
_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_form(
step_id="user",
data_schema=DATA_SCHEMA,
errors=errors,
description_placeholders={},
)
async def async_step_import(self, import_data):
"""Import habitica config from configuration.yaml."""
return await self.async_step_user(import_data)
class InvalidAuth(exceptions.HomeAssistantError):
"""Error to indicate there is invalid auth."""

View File

@ -0,0 +1,14 @@
"""Constants for the habitica integration."""
from homeassistant.const import CONF_NAME, CONF_PATH
CONF_API_USER = "api_user"
DEFAULT_URL = "https://habitica.com"
DOMAIN = "habitica"
SERVICE_API_CALL = "api_call"
ATTR_NAME = CONF_NAME
ATTR_PATH = CONF_PATH
ATTR_ARGS = "args"
EVENT_API_CALL_SUCCESS = f"{DOMAIN}_{SERVICE_API_CALL}_success"

View File

@ -1,7 +1,8 @@
{
"domain": "habitica",
"name": "Habitica",
"documentation": "https://www.home-assistant.io/integrations/habitica",
"requirements": ["habitipy==0.2.0"],
"codeowners": []
"domain": "habitica",
"name": "Habitica",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/habitica",
"requirements": ["habitipy==0.2.0"],
"codeowners": ["@ASMfreaK", "@leikoilja"]
}

View File

@ -1,25 +1,82 @@
"""Support for Habitica sensors."""
from collections import namedtuple
from datetime import timedelta
import logging
from homeassistant.components import habitica
from aiohttp import ClientResponseError
from homeassistant.const import CONF_NAME, HTTP_TOO_MANY_REQUESTS
from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
ST = SensorType = namedtuple("SensorType", ["name", "icon", "unit", "path"])
async def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
"""Set up the habitica platform."""
if discovery_info is None:
return
SENSORS_TYPES = {
"name": ST("Name", None, "", ["profile", "name"]),
"hp": ST("HP", "mdi:heart", "HP", ["stats", "hp"]),
"maxHealth": ST("max HP", "mdi:heart", "HP", ["stats", "maxHealth"]),
"mp": ST("Mana", "mdi:auto-fix", "MP", ["stats", "mp"]),
"maxMP": ST("max Mana", "mdi:auto-fix", "MP", ["stats", "maxMP"]),
"exp": ST("EXP", "mdi:star", "EXP", ["stats", "exp"]),
"toNextLevel": ST("Next Lvl", "mdi:star", "EXP", ["stats", "toNextLevel"]),
"lvl": ST("Lvl", "mdi:arrow-up-bold-circle-outline", "Lvl", ["stats", "lvl"]),
"gp": ST("Gold", "mdi:currency-usd-circle", "Gold", ["stats", "gp"]),
"class": ST("Class", "mdi:sword", "", ["stats", "class"]),
}
name = discovery_info[habitica.CONF_NAME]
sensors = discovery_info[habitica.CONF_SENSORS]
sensor_data = HabitipyData(hass.data[habitica.DOMAIN][name])
TASKS_TYPES = {
"habits": ST("Habits", "mdi:clipboard-list-outline", "n_of_tasks", ["habits"]),
"dailys": ST("Dailys", "mdi:clipboard-list-outline", "n_of_tasks", ["dailys"]),
"todos": ST("TODOs", "mdi:clipboard-list-outline", "n_of_tasks", ["todos"]),
"rewards": ST("Rewards", "mdi:clipboard-list-outline", "n_of_tasks", ["rewards"]),
}
TASKS_MAP_ID = "id"
TASKS_MAP = {
"repeat": "repeat",
"challenge": "challenge",
"group": "group",
"frequency": "frequency",
"every_x": "everyX",
"streak": "streak",
"counter_up": "counterUp",
"counter_down": "counterDown",
"next_due": "nextDue",
"yester_daily": "yesterDaily",
"completed": "completed",
"collapse_checklist": "collapseChecklist",
"type": "type",
"notes": "notes",
"tags": "tags",
"value": "value",
"priority": "priority",
"start_date": "startDate",
"days_of_month": "daysOfMonth",
"weeks_of_month": "weeksOfMonth",
"created_at": "createdAt",
"text": "text",
"is_due": "isDue",
}
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the habitica sensors."""
entities = []
name = config_entry.data[CONF_NAME]
sensor_data = HabitipyData(hass.data[DOMAIN][config_entry.entry_id])
await sensor_data.update()
async_add_devices(
[HabitipySensor(name, sensor, sensor_data) for sensor in sensors], True
)
for sensor_type in SENSORS_TYPES:
entities.append(HabitipySensor(name, sensor_type, sensor_data))
for task_type in TASKS_TYPES:
entities.append(HabitipyTaskSensor(name, task_type, sensor_data))
async_add_entities(entities, True)
class HabitipyData:
@ -29,11 +86,43 @@ class HabitipyData:
"""Habitica API user data cache."""
self.api = api
self.data = None
self.tasks = {}
@Throttle(MIN_TIME_BETWEEN_UPDATES)
async def update(self):
"""Get a new fix from Habitica servers."""
self.data = await self.api.user.get()
try:
self.data = await self.api.user.get()
except ClientResponseError as error:
if error.status == HTTP_TOO_MANY_REQUESTS:
_LOGGER.warning(
"Sensor data update for %s has too many API requests."
" Skipping the update.",
DOMAIN,
)
else:
_LOGGER.error(
"Count not update sensor data for %s (%s)",
DOMAIN,
error,
)
for task_type in TASKS_TYPES:
try:
self.tasks[task_type] = await self.api.tasks.user.get(type=task_type)
except ClientResponseError as error:
if error.status == HTTP_TOO_MANY_REQUESTS:
_LOGGER.warning(
"Sensor data update for %s has too many API requests."
" Skipping the update.",
DOMAIN,
)
else:
_LOGGER.error(
"Count not update sensor data for %s (%s)",
DOMAIN,
error,
)
class HabitipySensor(Entity):
@ -43,7 +132,7 @@ class HabitipySensor(Entity):
"""Initialize a generic Habitica sensor."""
self._name = name
self._sensor_name = sensor_name
self._sensor_type = habitica.SENSORS_TYPES[sensor_name]
self._sensor_type = SENSORS_TYPES[sensor_name]
self._state = None
self._updater = updater
@ -63,7 +152,7 @@ class HabitipySensor(Entity):
@property
def name(self):
"""Return the name of the sensor."""
return f"{habitica.DOMAIN}_{self._name}_{self._sensor_name}"
return f"{DOMAIN}_{self._name}_{self._sensor_name}"
@property
def state(self):
@ -74,3 +163,63 @@ class HabitipySensor(Entity):
def unit_of_measurement(self):
"""Return the unit the value is expressed in."""
return self._sensor_type.unit
class HabitipyTaskSensor(Entity):
"""A Habitica task sensor."""
def __init__(self, name, task_name, updater):
"""Initialize a generic Habitica task."""
self._name = name
self._task_name = task_name
self._task_type = TASKS_TYPES[task_name]
self._state = None
self._updater = updater
async def async_update(self):
"""Update Condition and Forecast."""
await self._updater.update()
all_tasks = self._updater.tasks
for element in self._task_type.path:
tasks_length = len(all_tasks[element])
self._state = tasks_length
@property
def icon(self):
"""Return the icon to use in the frontend, if any."""
return self._task_type.icon
@property
def name(self):
"""Return the name of the task."""
return f"{DOMAIN}_{self._name}_{self._task_name}"
@property
def state(self):
"""Return the state of the device."""
return self._state
@property
def device_state_attributes(self):
"""Return the state attributes of all user tasks."""
if self._updater.tasks is not None:
all_received_tasks = self._updater.tasks
for element in self._task_type.path:
received_tasks = all_received_tasks[element]
attrs = {}
# Map tasks to TASKS_MAP
for received_task in received_tasks:
task_id = received_task[TASKS_MAP_ID]
task = {}
for map_key, map_value in TASKS_MAP.items():
value = received_task.get(map_value)
if value:
task[map_key] = value
attrs[task_id] = task
return attrs
@property
def unit_of_measurement(self):
"""Return the unit the value is expressed in."""
return self._task_type.unit

View File

@ -0,0 +1,20 @@
{
"config": {
"error": {
"invalid_credentials": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"user": {
"data": {
"url": "[%key:common::config_flow::data::url%]",
"name": "Override for Habiticas username. Will be used for service calls",
"api_user": "Habiticas 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"
}
}
},
"title": "Habitica"
}

View File

@ -0,0 +1,20 @@
{
"config": {
"error": {
"invalid_credentials": "Invalid credentials",
"unknown": "Unknown error"
},
"step": {
"user": {
"data": {
"url": "Habitica URL",
"name": "Override for Habiticas username. Will be used for service calls",
"api_user": "Habiticas API user ID",
"api_key": "Habitica's API user 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 grabed from https://habitica.com/user/settings/api"
}
}
},
"title": "Habitica"
}

View File

@ -86,6 +86,7 @@ FLOWS = [
"gree",
"griddy",
"guardian",
"habitica",
"hangouts",
"harmony",
"heos",

View File

@ -383,6 +383,9 @@ ha-ffmpeg==3.0.2
# homeassistant.components.philips_js
ha-philipsjs==0.1.0
# homeassistant.components.habitica
habitipy==0.2.0
# homeassistant.components.hangouts
hangups==0.4.11

View File

@ -0,0 +1 @@
"""Tests for the habitica integration."""

View File

@ -0,0 +1,135 @@
"""Test the habitica config flow."""
from asyncio import Future
from unittest.mock import AsyncMock, MagicMock, patch
from homeassistant import config_entries, setup
from homeassistant.components.habitica.config_flow import InvalidAuth
from homeassistant.components.habitica.const import DEFAULT_URL, DOMAIN
from homeassistant.const import HTTP_OK
from tests.common import MockConfigEntry
async def test_form(hass):
"""Test we get the form."""
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["errors"] == {}
request_mock = MagicMock()
type(request_mock).status_code = HTTP_OK
mock_obj = MagicMock()
mock_obj.user.get.return_value = Future()
mock_obj.user.get.return_value.set_result(None)
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"],
{"api_user": "test-api-user", "api_key": "test-api-key"},
)
await hass.async_block_till_done()
assert result2["type"] == "create_entry"
assert result2["title"] == "Default username"
assert result2["data"] == {
"url": DEFAULT_URL,
"api_user": "test-api-user",
"api_key": "test-api-key",
}
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
async def test_form_invalid_credentials(hass):
"""Test we handle invalid credentials error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"habitipy.aio.HabitipyAsync",
side_effect=InvalidAuth,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"url": DEFAULT_URL,
"api_user": "test-api-user",
"api_key": "test-api-key",
},
)
assert result2["type"] == "form"
assert result2["errors"] == {"base": "invalid_credentials"}
async def test_form_unexpected_exception(hass):
"""Test we handle unexpected exception error."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
with patch(
"homeassistant.components.habitica.config_flow.HabitipyAsync",
side_effect=Exception,
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"url": DEFAULT_URL,
"api_user": "test-api-user",
"api_key": "test-api-key",
},
)
assert result2["type"] == "form"
assert result2["errors"] == {"base": "unknown"}
async def test_manual_flow_config_exist(hass, aioclient_mock):
"""Test config flow discovers only already configured config."""
MockConfigEntry(
domain=DOMAIN,
unique_id="test-api-user",
data={"api_user": "test-api-user", "api_key": "test-api-key"},
).add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}
)
assert result["type"] == "form"
assert result["step_id"] == "user"
mock_obj = MagicMock()
mock_obj.user.get.side_effect = AsyncMock(
return_value={"api_user": "test-api-user"}
)
with patch(
"homeassistant.components.habitica.config_flow.HabitipyAsync",
return_value=mock_obj,
):
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"url": DEFAULT_URL,
"api_user": "test-api-user",
"api_key": "test-api-key",
},
)
assert result["type"] == "abort"
assert result["reason"] == "already_configured"