Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Habitica #15744

Merged
merged 6 commits into from Aug 29, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions .coveragerc
Expand Up @@ -116,6 +116,9 @@ omit =
homeassistant/components/google.py
homeassistant/components/*/google.py

homeassistant/components/habitica/*
homeassistant/components/*/habitica.py

homeassistant/components/hangouts/__init__.py
homeassistant/components/hangouts/const.py
homeassistant/components/hangouts/hangouts_bot.py
Expand Down
158 changes: 158 additions & 0 deletions homeassistant/components/habitica/__init__.py
@@ -0,0 +1,158 @@
"""
The Habitica API component.

For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/habitica/
"""

import logging
from collections import namedtuple

import voluptuous as vol
from homeassistant.const import \
CONF_NAME, CONF_URL, CONF_SENSORS, CONF_PATH, CONF_API_KEY
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers import \
config_validation as cv, discovery

REQUIREMENTS = ['habitipy==0.2.0']
_LOGGER = logging.getLogger(__name__)
DOMAIN = "habitica"

CONF_API_USER = "api_user"

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:coin', 'Gold', ["stats", "gp"]),
'class': ST('Class', 'mdi:sword', '', ["stats", "class"])
}

INSTANCE_SCHEMA = vol.Schema({
vol.Optional(CONF_URL, default='https://habitica.com'): 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()) # pylint: disable=invalid-name
# because we want a handy alias


def has_all_unique_users(value):
"""Validate that all `api_user`s are unique."""
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(
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 = "{0}_{1}_{2}".format(
DOMAIN, SERVICE_API_CALL, "success")

SERVICE_API_CALL_SCHEMA = vol.Schema({
vol.Required(ATTR_NAME): str,
vol.Required(ATTR_PATH): vol.All(cv.ensure_list, [str]),
vol.Optional(ATTR_ARGS): dict
})


async def async_setup(hass, config):
"""Set up the habitica service."""
conf = config[DOMAIN]
data = hass.data[DOMAIN] = {}
websession = async_get_clientsession(hass)
from habitipy.aio import HabitipyAsync

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)
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]
api = hass.data[DOMAIN].get(name)
if api is None:
_LOGGER.error(
"API_CALL: User '%s' not configured", name)
return
try:
for element in path:
api = api[element]
except KeyError:
_LOGGER.error(
"API_CALL: Path %s is invalid"
" for api on '{%s}' element", path, element)
return
kwargs = call.data.get(ATTR_ARGS, {})
data = await api(**kwargs)
hass.bus.async_fire(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)
return True
15 changes: 15 additions & 0 deletions homeassistant/components/habitica/services.yaml
@@ -0,0 +1,15 @@
# Describes the format for Habitica service

---
api_call:
description: Call Habitica api
fields:
name:
description: Habitica's username to call for
example: 'xxxNotAValidNickxxx'
path:
description: "Items from API URL in form of an array with method attached at the end. Consult https://habitica.com/apidoc/. Example uses https://habitica.com/apidoc/#api-Task-CreateUserTasks"
example: '["tasks", "user", "post"]'
args:
description: Any additional json or url parameter arguments. See apidoc mentioned for path. Example uses same api endpoint
example: '{"text": "Use API from Home Assistant", "type": "todo"}'
96 changes: 96 additions & 0 deletions homeassistant/components/sensor/habitica.py
@@ -0,0 +1,96 @@
"""
The Habitica sensor.

For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.habitica/
"""

import logging
from datetime import timedelta

from homeassistant.helpers.entity import Entity
from homeassistant.util import Throttle
from homeassistant.components import habitica

_LOGGER = logging.getLogger(__name__)
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)


async def async_setup_platform(
hass, config, async_add_devices, discovery_info=None):
"""Set up the habitica platform."""
if discovery_info is None:
return

name = discovery_info[habitica.CONF_NAME]
sensors = discovery_info[habitica.CONF_SENSORS]
sensor_data = HabitipyData(hass.data[habitica.DOMAIN][name])
await sensor_data.update()
async_add_devices([
HabitipySensor(name, sensor, sensor_data)
for sensor in sensors
], True)


class HabitipyData:
"""Habitica API user data cache."""

def __init__(self, api):
"""
Habitica API user data cache.

api - HAHabitipyAsync object
"""
self.api = api
self.data = None

@Throttle(MIN_TIME_BETWEEN_UPDATES)
async def update(self):
"""Get a new fix from Habitica servers."""
self.data = await self.api.user.get()


class HabitipySensor(Entity):
"""A generic Habitica sensor."""

def __init__(self, name, sensor_name, updater):
"""
Init a generic Habitica sensor.

name - Habitica platform name
sensor_name - one of the names from ALL_SENSOR_TYPES
"""
self._name = name
self._sensor_name = sensor_name
self._sensor_type = habitica.SENSORS_TYPES[sensor_name]
self._state = None
self._updater = updater

async def async_update(self):
"""Update Condition and Forecast."""
await self._updater.update()
data = self._updater.data
for element in self._sensor_type.path:
data = data[element]
self._state = data

@property
def icon(self):
"""Return the icon to use in the frontend, if any."""
return self._sensor_type.icon

@property
def name(self):
"""Return the name of the sensor."""
return "{0}_{1}_{2}".format(
habitica.DOMAIN, self._name, self._sensor_name)

@property
def state(self):
"""Return the state of the device."""
return self._state

@property
def unit_of_measurement(self):
"""Return the unit the value is expressed in."""
return self._sensor_type.unit
3 changes: 3 additions & 0 deletions requirements_all.txt
Expand Up @@ -419,6 +419,9 @@ ha-ffmpeg==1.9
# homeassistant.components.media_player.philips_js
ha-philipsjs==0.0.5

# homeassistant.components.habitica
habitipy==0.2.0

# homeassistant.components.hangouts
hangups==0.4.5

Expand Down