diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index d54bf9d63fb..885985c2d0a 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -82,7 +82,7 @@ INSTANCE_LIST_SCHEMA = vol.All( ) CONFIG_SCHEMA = vol.Schema({DOMAIN: INSTANCE_LIST_SCHEMA}, extra=vol.ALLOW_EXTRA) -PLATFORMS = [Platform.SENSOR, Platform.TODO] +PLATFORMS = [Platform.BUTTON, Platform.SENSOR, Platform.TODO] SERVICE_API_CALL_SCHEMA = vol.Schema( { diff --git a/homeassistant/components/habitica/button.py b/homeassistant/components/habitica/button.py new file mode 100644 index 00000000000..626655da1aa --- /dev/null +++ b/homeassistant/components/habitica/button.py @@ -0,0 +1,150 @@ +"""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 TYPE_CHECKING, Any + +from aiohttp import ClientResponseError + +from homeassistant.components.button import ButtonEntity, ButtonEntityDescription +from homeassistant.const import CONF_NAME, CONF_URL +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from . import HabiticaConfigEntry +from .const import DOMAIN, MANUFACTURER, NAME +from .coordinator import HabiticaData, HabiticaDataUpdateCoordinator + + +@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 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" + + +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 + ), + ), + 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, + ), +) + + +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 + + async_add_entities( + HabiticaButton(coordinator, description) for description in BUTTON_DESCRIPTIONS + ) + + +class HabiticaButton(CoordinatorEntity[HabiticaDataUpdateCoordinator], ButtonEntity): + """Representation of a Habitica button.""" + + _attr_has_entity_name = True + entity_description: HabiticaButtonEntityDescription + + def __init__( + self, + coordinator: HabiticaDataUpdateCoordinator, + entity_description: HabiticaButtonEntityDescription, + ) -> None: + """Initialize a Habitica button.""" + super().__init__(coordinator) + if TYPE_CHECKING: + assert coordinator.config_entry.unique_id + self.entity_description = entity_description + self._attr_unique_id = ( + f"{coordinator.config_entry.unique_id}_{entity_description.key}" + ) + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + manufacturer=MANUFACTURER, + model=NAME, + name=coordinator.config_entry.data[CONF_NAME], + configuration_url=coordinator.config_entry.data[CONF_URL], + identifiers={(DOMAIN, coordinator.config_entry.unique_id)}, + ) + + 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 ServiceValidationError( + translation_domain=DOMAIN, + translation_key="service_call_exception", + ) from e + else: + await self.coordinator.async_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 diff --git a/homeassistant/components/habitica/icons.json b/homeassistant/components/habitica/icons.json index def355678c1..8d4ec2b5249 100644 --- a/homeassistant/components/habitica/icons.json +++ b/homeassistant/components/habitica/icons.json @@ -8,6 +8,20 @@ "default": "mdi:calendar-month" } }, + "button": { + "run_cron": { + "default": "mdi:weather-sunset" + }, + "buy_health_potion": { + "default": "mdi:flask-round-bottom" + }, + "allocate_all_stat_points": { + "default": "mdi:chart-box-outline" + }, + "revive": { + "default": "mdi:grave-stone" + } + }, "sensor": { "display_name": { "default": "mdi:account-circle" diff --git a/homeassistant/components/habitica/strings.json b/homeassistant/components/habitica/strings.json index 7ff1e7b1a81..c49beaacd6e 100644 --- a/homeassistant/components/habitica/strings.json +++ b/homeassistant/components/habitica/strings.json @@ -20,6 +20,20 @@ } }, "entity": { + "button": { + "run_cron": { + "name": "Start my day" + }, + "buy_health_potion": { + "name": "Buy a health potion" + }, + "allocate_all_stat_points": { + "name": "Allocate all stat points" + }, + "revive": { + "name": "Revive from death" + } + }, "sensor": { "display_name": { "name": "Display name" @@ -94,6 +108,12 @@ }, "setup_rate_limit_exception": { "message": "Currently rate limited, try again later" + }, + "service_call_unallowed": { + "message": "Unable to carry out this action, because the required conditions are not met" + }, + "service_call_exception": { + "message": "Unable to connect to Habitica, try again later" } }, "issues": {