"""Habitica button platform.""" from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from enum import StrEnum from http import HTTPStatus from typing import Any from aiohttp import ClientResponseError 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, HEALER, MAGE, ROGUE, WARRIOR from .coordinator import HabiticaData, HabiticaDataUpdateCoordinator from .entity import HabiticaBase from .types import HabiticaConfigEntry @dataclass(kw_only=True, frozen=True) class HabiticaButtonEntityDescription(ButtonEntityDescription): """Describes Habitica button entity.""" press_fn: Callable[[HabiticaDataUpdateCoordinator], Any] available_fn: Callable[[HabiticaData], bool] | None = None class_needed: str | None = None entity_picture: str | None = None 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" 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=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 ), entity_picture="shop_potion.png", ), 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, ), ) 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, press_fn=lambda coordinator: coordinator.api.user.class_.cast["frost"].post(), # chilling frost can only be cast once per day (streaks buff is false) available_fn=( lambda data: data.user["stats"]["lvl"] >= 14 and data.user["stats"]["mp"] >= 40 and not data.user["stats"]["buffs"]["streaks"] ), 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" ].post() ), 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" ].post() ), 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=( lambda coordinator: coordinator.api.user.class_.cast["intimidate"].post() ), 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=( lambda coordinator: coordinator.api.user.class_.cast["stealth"].post() ), # Stealth buffs stack and it can only be cast if the amount of # unfinished dailies is smaller than the amount of buffs available_fn=( lambda data: data.user["stats"]["lvl"] >= 14 and data.user["stats"]["mp"] >= 45 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 ] ) ), class_needed=ROGUE, entity_picture="shop_stealth.png", ), HabiticaButtonEntityDescription( key=HabitipyButtonEntity.HEAL, translation_key=HabitipyButtonEntity.HEAL, press_fn=lambda coordinator: coordinator.api.user.class_.cast["heal"].post(), available_fn=( lambda data: data.user["stats"]["lvl"] >= 11 and data.user["stats"]["mp"] >= 15 and data.user["stats"]["hp"] < 50 ), class_needed=HEALER, entity_picture="shop_heal.png", ), HabiticaButtonEntityDescription( key=HabitipyButtonEntity.BRIGHTNESS, translation_key=HabitipyButtonEntity.BRIGHTNESS, press_fn=( lambda coordinator: coordinator.api.user.class_.cast["brightness"].post() ), 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", ), ) 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"] >= 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() 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 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 raise HomeAssistantError( translation_domain=DOMAIN, translation_key="service_call_exception", ) from e else: await self.coordinator.async_request_refresh() @property def available(self) -> bool: """Is entity available.""" if not super().available: return False if self.entity_description.available_fn: return self.entity_description.available_fn(self.coordinator.data) return True @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