diff --git a/homeassistant/components/habitica/__init__.py b/homeassistant/components/habitica/__init__.py index bcf8713f9b1..8781a6e2d48 100644 --- a/homeassistant/components/habitica/__init__.py +++ b/homeassistant/components/habitica/__init__.py @@ -2,6 +2,7 @@ from http import HTTPStatus import logging +from typing import Any from aiohttp import ClientResponseError from habitipy.aio import HabitipyAsync @@ -18,21 +19,35 @@ from homeassistant.const import ( CONF_VERIFY_SSL, Platform, ) -from homeassistant.core import HomeAssistant, ServiceCall -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.core import ( + HomeAssistant, + ServiceCall, + ServiceResponse, + SupportsResponse, +) +from homeassistant.exceptions import ( + ConfigEntryNotReady, + HomeAssistantError, + ServiceValidationError, +) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.selector import ConfigEntrySelector from homeassistant.helpers.typing import ConfigType from .const import ( ATTR_ARGS, + ATTR_CONFIG_ENTRY, ATTR_DATA, ATTR_PATH, + ATTR_SKILL, + ATTR_TASK, CONF_API_USER, DEFAULT_URL, DOMAIN, EVENT_API_CALL_SUCCESS, SERVICE_API_CALL, + SERVICE_CAST_SKILL, ) from .coordinator import HabiticaDataUpdateCoordinator @@ -92,6 +107,13 @@ SERVICE_API_CALL_SCHEMA = vol.Schema( vol.Optional(ATTR_ARGS): dict, } ) +SERVICE_CAST_SKILL_SCHEMA = vol.Schema( + { + vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector(), + vol.Required(ATTR_SKILL): cv.string, + vol.Optional(ATTR_TASK): cv.string, + } +) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -108,6 +130,80 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: ) ) + async def cast_skill(call: ServiceCall) -> ServiceResponse: + """Skill action.""" + entry: HabiticaConfigEntry | None + if not ( + entry := hass.config_entries.async_get_entry(call.data[ATTR_CONFIG_ENTRY]) + ): + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="entry_not_found", + ) + coordinator = entry.runtime_data + skill = { + "pickpocket": {"spellId": "pickPocket", "cost": "10 MP"}, + "backstab": {"spellId": "backStab", "cost": "15 MP"}, + "smash": {"spellId": "smash", "cost": "10 MP"}, + "fireball": {"spellId": "fireball", "cost": "10 MP"}, + } + try: + task_id = next( + task["id"] + for task in coordinator.data.tasks + if call.data[ATTR_TASK] in (task["id"], task.get("alias")) + or call.data[ATTR_TASK] == task["text"] + ) + except StopIteration as e: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="task_not_found", + translation_placeholders={"task": f"'{call.data[ATTR_TASK]}'"}, + ) from e + + try: + response: dict[str, Any] = await coordinator.api.user.class_.cast[ + skill[call.data[ATTR_SKILL]]["spellId"] + ].post(targetId=task_id) + 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="not_enough_mana", + translation_placeholders={ + "cost": skill[call.data[ATTR_SKILL]]["cost"], + "mana": f"{int(coordinator.data.user.get("stats", {}).get("mp", 0))} MP", + }, + ) from e + if e.status == HTTPStatus.NOT_FOUND: + # could also be task not found, but the task is looked up + # before the request, so most likely wrong skill selected + # or the skill hasn't been unlocked yet. + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="skill_not_found", + translation_placeholders={"skill": call.data[ATTR_SKILL]}, + ) from e + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="service_call_exception", + ) from e + else: + await coordinator.async_request_refresh() + return response + + hass.services.async_register( + DOMAIN, + SERVICE_CAST_SKILL, + cast_skill, + schema=SERVICE_CAST_SKILL_SCHEMA, + supports_response=SupportsResponse.ONLY, + ) return True diff --git a/homeassistant/components/habitica/const.py b/homeassistant/components/habitica/const.py index 4b10e9a705b..f089be1b736 100644 --- a/homeassistant/components/habitica/const.py +++ b/homeassistant/components/habitica/const.py @@ -21,3 +21,8 @@ MANUFACTURER = "HabitRPG, Inc." NAME = "Habitica" UNIT_TASKS = "tasks" + +ATTR_CONFIG_ENTRY = "config_entry" +ATTR_SKILL = "skill" +ATTR_TASK = "task" +SERVICE_CAST_SKILL = "cast_skill" diff --git a/homeassistant/components/habitica/icons.json b/homeassistant/components/habitica/icons.json index db025c26060..544c28e4b9d 100644 --- a/homeassistant/components/habitica/icons.json +++ b/homeassistant/components/habitica/icons.json @@ -96,6 +96,9 @@ "services": { "api_call": { "service": "mdi:console" + }, + "cast_skill": { + "service": "mdi:creation-outline" } } } diff --git a/homeassistant/components/habitica/services.yaml b/homeassistant/components/habitica/services.yaml index a7ef39eb529..546ac8c1c34 100644 --- a/homeassistant/components/habitica/services.yaml +++ b/homeassistant/components/habitica/services.yaml @@ -15,3 +15,25 @@ api_call: example: '{"text": "Use API from Home Assistant", "type": "todo"}' selector: object: +cast_skill: + fields: + config_entry: + required: true + selector: + config_entry: + integration: habitica + skill: + required: true + selector: + select: + options: + - "pickpocket" + - "backstab" + - "smash" + - "fireball" + mode: dropdown + translation_key: "skill_select" + task: + required: true + selector: + text: diff --git a/homeassistant/components/habitica/strings.json b/homeassistant/components/habitica/strings.json index 8d435a5e108..824b3ab3457 100644 --- a/homeassistant/components/habitica/strings.json +++ b/homeassistant/components/habitica/strings.json @@ -154,6 +154,18 @@ }, "service_call_exception": { "message": "Unable to connect to Habitica, try again later" + }, + "not_enough_mana": { + "message": "Unable to cast skill, not enough mana. Your character has {mana}, but the skill costs {cost}." + }, + "skill_not_found": { + "message": "Unable to cast skill, your character does not have the skill or spell {skill}." + }, + "entry_not_found": { + "message": "The selected character is currently not configured or loaded in Home Assistant." + }, + "task_not_found": { + "message": "Unable to cast skill, could not find the task {task}" } }, "issues": { @@ -180,6 +192,34 @@ "description": "Any additional JSON or URL parameter arguments. See apidoc mentioned for path. Example uses same API endpoint." } } + }, + "cast_skill": { + "name": "Cast a skill", + "description": "Use a skill or spell from your Habitica character on a specific task to affect its progress or status.", + "fields": { + "config_entry": { + "name": "Select character", + "description": "Choose the Habitica character to cast the skill." + }, + "skill": { + "name": "Skill", + "description": "Select the skill or spell you want to cast on the task. Only skills corresponding to your character's class can be used." + }, + "task": { + "name": "Task name", + "description": "The name (or task ID) of the task you want to target with the skill or spell." + } + } + } + }, + "selector": { + "skill_select": { + "options": { + "fireball": "Mage: Burst of flames", + "pickpocket": "Rogue: Pickpocket", + "backstab": "Rogue: Backstab", + "smash": "Warrior: Brutal smash" + } } } }