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 from http import HTTPStatus
import logging import logging
from typing import Any
from aiohttp import ClientResponseError from aiohttp import ClientResponseError
from habitipy.aio import HabitipyAsync from habitipy.aio import HabitipyAsync
@ -18,21 +19,35 @@ from homeassistant.const import (
CONF_VERIFY_SSL, CONF_VERIFY_SSL,
Platform, Platform,
) )
from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core import (
from homeassistant.exceptions import ConfigEntryNotReady HomeAssistant,
ServiceCall,
ServiceResponse,
SupportsResponse,
)
from homeassistant.exceptions import (
ConfigEntryNotReady,
HomeAssistantError,
ServiceValidationError,
)
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import ConfigEntrySelector
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from .const import ( from .const import (
ATTR_ARGS, ATTR_ARGS,
ATTR_CONFIG_ENTRY,
ATTR_DATA, ATTR_DATA,
ATTR_PATH, ATTR_PATH,
ATTR_SKILL,
ATTR_TASK,
CONF_API_USER, CONF_API_USER,
DEFAULT_URL, DEFAULT_URL,
DOMAIN, DOMAIN,
EVENT_API_CALL_SUCCESS, EVENT_API_CALL_SUCCESS,
SERVICE_API_CALL, SERVICE_API_CALL,
SERVICE_CAST_SKILL,
) )
from .coordinator import HabiticaDataUpdateCoordinator from .coordinator import HabiticaDataUpdateCoordinator
@ -92,6 +107,13 @@ SERVICE_API_CALL_SCHEMA = vol.Schema(
vol.Optional(ATTR_ARGS): dict, 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: 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 return True

View File

@ -21,3 +21,8 @@ MANUFACTURER = "HabitRPG, Inc."
NAME = "Habitica" NAME = "Habitica"
UNIT_TASKS = "tasks" 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": { "services": {
"api_call": { "api_call": {
"service": "mdi:console" "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"}' example: '{"text": "Use API from Home Assistant", "type": "todo"}'
selector: selector:
object: 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": { "service_call_exception": {
"message": "Unable to connect to Habitica, try again later" "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": { "issues": {
@ -180,6 +192,34 @@
"description": "Any additional JSON or URL parameter arguments. See apidoc mentioned for path. Example uses same API endpoint." "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"
}
} }
} }
} }