Skip to content

Commit

Permalink
Add actions for quest handling to Habitica (#129650)
Browse files Browse the repository at this point in the history
  • Loading branch information
tr4nt0r authored Nov 9, 2024
1 parent 21d81d5 commit 5d0277a
Show file tree
Hide file tree
Showing 6 changed files with 282 additions and 5 deletions.
7 changes: 6 additions & 1 deletion homeassistant/components/habitica/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@
ATTR_SKILL = "skill"
ATTR_TASK = "task"
SERVICE_CAST_SKILL = "cast_skill"

SERVICE_START_QUEST = "start_quest"
SERVICE_ACCEPT_QUEST = "accept_quest"
SERVICE_CANCEL_QUEST = "cancel_quest"
SERVICE_ABORT_QUEST = "abort_quest"
SERVICE_REJECT_QUEST = "reject_quest"
SERVICE_LEAVE_QUEST = "leave_quest"
WARRIOR = "warrior"
ROGUE = "rogue"
HEALER = "healer"
Expand Down
18 changes: 18 additions & 0 deletions homeassistant/components/habitica/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,24 @@
},
"cast_skill": {
"service": "mdi:creation-outline"
},
"accept_quest": {
"service": "mdi:script-text"
},
"reject_quest": {
"service": "mdi:script-text"
},
"leave_quest": {
"service": "mdi:script-text"
},
"abort_quest": {
"service": "mdi:script-text-key"
},
"cancel_quest": {
"service": "mdi:script-text-key"
},
"start_quest": {
"service": "mdi:script-text-key"
}
}
}
63 changes: 63 additions & 0 deletions homeassistant/components/habitica/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,14 @@
ATTR_TASK,
DOMAIN,
EVENT_API_CALL_SUCCESS,
SERVICE_ABORT_QUEST,
SERVICE_ACCEPT_QUEST,
SERVICE_API_CALL,
SERVICE_CANCEL_QUEST,
SERVICE_CAST_SKILL,
SERVICE_LEAVE_QUEST,
SERVICE_REJECT_QUEST,
SERVICE_START_QUEST,
)
from .types import HabiticaConfigEntry

Expand All @@ -54,6 +60,12 @@
}
)

SERVICE_MANAGE_QUEST_SCHEMA = vol.Schema(
{
vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector(),
}
)


def get_config_entry(hass: HomeAssistant, entry_id: str) -> HabiticaConfigEntry:
"""Return config entry or raise if not found or not loaded."""
Expand Down Expand Up @@ -160,6 +172,57 @@ async def cast_skill(call: ServiceCall) -> ServiceResponse:
await coordinator.async_request_refresh()
return response

async def manage_quests(call: ServiceCall) -> ServiceResponse:
"""Accept, reject, start, leave or cancel quests."""
entry = get_config_entry(hass, call.data[ATTR_CONFIG_ENTRY])
coordinator = entry.runtime_data

COMMAND_MAP = {
SERVICE_ABORT_QUEST: "abort",
SERVICE_ACCEPT_QUEST: "accept",
SERVICE_CANCEL_QUEST: "cancel",
SERVICE_LEAVE_QUEST: "leave",
SERVICE_REJECT_QUEST: "reject",
SERVICE_START_QUEST: "force-start",
}
try:
return await coordinator.api.groups.party.quests[
COMMAND_MAP[call.service]
].post()
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="quest_action_unallowed"
) from e
if e.status == HTTPStatus.NOT_FOUND:
raise ServiceValidationError(
translation_domain=DOMAIN, translation_key="quest_not_found"
) from e
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="service_call_exception"
) from e

for service in (
SERVICE_ABORT_QUEST,
SERVICE_ACCEPT_QUEST,
SERVICE_CANCEL_QUEST,
SERVICE_LEAVE_QUEST,
SERVICE_REJECT_QUEST,
SERVICE_START_QUEST,
):
hass.services.async_register(
DOMAIN,
service,
manage_quests,
schema=SERVICE_MANAGE_QUEST_SCHEMA,
supports_response=SupportsResponse.ONLY,
)

hass.services.async_register(
DOMAIN,
SERVICE_API_CALL,
Expand Down
20 changes: 19 additions & 1 deletion homeassistant/components/habitica/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ api_call:
object:
cast_skill:
fields:
config_entry:
config_entry: &config_entry
required: true
selector:
config_entry:
Expand All @@ -37,3 +37,21 @@ cast_skill:
required: true
selector:
text:
accept_quest:
fields:
config_entry: *config_entry
reject_quest:
fields:
config_entry: *config_entry
start_quest:
fields:
config_entry: *config_entry
cancel_quest:
fields:
config_entry: *config_entry
abort_quest:
fields:
config_entry: *config_entry
leave_quest:
fields:
config_entry: *config_entry
69 changes: 68 additions & 1 deletion homeassistant/components/habitica/strings.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"common": {
"todos": "To-Do's",
"dailies": "Dailies"
"dailies": "Dailies",
"config_entry_name": "Select character"
},
"config": {
"abort": {
Expand Down Expand Up @@ -311,6 +312,12 @@
},
"task_not_found": {
"message": "Unable to cast skill, could not find the task {task}"
},
"quest_action_unallowed": {
"message": "Action not allowed, only quest leader or group leader can perform this action"
},
"quest_not_found": {
"message": "Unable to complete action, quest or group not found"
}
},
"issues": {
Expand Down Expand Up @@ -355,6 +362,66 @@
"description": "The name (or task ID) of the task you want to target with the skill or spell."
}
}
},
"accept_quest": {
"name": "Accept a quest invitation",
"description": "Accept a pending invitation to a quest.",
"fields": {
"config_entry": {
"name": "[%key:component::habitica::common::config_entry_name%]",
"description": "Choose the Habitica character for which to perform the action."
}
}
},
"reject_quest": {
"name": "Reject a quest invitation",
"description": "Reject a pending invitation to a quest.",
"fields": {
"config_entry": {
"name": "[%key:component::habitica::common::config_entry_name%]",
"description": "[%key:component::habitica::services::accept_quest::fields::config_entry::description%]"
}
}
},
"leave_quest": {
"name": "Leave a quest",
"description": "Leave the current quest you are participating in.",
"fields": {
"config_entry": {
"name": "[%key:component::habitica::common::config_entry_name%]",
"description": "[%key:component::habitica::services::accept_quest::fields::config_entry::description%]"
}
}
},
"abort_quest": {
"name": "Abort an active quest",
"description": "Terminate your party's ongoing quest. All progress will be lost and the quest roll returned to the owner's inventory. Only quest leader or group leader can perform this action.",
"fields": {
"config_entry": {
"name": "[%key:component::habitica::common::config_entry_name%]",
"description": "[%key:component::habitica::services::accept_quest::fields::config_entry::description%]"
}
}
},
"cancel_quest": {
"name": "Cancel a pending quest",
"description": "Cancel a quest that has not yet startet. All accepted and pending invitations will be canceled and the quest roll returned to the owner's inventory. Only quest leader or group leader can perform this action.",
"fields": {
"config_entry": {
"name": "[%key:component::habitica::common::config_entry_name%]",
"description": "[%key:component::habitica::services::accept_quest::fields::config_entry::description%]"
}
}
},
"start_quest": {
"name": "Force-start a pending quest",
"description": "Begin the quest immediately, bypassing any pending invitations that haven't been accepted or rejected. Only quest leader or group leader can perform this action.",
"fields": {
"config_entry": {
"name": "[%key:component::habitica::common::config_entry_name%]",
"description": "[%key:component::habitica::services::accept_quest::fields::config_entry::description%]"
}
}
}
},
"selector": {
Expand Down
110 changes: 108 additions & 2 deletions tests/components/habitica/test_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@
ATTR_TASK,
DEFAULT_URL,
DOMAIN,
SERVICE_ABORT_QUEST,
SERVICE_ACCEPT_QUEST,
SERVICE_CANCEL_QUEST,
SERVICE_CAST_SKILL,
SERVICE_LEAVE_QUEST,
SERVICE_REJECT_QUEST,
SERVICE_START_QUEST,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
Expand All @@ -24,6 +30,9 @@
from tests.common import MockConfigEntry
from tests.test_util.aiohttp import AiohttpClientMocker

REQUEST_EXCEPTION_MSG = "Unable to connect to Habitica, try again later"
RATE_LIMIT_EXCEPTION_MSG = "Rate limit exceeded, try again later"


@pytest.fixture(autouse=True)
def services_only() -> Generator[None]:
Expand Down Expand Up @@ -168,7 +177,7 @@ async def test_cast_skill(
},
HTTPStatus.TOO_MANY_REQUESTS,
ServiceValidationError,
"Rate limit exceeded, try again later",
RATE_LIMIT_EXCEPTION_MSG,
),
(
{
Expand All @@ -195,7 +204,7 @@ async def test_cast_skill(
},
HTTPStatus.BAD_REQUEST,
HomeAssistantError,
"Unable to connect to Habitica, try again later",
REQUEST_EXCEPTION_MSG,
),
],
)
Expand Down Expand Up @@ -271,3 +280,100 @@ async def test_get_config_entry(
return_response=True,
blocking=True,
)


@pytest.mark.parametrize(
("service", "command"),
[
(SERVICE_ABORT_QUEST, "abort"),
(SERVICE_ACCEPT_QUEST, "accept"),
(SERVICE_CANCEL_QUEST, "cancel"),
(SERVICE_LEAVE_QUEST, "leave"),
(SERVICE_REJECT_QUEST, "reject"),
(SERVICE_START_QUEST, "force-start"),
],
ids=[],
)
async def test_handle_quests(
hass: HomeAssistant,
config_entry: MockConfigEntry,
mock_habitica: AiohttpClientMocker,
service: str,
command: str,
) -> None:
"""Test Habitica actions for quest handling."""

mock_habitica.post(
f"{DEFAULT_URL}/api/v3/groups/party/quests/{command}",
json={"success": True, "data": {}},
)

await hass.services.async_call(
DOMAIN,
service,
service_data={ATTR_CONFIG_ENTRY: config_entry.entry_id},
return_response=True,
blocking=True,
)

assert mock_called_with(
mock_habitica,
"post",
f"{DEFAULT_URL}/api/v3/groups/party/quests/{command}",
)


@pytest.mark.parametrize(
(
"http_status",
"expected_exception",
"expected_exception_msg",
),
[
(
HTTPStatus.TOO_MANY_REQUESTS,
ServiceValidationError,
RATE_LIMIT_EXCEPTION_MSG,
),
(
HTTPStatus.NOT_FOUND,
ServiceValidationError,
"Unable to complete action, quest or group not found",
),
(
HTTPStatus.UNAUTHORIZED,
ServiceValidationError,
"Action not allowed, only quest leader or group leader can perform this action",
),
(
HTTPStatus.BAD_REQUEST,
HomeAssistantError,
REQUEST_EXCEPTION_MSG,
),
],
)
@pytest.mark.usefixtures("mock_habitica")
async def test_handle_quests_exceptions(
hass: HomeAssistant,
config_entry: MockConfigEntry,
mock_habitica: AiohttpClientMocker,
http_status: HTTPStatus,
expected_exception: Exception,
expected_exception_msg: str,
) -> None:
"""Test Habitica handle quests action exceptions."""

mock_habitica.post(
f"{DEFAULT_URL}/api/v3/groups/party/quests/accept",
json={"success": True, "data": {}},
status=http_status,
)

with pytest.raises(expected_exception, match=expected_exception_msg):
await hass.services.async_call(
DOMAIN,
SERVICE_ACCEPT_QUEST,
service_data={ATTR_CONFIG_ENTRY: config_entry.entry_id},
return_response=True,
blocking=True,
)

0 comments on commit 5d0277a

Please sign in to comment.