2018-08-29 19:13:01 +00:00
|
|
|
"""
|
|
|
|
The Habitica API component.
|
|
|
|
|
2018-10-14 15:11:39 +00:00
|
|
|
For more details about this component, please refer to the documentation at
|
2018-08-29 19:13:01 +00:00
|
|
|
https://home-assistant.io/components/habitica/
|
|
|
|
"""
|
|
|
|
from collections import namedtuple
|
2018-10-14 15:11:39 +00:00
|
|
|
import logging
|
2018-08-29 19:13:01 +00:00
|
|
|
|
|
|
|
import voluptuous as vol
|
2018-10-14 15:11:39 +00:00
|
|
|
|
|
|
|
from homeassistant.const import (
|
|
|
|
CONF_API_KEY, CONF_NAME, CONF_PATH, CONF_SENSORS, CONF_URL)
|
|
|
|
from homeassistant.helpers import config_validation as cv, discovery
|
2018-08-29 19:13:01 +00:00
|
|
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
|
|
|
|
|
|
|
REQUIREMENTS = ['habitipy==0.2.0']
|
2018-10-14 15:11:39 +00:00
|
|
|
|
2018-08-29 19:13:01 +00:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2018-10-14 15:11:39 +00:00
|
|
|
CONF_API_USER = 'api_user'
|
|
|
|
|
|
|
|
DEFAULT_URL = 'https://habitica.com'
|
|
|
|
DOMAIN = 'habitica'
|
2018-08-29 19:13:01 +00:00
|
|
|
|
|
|
|
ST = SensorType = namedtuple('SensorType', [
|
2018-10-14 15:11:39 +00:00
|
|
|
'name', 'icon', 'unit', 'path'
|
2018-08-29 19:13:01 +00:00
|
|
|
])
|
|
|
|
|
|
|
|
SENSORS_TYPES = {
|
2018-10-14 15:11:39 +00:00
|
|
|
'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']),
|
2018-08-29 19:13:01 +00:00
|
|
|
'toNextLevel': ST(
|
2018-10-14 15:11:39 +00:00
|
|
|
'Next Lvl', 'mdi:star', 'EXP', ['stats', 'toNextLevel']),
|
2018-08-29 19:13:01 +00:00
|
|
|
'lvl': ST(
|
2018-10-14 15:11:39 +00:00
|
|
|
'Lvl', 'mdi:arrow-up-bold-circle-outline', 'Lvl', ['stats', 'lvl']),
|
|
|
|
'gp': ST('Gold', 'mdi:coin', 'Gold', ['stats', 'gp']),
|
|
|
|
'class': ST('Class', 'mdi:sword', '', ['stats', 'class'])
|
2018-08-29 19:13:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
INSTANCE_SCHEMA = vol.Schema({
|
2018-10-14 15:11:39 +00:00
|
|
|
vol.Optional(CONF_URL, default=DEFAULT_URL): cv.url,
|
2018-08-29 19:13:01 +00:00
|
|
|
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)):
|
2018-10-14 15:11:39 +00:00
|
|
|
vol.All(cv.ensure_list, vol.Unique(), [vol.In(list(SENSORS_TYPES))]),
|
2018-08-29 19:13:01 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
has_unique_values = vol.Schema(vol.Unique()) # pylint: disable=invalid-name
|
|
|
|
# because we want a handy alias
|
|
|
|
|
|
|
|
|
|
|
|
def has_all_unique_users(value):
|
2018-10-14 15:11:39 +00:00
|
|
|
"""Validate that all API users are unique."""
|
2018-08-29 19:13:01 +00:00
|
|
|
api_users = [user[CONF_API_USER] for user in value]
|
|
|
|
has_unique_values(api_users)
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
def has_all_unique_users_names(value):
|
|
|
|
"""Validate that all user's names are unique and set if any is set."""
|
|
|
|
names = [user.get(CONF_NAME) for user in value]
|
|
|
|
if None in names and any(name is not None for name in names):
|
|
|
|
raise vol.Invalid(
|
|
|
|
'user names of all users must be set if any is set')
|
|
|
|
if not all(name is None for name in names):
|
|
|
|
has_unique_values(names)
|
|
|
|
return value
|
|
|
|
|
|
|
|
|
|
|
|
INSTANCE_LIST_SCHEMA = vol.All(
|
2018-10-14 15:11:39 +00:00
|
|
|
cv.ensure_list, has_all_unique_users, has_all_unique_users_names,
|
2018-08-29 19:13:01 +00:00
|
|
|
[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
|
2018-10-14 15:11:39 +00:00
|
|
|
ATTR_ARGS = 'args'
|
|
|
|
EVENT_API_CALL_SUCCESS = '{0}_{1}_{2}'.format(
|
|
|
|
DOMAIN, SERVICE_API_CALL, 'success')
|
2018-08-29 19:13:01 +00:00
|
|
|
|
|
|
|
SERVICE_API_CALL_SCHEMA = vol.Schema({
|
|
|
|
vol.Required(ATTR_NAME): str,
|
|
|
|
vol.Required(ATTR_PATH): vol.All(cv.ensure_list, [str]),
|
2018-10-14 15:11:39 +00:00
|
|
|
vol.Optional(ATTR_ARGS): dict,
|
2018-08-29 19:13:01 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
async def async_setup(hass, config):
|
2018-10-14 15:11:39 +00:00
|
|
|
"""Set up the Habitica service."""
|
|
|
|
from habitipy.aio import HabitipyAsync
|
|
|
|
|
2018-08-29 19:13:01 +00:00
|
|
|
conf = config[DOMAIN]
|
|
|
|
data = hass.data[DOMAIN] = {}
|
|
|
|
websession = async_get_clientsession(hass)
|
|
|
|
|
|
|
|
class HAHabitipyAsync(HabitipyAsync):
|
|
|
|
"""Closure API class to hold session."""
|
|
|
|
|
|
|
|
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)
|
2018-10-14 15:11:39 +00:00
|
|
|
config_dict = {'url': url, 'login': username, 'password': password}
|
2018-08-29 19:13:01 +00:00
|
|
|
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(
|
2018-10-14 15:11:39 +00:00
|
|
|
hass, 'sensor', DOMAIN,
|
|
|
|
{'name': name, 'sensors': instance[CONF_SENSORS]}, config))
|
2018-08-29 19:13:01 +00:00
|
|
|
|
|
|
|
async def handle_api_call(call):
|
|
|
|
name = call.data[ATTR_NAME]
|
|
|
|
path = call.data[ATTR_PATH]
|
|
|
|
api = hass.data[DOMAIN].get(name)
|
|
|
|
if api is None:
|
2018-10-14 15:11:39 +00:00
|
|
|
_LOGGER.error("API_CALL: User '%s' not configured", name)
|
2018-08-29 19:13:01 +00:00
|
|
|
return
|
|
|
|
try:
|
|
|
|
for element in path:
|
|
|
|
api = api[element]
|
|
|
|
except KeyError:
|
|
|
|
_LOGGER.error(
|
2018-10-14 15:11:39 +00:00
|
|
|
"API_CALL: Path %s is invalid for API on '{%s}' element",
|
|
|
|
path, element)
|
2018-08-29 19:13:01 +00:00
|
|
|
return
|
|
|
|
kwargs = call.data.get(ATTR_ARGS, {})
|
|
|
|
data = await api(**kwargs)
|
2018-10-14 15:11:39 +00:00
|
|
|
hass.bus.async_fire(
|
|
|
|
EVENT_API_CALL_SUCCESS, {'name': name, 'path': path, 'data': data})
|
2018-08-29 19:13:01 +00:00
|
|
|
|
|
|
|
hass.services.async_register(
|
2018-10-14 15:11:39 +00:00
|
|
|
DOMAIN, SERVICE_API_CALL, handle_api_call,
|
2018-08-29 19:13:01 +00:00
|
|
|
schema=SERVICE_API_CALL_SCHEMA)
|
|
|
|
return True
|