mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 01:08:12 +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/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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
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",
|
||||
"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"]
|
||||
}
|
||||
|
@ -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
|
||||
|
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",
|
||||
"griddy",
|
||||
"guardian",
|
||||
"habitica",
|
||||
"hangouts",
|
||||
"harmony",
|
||||
"heos",
|
||||
|
@ -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
|
||||
|
||||
|
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