2024-07-08 07:10:28 +00:00
|
|
|
"""Habitica button platform."""
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
from collections.abc import Callable
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from enum import StrEnum
|
|
|
|
from http import HTTPStatus
|
2024-07-08 08:18:09 +00:00
|
|
|
from typing import Any
|
2024-07-08 07:10:28 +00:00
|
|
|
|
|
|
|
from aiohttp import ClientResponseError
|
|
|
|
|
2024-10-24 17:54:59 +00:00
|
|
|
from homeassistant.components.button import (
|
|
|
|
DOMAIN as BUTTON_DOMAIN,
|
|
|
|
ButtonEntity,
|
|
|
|
ButtonEntityDescription,
|
|
|
|
)
|
|
|
|
from homeassistant.core import HomeAssistant, callback
|
2024-10-29 11:48:20 +00:00
|
|
|
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
2024-10-24 17:54:59 +00:00
|
|
|
from homeassistant.helpers import entity_registry as er
|
2024-07-08 07:10:28 +00:00
|
|
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
|
|
|
2024-10-24 17:54:59 +00:00
|
|
|
from .const import ASSETS_URL, DOMAIN, HEALER, MAGE, ROGUE, WARRIOR
|
2024-07-08 07:10:28 +00:00
|
|
|
from .coordinator import HabiticaData, HabiticaDataUpdateCoordinator
|
2024-07-08 08:18:09 +00:00
|
|
|
from .entity import HabiticaBase
|
2024-10-25 09:00:58 +00:00
|
|
|
from .types import HabiticaConfigEntry
|
2024-07-08 07:10:28 +00:00
|
|
|
|
2024-11-24 23:55:11 +00:00
|
|
|
PARALLEL_UPDATES = 1
|
|
|
|
|
2024-07-08 07:10:28 +00:00
|
|
|
|
|
|
|
@dataclass(kw_only=True, frozen=True)
|
|
|
|
class HabiticaButtonEntityDescription(ButtonEntityDescription):
|
|
|
|
"""Describes Habitica button entity."""
|
|
|
|
|
|
|
|
press_fn: Callable[[HabiticaDataUpdateCoordinator], Any]
|
2024-11-28 07:08:00 +00:00
|
|
|
available_fn: Callable[[HabiticaData], bool]
|
2024-10-24 17:54:59 +00:00
|
|
|
class_needed: str | None = None
|
|
|
|
entity_picture: str | None = None
|
2024-07-08 07:10:28 +00:00
|
|
|
|
|
|
|
|
|
|
|
class HabitipyButtonEntity(StrEnum):
|
|
|
|
"""Habitica button entities."""
|
|
|
|
|
|
|
|
RUN_CRON = "run_cron"
|
|
|
|
BUY_HEALTH_POTION = "buy_health_potion"
|
|
|
|
ALLOCATE_ALL_STAT_POINTS = "allocate_all_stat_points"
|
|
|
|
REVIVE = "revive"
|
2024-10-24 17:54:59 +00:00
|
|
|
MPHEAL = "mpheal"
|
|
|
|
EARTH = "earth"
|
|
|
|
FROST = "frost"
|
|
|
|
DEFENSIVE_STANCE = "defensive_stance"
|
|
|
|
VALOROUS_PRESENCE = "valorous_presence"
|
|
|
|
INTIMIDATE = "intimidate"
|
|
|
|
TOOLS_OF_TRADE = "tools_of_trade"
|
|
|
|
STEALTH = "stealth"
|
|
|
|
HEAL = "heal"
|
|
|
|
PROTECT_AURA = "protect_aura"
|
|
|
|
BRIGHTNESS = "brightness"
|
|
|
|
HEAL_ALL = "heal_all"
|
2024-07-08 07:10:28 +00:00
|
|
|
|
|
|
|
|
|
|
|
BUTTON_DESCRIPTIONS: tuple[HabiticaButtonEntityDescription, ...] = (
|
|
|
|
HabiticaButtonEntityDescription(
|
|
|
|
key=HabitipyButtonEntity.RUN_CRON,
|
|
|
|
translation_key=HabitipyButtonEntity.RUN_CRON,
|
|
|
|
press_fn=lambda coordinator: coordinator.api.cron.post(),
|
|
|
|
available_fn=lambda data: data.user["needsCron"],
|
|
|
|
),
|
|
|
|
HabiticaButtonEntityDescription(
|
|
|
|
key=HabitipyButtonEntity.BUY_HEALTH_POTION,
|
|
|
|
translation_key=HabitipyButtonEntity.BUY_HEALTH_POTION,
|
|
|
|
press_fn=(
|
|
|
|
lambda coordinator: coordinator.api["user"]["buy-health-potion"].post()
|
|
|
|
),
|
|
|
|
available_fn=(
|
|
|
|
lambda data: data.user["stats"]["gp"] >= 25
|
|
|
|
and data.user["stats"]["hp"] < 50
|
|
|
|
),
|
2024-10-25 08:59:37 +00:00
|
|
|
entity_picture="shop_potion.png",
|
2024-07-08 07:10:28 +00:00
|
|
|
),
|
|
|
|
HabiticaButtonEntityDescription(
|
|
|
|
key=HabitipyButtonEntity.ALLOCATE_ALL_STAT_POINTS,
|
|
|
|
translation_key=HabitipyButtonEntity.ALLOCATE_ALL_STAT_POINTS,
|
|
|
|
press_fn=lambda coordinator: coordinator.api["user"]["allocate-now"].post(),
|
|
|
|
available_fn=(
|
|
|
|
lambda data: data.user["preferences"].get("automaticAllocation") is True
|
|
|
|
and data.user["stats"]["points"] > 0
|
|
|
|
),
|
|
|
|
),
|
|
|
|
HabiticaButtonEntityDescription(
|
|
|
|
key=HabitipyButtonEntity.REVIVE,
|
|
|
|
translation_key=HabitipyButtonEntity.REVIVE,
|
|
|
|
press_fn=lambda coordinator: coordinator.api["user"]["revive"].post(),
|
|
|
|
available_fn=lambda data: data.user["stats"]["hp"] == 0,
|
|
|
|
),
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2024-10-24 17:54:59 +00:00
|
|
|
CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = (
|
|
|
|
HabiticaButtonEntityDescription(
|
|
|
|
key=HabitipyButtonEntity.MPHEAL,
|
|
|
|
translation_key=HabitipyButtonEntity.MPHEAL,
|
|
|
|
press_fn=lambda coordinator: coordinator.api.user.class_.cast["mpheal"].post(),
|
|
|
|
available_fn=(
|
|
|
|
lambda data: data.user["stats"]["lvl"] >= 12
|
|
|
|
and data.user["stats"]["mp"] >= 30
|
|
|
|
),
|
|
|
|
class_needed=MAGE,
|
|
|
|
entity_picture="shop_mpheal.png",
|
|
|
|
),
|
|
|
|
HabiticaButtonEntityDescription(
|
|
|
|
key=HabitipyButtonEntity.EARTH,
|
|
|
|
translation_key=HabitipyButtonEntity.EARTH,
|
|
|
|
press_fn=lambda coordinator: coordinator.api.user.class_.cast["earth"].post(),
|
|
|
|
available_fn=(
|
|
|
|
lambda data: data.user["stats"]["lvl"] >= 13
|
|
|
|
and data.user["stats"]["mp"] >= 35
|
|
|
|
),
|
|
|
|
class_needed=MAGE,
|
|
|
|
entity_picture="shop_earth.png",
|
|
|
|
),
|
|
|
|
HabiticaButtonEntityDescription(
|
|
|
|
key=HabitipyButtonEntity.FROST,
|
|
|
|
translation_key=HabitipyButtonEntity.FROST,
|
2024-10-29 11:48:20 +00:00
|
|
|
press_fn=lambda coordinator: coordinator.api.user.class_.cast["frost"].post(),
|
2024-10-29 13:24:23 +00:00
|
|
|
# chilling frost can only be cast once per day (streaks buff is false)
|
2024-10-24 17:54:59 +00:00
|
|
|
available_fn=(
|
|
|
|
lambda data: data.user["stats"]["lvl"] >= 14
|
|
|
|
and data.user["stats"]["mp"] >= 40
|
2024-10-29 13:24:23 +00:00
|
|
|
and not data.user["stats"]["buffs"]["streaks"]
|
2024-10-24 17:54:59 +00:00
|
|
|
),
|
|
|
|
class_needed=MAGE,
|
|
|
|
entity_picture="shop_frost.png",
|
|
|
|
),
|
|
|
|
HabiticaButtonEntityDescription(
|
|
|
|
key=HabitipyButtonEntity.DEFENSIVE_STANCE,
|
|
|
|
translation_key=HabitipyButtonEntity.DEFENSIVE_STANCE,
|
|
|
|
press_fn=(
|
|
|
|
lambda coordinator: coordinator.api.user.class_.cast[
|
|
|
|
"defensiveStance"
|
2024-10-29 11:48:20 +00:00
|
|
|
].post()
|
2024-10-24 17:54:59 +00:00
|
|
|
),
|
|
|
|
available_fn=(
|
|
|
|
lambda data: data.user["stats"]["lvl"] >= 12
|
|
|
|
and data.user["stats"]["mp"] >= 25
|
|
|
|
),
|
|
|
|
class_needed=WARRIOR,
|
|
|
|
entity_picture="shop_defensiveStance.png",
|
|
|
|
),
|
|
|
|
HabiticaButtonEntityDescription(
|
|
|
|
key=HabitipyButtonEntity.VALOROUS_PRESENCE,
|
|
|
|
translation_key=HabitipyButtonEntity.VALOROUS_PRESENCE,
|
|
|
|
press_fn=(
|
|
|
|
lambda coordinator: coordinator.api.user.class_.cast[
|
|
|
|
"valorousPresence"
|
2024-10-29 11:48:20 +00:00
|
|
|
].post()
|
2024-10-24 17:54:59 +00:00
|
|
|
),
|
|
|
|
available_fn=(
|
|
|
|
lambda data: data.user["stats"]["lvl"] >= 13
|
|
|
|
and data.user["stats"]["mp"] >= 20
|
|
|
|
),
|
|
|
|
class_needed=WARRIOR,
|
|
|
|
entity_picture="shop_valorousPresence.png",
|
|
|
|
),
|
|
|
|
HabiticaButtonEntityDescription(
|
|
|
|
key=HabitipyButtonEntity.INTIMIDATE,
|
|
|
|
translation_key=HabitipyButtonEntity.INTIMIDATE,
|
|
|
|
press_fn=(
|
2024-10-29 11:48:20 +00:00
|
|
|
lambda coordinator: coordinator.api.user.class_.cast["intimidate"].post()
|
2024-10-24 17:54:59 +00:00
|
|
|
),
|
|
|
|
available_fn=(
|
|
|
|
lambda data: data.user["stats"]["lvl"] >= 14
|
|
|
|
and data.user["stats"]["mp"] >= 15
|
|
|
|
),
|
|
|
|
class_needed=WARRIOR,
|
|
|
|
entity_picture="shop_intimidate.png",
|
|
|
|
),
|
|
|
|
HabiticaButtonEntityDescription(
|
|
|
|
key=HabitipyButtonEntity.TOOLS_OF_TRADE,
|
|
|
|
translation_key=HabitipyButtonEntity.TOOLS_OF_TRADE,
|
|
|
|
press_fn=(
|
|
|
|
lambda coordinator: coordinator.api.user.class_.cast["toolsOfTrade"].post()
|
|
|
|
),
|
|
|
|
available_fn=(
|
|
|
|
lambda data: data.user["stats"]["lvl"] >= 13
|
|
|
|
and data.user["stats"]["mp"] >= 25
|
|
|
|
),
|
|
|
|
class_needed=ROGUE,
|
|
|
|
entity_picture="shop_toolsOfTrade.png",
|
|
|
|
),
|
|
|
|
HabiticaButtonEntityDescription(
|
|
|
|
key=HabitipyButtonEntity.STEALTH,
|
|
|
|
translation_key=HabitipyButtonEntity.STEALTH,
|
|
|
|
press_fn=(
|
2024-10-29 11:48:20 +00:00
|
|
|
lambda coordinator: coordinator.api.user.class_.cast["stealth"].post()
|
2024-10-24 17:54:59 +00:00
|
|
|
),
|
2024-10-29 13:24:23 +00:00
|
|
|
# Stealth buffs stack and it can only be cast if the amount of
|
|
|
|
# unfinished dailies is smaller than the amount of buffs
|
2024-10-24 17:54:59 +00:00
|
|
|
available_fn=(
|
|
|
|
lambda data: data.user["stats"]["lvl"] >= 14
|
|
|
|
and data.user["stats"]["mp"] >= 45
|
2024-10-29 13:24:23 +00:00
|
|
|
and data.user["stats"]["buffs"]["stealth"]
|
|
|
|
< len(
|
|
|
|
[
|
|
|
|
r
|
|
|
|
for r in data.tasks
|
|
|
|
if r.get("type") == "daily"
|
|
|
|
and r.get("isDue") is True
|
|
|
|
and r.get("completed") is False
|
|
|
|
]
|
|
|
|
)
|
2024-10-24 17:54:59 +00:00
|
|
|
),
|
|
|
|
class_needed=ROGUE,
|
|
|
|
entity_picture="shop_stealth.png",
|
|
|
|
),
|
|
|
|
HabiticaButtonEntityDescription(
|
|
|
|
key=HabitipyButtonEntity.HEAL,
|
|
|
|
translation_key=HabitipyButtonEntity.HEAL,
|
2024-10-29 11:48:20 +00:00
|
|
|
press_fn=lambda coordinator: coordinator.api.user.class_.cast["heal"].post(),
|
2024-10-24 17:54:59 +00:00
|
|
|
available_fn=(
|
|
|
|
lambda data: data.user["stats"]["lvl"] >= 11
|
|
|
|
and data.user["stats"]["mp"] >= 15
|
2024-10-29 13:24:23 +00:00
|
|
|
and data.user["stats"]["hp"] < 50
|
2024-10-24 17:54:59 +00:00
|
|
|
),
|
|
|
|
class_needed=HEALER,
|
2024-10-29 13:24:23 +00:00
|
|
|
entity_picture="shop_heal.png",
|
2024-10-24 17:54:59 +00:00
|
|
|
),
|
|
|
|
HabiticaButtonEntityDescription(
|
|
|
|
key=HabitipyButtonEntity.BRIGHTNESS,
|
|
|
|
translation_key=HabitipyButtonEntity.BRIGHTNESS,
|
|
|
|
press_fn=(
|
2024-10-29 11:48:20 +00:00
|
|
|
lambda coordinator: coordinator.api.user.class_.cast["brightness"].post()
|
2024-10-24 17:54:59 +00:00
|
|
|
),
|
|
|
|
available_fn=(
|
|
|
|
lambda data: data.user["stats"]["lvl"] >= 12
|
|
|
|
and data.user["stats"]["mp"] >= 15
|
|
|
|
),
|
|
|
|
class_needed=HEALER,
|
|
|
|
entity_picture="shop_brightness.png",
|
|
|
|
),
|
|
|
|
HabiticaButtonEntityDescription(
|
|
|
|
key=HabitipyButtonEntity.PROTECT_AURA,
|
|
|
|
translation_key=HabitipyButtonEntity.PROTECT_AURA,
|
|
|
|
press_fn=(
|
|
|
|
lambda coordinator: coordinator.api.user.class_.cast["protectAura"].post()
|
|
|
|
),
|
|
|
|
available_fn=(
|
|
|
|
lambda data: data.user["stats"]["lvl"] >= 13
|
|
|
|
and data.user["stats"]["mp"] >= 30
|
|
|
|
),
|
|
|
|
class_needed=HEALER,
|
|
|
|
entity_picture="shop_protectAura.png",
|
|
|
|
),
|
|
|
|
HabiticaButtonEntityDescription(
|
|
|
|
key=HabitipyButtonEntity.HEAL_ALL,
|
|
|
|
translation_key=HabitipyButtonEntity.HEAL_ALL,
|
|
|
|
press_fn=lambda coordinator: coordinator.api.user.class_.cast["healAll"].post(),
|
|
|
|
available_fn=(
|
|
|
|
lambda data: data.user["stats"]["lvl"] >= 14
|
|
|
|
and data.user["stats"]["mp"] >= 25
|
|
|
|
),
|
|
|
|
class_needed=HEALER,
|
|
|
|
entity_picture="shop_healAll.png",
|
|
|
|
),
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2024-07-08 07:10:28 +00:00
|
|
|
async def async_setup_entry(
|
|
|
|
hass: HomeAssistant,
|
|
|
|
entry: HabiticaConfigEntry,
|
|
|
|
async_add_entities: AddEntitiesCallback,
|
|
|
|
) -> None:
|
|
|
|
"""Set up buttons from a config entry."""
|
|
|
|
|
|
|
|
coordinator = entry.runtime_data
|
2024-10-24 17:54:59 +00:00
|
|
|
skills_added: set[str] = set()
|
|
|
|
|
|
|
|
@callback
|
|
|
|
def add_entities() -> None:
|
|
|
|
"""Add or remove a skillset based on the player's class."""
|
|
|
|
|
|
|
|
nonlocal skills_added
|
|
|
|
buttons = []
|
|
|
|
entity_registry = er.async_get(hass)
|
|
|
|
|
|
|
|
for description in CLASS_SKILLS:
|
|
|
|
if (
|
|
|
|
coordinator.data.user["stats"]["lvl"] >= 10
|
|
|
|
and coordinator.data.user["flags"]["classSelected"]
|
|
|
|
and not coordinator.data.user["preferences"]["disableClasses"]
|
|
|
|
and description.class_needed == coordinator.data.user["stats"]["class"]
|
|
|
|
):
|
|
|
|
if description.key not in skills_added:
|
|
|
|
buttons.append(HabiticaButton(coordinator, description))
|
|
|
|
skills_added.add(description.key)
|
|
|
|
elif description.key in skills_added:
|
|
|
|
if entity_id := entity_registry.async_get_entity_id(
|
|
|
|
BUTTON_DOMAIN,
|
|
|
|
DOMAIN,
|
|
|
|
f"{coordinator.config_entry.unique_id}_{description.key}",
|
|
|
|
):
|
|
|
|
entity_registry.async_remove(entity_id)
|
|
|
|
skills_added.remove(description.key)
|
|
|
|
|
|
|
|
if buttons:
|
|
|
|
async_add_entities(buttons)
|
|
|
|
|
|
|
|
coordinator.async_add_listener(add_entities)
|
|
|
|
add_entities()
|
2024-07-08 07:10:28 +00:00
|
|
|
|
|
|
|
async_add_entities(
|
|
|
|
HabiticaButton(coordinator, description) for description in BUTTON_DESCRIPTIONS
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2024-07-08 08:18:09 +00:00
|
|
|
class HabiticaButton(HabiticaBase, ButtonEntity):
|
2024-07-08 07:10:28 +00:00
|
|
|
"""Representation of a Habitica button."""
|
|
|
|
|
|
|
|
entity_description: HabiticaButtonEntityDescription
|
|
|
|
|
|
|
|
async def async_press(self) -> None:
|
|
|
|
"""Handle the button press."""
|
|
|
|
try:
|
|
|
|
await self.entity_description.press_fn(self.coordinator)
|
|
|
|
except ClientResponseError as e:
|
|
|
|
if e.status == HTTPStatus.TOO_MANY_REQUESTS:
|
|
|
|
raise ServiceValidationError(
|
|
|
|
translation_domain=DOMAIN,
|
|
|
|
translation_key="setup_rate_limit_exception",
|
|
|
|
) from e
|
|
|
|
if e.status == HTTPStatus.UNAUTHORIZED:
|
|
|
|
raise ServiceValidationError(
|
|
|
|
translation_domain=DOMAIN,
|
|
|
|
translation_key="service_call_unallowed",
|
|
|
|
) from e
|
2024-10-29 11:48:20 +00:00
|
|
|
raise HomeAssistantError(
|
2024-07-08 07:10:28 +00:00
|
|
|
translation_domain=DOMAIN,
|
|
|
|
translation_key="service_call_exception",
|
|
|
|
) from e
|
|
|
|
else:
|
2024-08-16 09:41:04 +00:00
|
|
|
await self.coordinator.async_request_refresh()
|
2024-07-08 07:10:28 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def available(self) -> bool:
|
|
|
|
"""Is entity available."""
|
2024-11-28 07:08:00 +00:00
|
|
|
|
|
|
|
return super().available and self.entity_description.available_fn(
|
|
|
|
self.coordinator.data
|
|
|
|
)
|
2024-10-24 17:54:59 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def entity_picture(self) -> str | None:
|
|
|
|
"""Return the entity picture to use in the frontend, if any."""
|
|
|
|
if entity_picture := self.entity_description.entity_picture:
|
|
|
|
return f"{ASSETS_URL}{entity_picture}"
|
|
|
|
return None
|