Add support for Habitica (#15744)
* Added support for Habitica Second refactoring Moved all config to component. Sensors are autodiscovered. Signed-off-by: delphi <cpp.create@gmail.com> * Apply requested changes Signed-off-by: delphi <cpp.create@gmail.com> * Made event fire async. Made `sensors` config implicit and opt-out-style. Signed-off-by: delphi <cpp.create@gmail.com> * Removed unneeded check and await. Signed-off-by: delphi <cpp.create@gmail.com> * Moved into separate component package and added service.yaml Signed-off-by: delphi <cpp.create@gmail.com> * Fix coveralls Signed-off-by: delphi <cpp.create@gmail.com>
This commit is contained in:
parent
5681fa8f07
commit
99d48795b9
5 changed files with 275 additions and 0 deletions
|
@ -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
|
||||
|
|
158
homeassistant/components/habitica/__init__.py
Normal file
158
homeassistant/components/habitica/__init__.py
Normal file
|
@ -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
homeassistant/components/habitica/services.yaml
Normal file
15
homeassistant/components/habitica/services.yaml
Normal file
|
@ -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
homeassistant/components/sensor/habitica.py
Normal file
96
homeassistant/components/sensor/habitica.py
Normal file
|
@ -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
|
|
@ -420,6 +420,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
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue