Add cast skill action to Habitica integration (#127000)

* Add cast skill action for task skills

* exceptions

* task not found exception

* request refresh to update mana/xp sensors

* Changes

* remove service_call prefix

* fixes
This commit is contained in:
Manu 2024-10-06 10:33:32 +02:00 committed by GitHub
parent 546d0b25b0
commit 3e8bc98f23
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 168 additions and 2 deletions

View file

@ -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

View file

@ -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"

View file

@ -96,6 +96,9 @@
"services": {
"api_call": {
"service": "mdi:console"
},
"cast_skill": {
"service": "mdi:creation-outline"
}
}
}

View file

@ -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:

View file

@ -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"
}
}
}
}