mirror of
https://github.com/home-assistant/core.git
synced 2025-07-27 15:17:35 +00:00
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:
parent
56f32196bd
commit
8c72cb6163
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
85
homeassistant/components/habitica/config_flow.py
Normal file
85
homeassistant/components/habitica/config_flow.py
Normal 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."""
|
14
homeassistant/components/habitica/const.py
Normal file
14
homeassistant/components/habitica/const.py
Normal 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"
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
20
homeassistant/components/habitica/strings.json
Normal file
20
homeassistant/components/habitica/strings.json
Normal 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 Habitica’s username. Will be used for service calls",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Habitica"
|
||||||
|
}
|
20
homeassistant/components/habitica/translations/en.json
Normal file
20
homeassistant/components/habitica/translations/en.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"error": {
|
||||||
|
"invalid_credentials": "Invalid credentials",
|
||||||
|
"unknown": "Unknown error"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"url": "Habitica URL",
|
||||||
|
"name": "Override for Habitica’s username. Will be used for service calls",
|
||||||
|
"api_user": "Habitica’s 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"
|
||||||
|
}
|
@ -86,6 +86,7 @@ FLOWS = [
|
|||||||
"gree",
|
"gree",
|
||||||
"griddy",
|
"griddy",
|
||||||
"guardian",
|
"guardian",
|
||||||
|
"habitica",
|
||||||
"hangouts",
|
"hangouts",
|
||||||
"harmony",
|
"harmony",
|
||||||
"heos",
|
"heos",
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
1
tests/components/habitica/__init__.py
Normal file
1
tests/components/habitica/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Tests for the habitica integration."""
|
135
tests/components/habitica/test_config_flow.py
Normal file
135
tests/components/habitica/test_config_flow.py
Normal 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"
|
Loading…
x
Reference in New Issue
Block a user