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/sensor.py
homeassistant/components/guardian/switch.py homeassistant/components/guardian/switch.py
homeassistant/components/guardian/util.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/*
homeassistant/components/hangouts/__init__.py homeassistant/components/hangouts/__init__.py
homeassistant/components/hangouts/const.py homeassistant/components/hangouts/const.py

View File

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

View File

@ -1,55 +1,47 @@
"""Support for Habitica devices.""" """The habitica integration."""
from collections import namedtuple import asyncio
import logging import logging
from habitipy.aio import HabitipyAsync from habitipy.aio import HabitipyAsync
import voluptuous as vol import voluptuous as vol
from homeassistant.const import ( from homeassistant import config_entries
CONF_API_KEY, from homeassistant.config_entries import ConfigEntry
CONF_NAME, from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_SENSORS, CONF_URL
CONF_PATH, from homeassistant.core import HomeAssistant
CONF_SENSORS, from homeassistant.helpers import config_validation as cv
CONF_URL,
)
from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.helpers.aiohttp_client import async_get_clientsession 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__) _LOGGER = logging.getLogger(__name__)
CONF_API_USER = "api_user" INSTANCE_SCHEMA = vol.All(
cv.deprecated(CONF_SENSORS),
DEFAULT_URL = "https://habitica.com" vol.Schema(
DOMAIN = "habitica" {
vol.Optional(CONF_URL, default=DEFAULT_URL): cv.url,
ST = SensorType = namedtuple("SensorType", ["name", "icon", "unit", "path"]) vol.Optional(CONF_NAME): cv.string,
vol.Required(CONF_API_USER): cv.string,
SENSORS_TYPES = { vol.Required(CONF_API_KEY): cv.string,
"name": ST("Name", None, "", ["profile", "name"]), vol.Optional(CONF_SENSORS, default=list(SENSORS_TYPES)): vol.All(
"hp": ST("HP", "mdi:heart", "HP", ["stats", "hp"]), cv.ensure_list, vol.Unique(), [vol.In(list(SENSORS_TYPES))]
"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))]
),
}
) )
has_unique_values = vol.Schema(vol.Unique()) has_unique_values = vol.Schema(vol.Unique()) # pylint: disable=invalid-name
# because we want a handy alias # because we want a handy alias
@ -73,14 +65,9 @@ def has_all_unique_users_names(value):
INSTANCE_LIST_SCHEMA = vol.All( INSTANCE_LIST_SCHEMA = vol.All(
cv.ensure_list, has_all_unique_users, has_all_unique_users_names, [INSTANCE_SCHEMA] 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) CONFIG_SCHEMA = vol.Schema({DOMAIN: INSTANCE_LIST_SCHEMA}, extra=vol.ALLOW_EXTRA)
SERVICE_API_CALL = "api_call" PLATFORMS = ["sensor"]
ATTR_NAME = CONF_NAME
ATTR_PATH = CONF_PATH
ATTR_ARGS = "args"
EVENT_API_CALL_SUCCESS = f"{DOMAIN}_{SERVICE_API_CALL}_success"
SERVICE_API_CALL_SCHEMA = vol.Schema( 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.""" """Set up the Habitica service."""
configs = config.get(DOMAIN, [])
conf = config[DOMAIN] for conf in configs:
data = hass.data[DOMAIN] = {} if conf.get(CONF_URL) is None:
websession = async_get_clientsession(hass) 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): class HAHabitipyAsync(HabitipyAsync):
"""Closure API class to hold session.""" """Closure API class to hold session."""
@ -104,28 +104,6 @@ async def async_setup(hass, config):
def __call__(self, **kwargs): def __call__(self, **kwargs):
return super().__call__(websession, **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): async def handle_api_call(call):
name = call.data[ATTR_NAME] name = call.data[ATTR_NAME]
path = call.data[ATTR_PATH] 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} EVENT_API_CALL_SUCCESS, {"name": name, "path": path, "data": data}
) )
hass.services.async_register( data = hass.data.setdefault(DOMAIN, {})
DOMAIN, SERVICE_API_CALL, handle_api_call, schema=SERVICE_API_CALL_SCHEMA 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 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", "domain": "habitica",
"name": "Habitica", "name": "Habitica",
"documentation": "https://www.home-assistant.io/integrations/habitica", "config_flow": true,
"requirements": ["habitipy==0.2.0"], "documentation": "https://www.home-assistant.io/integrations/habitica",
"codeowners": [] "requirements": ["habitipy==0.2.0"],
"codeowners": ["@ASMfreaK", "@leikoilja"]
} }

View File

@ -1,25 +1,82 @@
"""Support for Habitica sensors.""" """Support for Habitica sensors."""
from collections import namedtuple
from datetime import timedelta 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.helpers.entity import Entity
from homeassistant.util import Throttle from homeassistant.util import Throttle
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15) 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): SENSORS_TYPES = {
"""Set up the habitica platform.""" "name": ST("Name", None, "", ["profile", "name"]),
if discovery_info is None: "hp": ST("HP", "mdi:heart", "HP", ["stats", "hp"]),
return "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] TASKS_TYPES = {
sensors = discovery_info[habitica.CONF_SENSORS] "habits": ST("Habits", "mdi:clipboard-list-outline", "n_of_tasks", ["habits"]),
sensor_data = HabitipyData(hass.data[habitica.DOMAIN][name]) "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() await sensor_data.update()
async_add_devices( for sensor_type in SENSORS_TYPES:
[HabitipySensor(name, sensor, sensor_data) for sensor in sensors], True 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: class HabitipyData:
@ -29,11 +86,43 @@ class HabitipyData:
"""Habitica API user data cache.""" """Habitica API user data cache."""
self.api = api self.api = api
self.data = None self.data = None
self.tasks = {}
@Throttle(MIN_TIME_BETWEEN_UPDATES) @Throttle(MIN_TIME_BETWEEN_UPDATES)
async def update(self): async def update(self):
"""Get a new fix from Habitica servers.""" """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): class HabitipySensor(Entity):
@ -43,7 +132,7 @@ class HabitipySensor(Entity):
"""Initialize a generic Habitica sensor.""" """Initialize a generic Habitica sensor."""
self._name = name self._name = name
self._sensor_name = sensor_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._state = None
self._updater = updater self._updater = updater
@ -63,7 +152,7 @@ class HabitipySensor(Entity):
@property @property
def name(self): def name(self):
"""Return the name of the sensor.""" """Return the name of the sensor."""
return f"{habitica.DOMAIN}_{self._name}_{self._sensor_name}" return f"{DOMAIN}_{self._name}_{self._sensor_name}"
@property @property
def state(self): def state(self):
@ -74,3 +163,63 @@ class HabitipySensor(Entity):
def unit_of_measurement(self): def unit_of_measurement(self):
"""Return the unit the value is expressed in.""" """Return the unit the value is expressed in."""
return self._sensor_type.unit 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", "gree",
"griddy", "griddy",
"guardian", "guardian",
"habitica",
"hangouts", "hangouts",
"harmony", "harmony",
"heos", "heos",

View File

@ -383,6 +383,9 @@ ha-ffmpeg==3.0.2
# homeassistant.components.philips_js # homeassistant.components.philips_js
ha-philipsjs==0.1.0 ha-philipsjs==0.1.0
# homeassistant.components.habitica
habitipy==0.2.0
# homeassistant.components.hangouts # homeassistant.components.hangouts
hangups==0.4.11 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"