374 lines
13 KiB
Python
374 lines
13 KiB
Python
"""Habitica button platform."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Callable
|
|
from dataclasses import dataclass
|
|
from enum import StrEnum
|
|
from typing import Any
|
|
|
|
from aiohttp import ClientError
|
|
from habiticalib import (
|
|
HabiticaClass,
|
|
HabiticaException,
|
|
NotAuthorizedError,
|
|
Skill,
|
|
TaskType,
|
|
TooManyRequestsError,
|
|
)
|
|
|
|
from homeassistant.components.button import (
|
|
DOMAIN as BUTTON_DOMAIN,
|
|
ButtonEntity,
|
|
ButtonEntityDescription,
|
|
)
|
|
from homeassistant.core import HomeAssistant, callback
|
|
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
|
from homeassistant.helpers import entity_registry as er
|
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
|
|
from .const import ASSETS_URL, DOMAIN
|
|
from .coordinator import HabiticaData, HabiticaDataUpdateCoordinator
|
|
from .entity import HabiticaBase
|
|
from .types import HabiticaConfigEntry
|
|
|
|
PARALLEL_UPDATES = 1
|
|
|
|
|
|
@dataclass(kw_only=True, frozen=True)
|
|
class HabiticaButtonEntityDescription(ButtonEntityDescription):
|
|
"""Describes Habitica button entity."""
|
|
|
|
press_fn: Callable[[HabiticaDataUpdateCoordinator], Any]
|
|
available_fn: Callable[[HabiticaData], bool]
|
|
class_needed: HabiticaClass | None = None
|
|
entity_picture: str | None = None
|
|
|
|
|
|
class HabiticaButtonEntity(StrEnum):
|
|
"""Habitica button entities."""
|
|
|
|
RUN_CRON = "run_cron"
|
|
BUY_HEALTH_POTION = "buy_health_potion"
|
|
ALLOCATE_ALL_STAT_POINTS = "allocate_all_stat_points"
|
|
REVIVE = "revive"
|
|
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"
|
|
|
|
|
|
BUTTON_DESCRIPTIONS: tuple[HabiticaButtonEntityDescription, ...] = (
|
|
HabiticaButtonEntityDescription(
|
|
key=HabiticaButtonEntity.RUN_CRON,
|
|
translation_key=HabiticaButtonEntity.RUN_CRON,
|
|
press_fn=lambda coordinator: coordinator.habitica.run_cron(),
|
|
available_fn=lambda data: data.user.needsCron is True,
|
|
),
|
|
HabiticaButtonEntityDescription(
|
|
key=HabiticaButtonEntity.BUY_HEALTH_POTION,
|
|
translation_key=HabiticaButtonEntity.BUY_HEALTH_POTION,
|
|
press_fn=lambda coordinator: coordinator.habitica.buy_health_potion(),
|
|
available_fn=(
|
|
lambda data: (data.user.stats.gp or 0) >= 25
|
|
and (data.user.stats.hp or 0) < 50
|
|
),
|
|
entity_picture="shop_potion.png",
|
|
),
|
|
HabiticaButtonEntityDescription(
|
|
key=HabiticaButtonEntity.ALLOCATE_ALL_STAT_POINTS,
|
|
translation_key=HabiticaButtonEntity.ALLOCATE_ALL_STAT_POINTS,
|
|
press_fn=lambda coordinator: coordinator.habitica.allocate_stat_points(),
|
|
available_fn=(
|
|
lambda data: data.user.preferences.automaticAllocation is True
|
|
and (data.user.stats.points or 0) > 0
|
|
),
|
|
),
|
|
HabiticaButtonEntityDescription(
|
|
key=HabiticaButtonEntity.REVIVE,
|
|
translation_key=HabiticaButtonEntity.REVIVE,
|
|
press_fn=lambda coordinator: coordinator.habitica.revive(),
|
|
available_fn=lambda data: data.user.stats.hp == 0,
|
|
),
|
|
)
|
|
|
|
|
|
CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = (
|
|
HabiticaButtonEntityDescription(
|
|
key=HabiticaButtonEntity.MPHEAL,
|
|
translation_key=HabiticaButtonEntity.MPHEAL,
|
|
press_fn=(
|
|
lambda coordinator: coordinator.habitica.cast_skill(Skill.ETHEREAL_SURGE)
|
|
),
|
|
available_fn=(
|
|
lambda data: (data.user.stats.lvl or 0) >= 12
|
|
and (data.user.stats.mp or 0) >= 30
|
|
),
|
|
class_needed=HabiticaClass.MAGE,
|
|
entity_picture="shop_mpheal.png",
|
|
),
|
|
HabiticaButtonEntityDescription(
|
|
key=HabiticaButtonEntity.EARTH,
|
|
translation_key=HabiticaButtonEntity.EARTH,
|
|
press_fn=lambda coordinator: coordinator.habitica.cast_skill(Skill.EARTHQUAKE),
|
|
available_fn=(
|
|
lambda data: (data.user.stats.lvl or 0) >= 13
|
|
and (data.user.stats.mp or 0) >= 35
|
|
),
|
|
class_needed=HabiticaClass.MAGE,
|
|
entity_picture="shop_earth.png",
|
|
),
|
|
HabiticaButtonEntityDescription(
|
|
key=HabiticaButtonEntity.FROST,
|
|
translation_key=HabiticaButtonEntity.FROST,
|
|
press_fn=(
|
|
lambda coordinator: coordinator.habitica.cast_skill(Skill.CHILLING_FROST)
|
|
),
|
|
# chilling frost can only be cast once per day (streaks buff is false)
|
|
available_fn=(
|
|
lambda data: (data.user.stats.lvl or 0) >= 14
|
|
and (data.user.stats.mp or 0) >= 40
|
|
and not data.user.stats.buffs.streaks
|
|
),
|
|
class_needed=HabiticaClass.MAGE,
|
|
entity_picture="shop_frost.png",
|
|
),
|
|
HabiticaButtonEntityDescription(
|
|
key=HabiticaButtonEntity.DEFENSIVE_STANCE,
|
|
translation_key=HabiticaButtonEntity.DEFENSIVE_STANCE,
|
|
press_fn=(
|
|
lambda coordinator: coordinator.habitica.cast_skill(Skill.DEFENSIVE_STANCE)
|
|
),
|
|
available_fn=(
|
|
lambda data: (data.user.stats.lvl or 0) >= 12
|
|
and (data.user.stats.mp or 0) >= 25
|
|
),
|
|
class_needed=HabiticaClass.WARRIOR,
|
|
entity_picture="shop_defensiveStance.png",
|
|
),
|
|
HabiticaButtonEntityDescription(
|
|
key=HabiticaButtonEntity.VALOROUS_PRESENCE,
|
|
translation_key=HabiticaButtonEntity.VALOROUS_PRESENCE,
|
|
press_fn=(
|
|
lambda coordinator: coordinator.habitica.cast_skill(Skill.VALOROUS_PRESENCE)
|
|
),
|
|
available_fn=(
|
|
lambda data: (data.user.stats.lvl or 0) >= 13
|
|
and (data.user.stats.mp or 0) >= 20
|
|
),
|
|
class_needed=HabiticaClass.WARRIOR,
|
|
entity_picture="shop_valorousPresence.png",
|
|
),
|
|
HabiticaButtonEntityDescription(
|
|
key=HabiticaButtonEntity.INTIMIDATE,
|
|
translation_key=HabiticaButtonEntity.INTIMIDATE,
|
|
press_fn=(
|
|
lambda coordinator: coordinator.habitica.cast_skill(Skill.INTIMIDATING_GAZE)
|
|
),
|
|
available_fn=(
|
|
lambda data: (data.user.stats.lvl or 0) >= 14
|
|
and (data.user.stats.mp or 0) >= 15
|
|
),
|
|
class_needed=HabiticaClass.WARRIOR,
|
|
entity_picture="shop_intimidate.png",
|
|
),
|
|
HabiticaButtonEntityDescription(
|
|
key=HabiticaButtonEntity.TOOLS_OF_TRADE,
|
|
translation_key=HabiticaButtonEntity.TOOLS_OF_TRADE,
|
|
press_fn=(
|
|
lambda coordinator: coordinator.habitica.cast_skill(
|
|
Skill.TOOLS_OF_THE_TRADE
|
|
)
|
|
),
|
|
available_fn=(
|
|
lambda data: (data.user.stats.lvl or 0) >= 13
|
|
and (data.user.stats.mp or 0) >= 25
|
|
),
|
|
class_needed=HabiticaClass.ROGUE,
|
|
entity_picture="shop_toolsOfTrade.png",
|
|
),
|
|
HabiticaButtonEntityDescription(
|
|
key=HabiticaButtonEntity.STEALTH,
|
|
translation_key=HabiticaButtonEntity.STEALTH,
|
|
press_fn=lambda coordinator: coordinator.habitica.cast_skill(Skill.STEALTH),
|
|
# Stealth buffs stack and it can only be cast if the amount of
|
|
# buffs is smaller than the amount of unfinished dailies
|
|
available_fn=(
|
|
lambda data: (data.user.stats.lvl or 0) >= 14
|
|
and (data.user.stats.mp or 0) >= 45
|
|
and (data.user.stats.buffs.stealth or 0)
|
|
< len(
|
|
[
|
|
r
|
|
for r in data.tasks
|
|
if r.Type is TaskType.DAILY
|
|
and r.isDue is True
|
|
and r.completed is False
|
|
]
|
|
)
|
|
),
|
|
class_needed=HabiticaClass.ROGUE,
|
|
entity_picture="shop_stealth.png",
|
|
),
|
|
HabiticaButtonEntityDescription(
|
|
key=HabiticaButtonEntity.HEAL,
|
|
translation_key=HabiticaButtonEntity.HEAL,
|
|
press_fn=(
|
|
lambda coordinator: coordinator.habitica.cast_skill(Skill.HEALING_LIGHT)
|
|
),
|
|
available_fn=(
|
|
lambda data: (data.user.stats.lvl or 0) >= 11
|
|
and (data.user.stats.mp or 0) >= 15
|
|
and (data.user.stats.hp or 0) < 50
|
|
),
|
|
class_needed=HabiticaClass.HEALER,
|
|
entity_picture="shop_heal.png",
|
|
),
|
|
HabiticaButtonEntityDescription(
|
|
key=HabiticaButtonEntity.BRIGHTNESS,
|
|
translation_key=HabiticaButtonEntity.BRIGHTNESS,
|
|
press_fn=(
|
|
lambda coordinator: coordinator.habitica.cast_skill(
|
|
Skill.SEARING_BRIGHTNESS
|
|
)
|
|
),
|
|
available_fn=(
|
|
lambda data: (data.user.stats.lvl or 0) >= 12
|
|
and (data.user.stats.mp or 0) >= 15
|
|
),
|
|
class_needed=HabiticaClass.HEALER,
|
|
entity_picture="shop_brightness.png",
|
|
),
|
|
HabiticaButtonEntityDescription(
|
|
key=HabiticaButtonEntity.PROTECT_AURA,
|
|
translation_key=HabiticaButtonEntity.PROTECT_AURA,
|
|
press_fn=(
|
|
lambda coordinator: coordinator.habitica.cast_skill(Skill.PROTECTIVE_AURA)
|
|
),
|
|
available_fn=(
|
|
lambda data: (data.user.stats.lvl or 0) >= 13
|
|
and (data.user.stats.mp or 0) >= 30
|
|
),
|
|
class_needed=HabiticaClass.HEALER,
|
|
entity_picture="shop_protectAura.png",
|
|
),
|
|
HabiticaButtonEntityDescription(
|
|
key=HabiticaButtonEntity.HEAL_ALL,
|
|
translation_key=HabiticaButtonEntity.HEAL_ALL,
|
|
press_fn=lambda coordinator: coordinator.habitica.cast_skill(Skill.BLESSING),
|
|
available_fn=(
|
|
lambda data: (data.user.stats.lvl or 0) >= 14
|
|
and (data.user.stats.mp or 0) >= 25
|
|
),
|
|
class_needed=HabiticaClass.HEALER,
|
|
entity_picture="shop_healAll.png",
|
|
),
|
|
)
|
|
|
|
|
|
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
|
|
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 or 0) >= 10
|
|
and coordinator.data.user.flags.classSelected
|
|
and not coordinator.data.user.preferences.disableClasses
|
|
and description.class_needed is 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()
|
|
|
|
async_add_entities(
|
|
HabiticaButton(coordinator, description) for description in BUTTON_DESCRIPTIONS
|
|
)
|
|
|
|
|
|
class HabiticaButton(HabiticaBase, ButtonEntity):
|
|
"""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 TooManyRequestsError as e:
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="setup_rate_limit_exception",
|
|
translation_placeholders={"retry_after": str(e.retry_after)},
|
|
) from e
|
|
except NotAuthorizedError as e:
|
|
raise ServiceValidationError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="service_call_unallowed",
|
|
) from e
|
|
except HabiticaException as e:
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="service_call_exception",
|
|
translation_placeholders={"reason": e.error.message},
|
|
) from e
|
|
except ClientError as e:
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="service_call_exception",
|
|
translation_placeholders={"reason": str(e)},
|
|
) from e
|
|
else:
|
|
await self.coordinator.async_request_refresh()
|
|
|
|
@property
|
|
def available(self) -> bool:
|
|
"""Is entity available."""
|
|
|
|
return super().available and self.entity_description.available_fn(
|
|
self.coordinator.data
|
|
)
|
|
|
|
@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
|