mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 03:07:37 +00:00
Add actions for scoring habits and rewards in Habitica (#129605)
This commit is contained in:
parent
d0ad834d93
commit
0677bba5bd
@ -25,6 +25,7 @@ UNIT_TASKS = "tasks"
|
||||
ATTR_CONFIG_ENTRY = "config_entry"
|
||||
ATTR_SKILL = "skill"
|
||||
ATTR_TASK = "task"
|
||||
ATTR_DIRECTION = "direction"
|
||||
SERVICE_CAST_SKILL = "cast_skill"
|
||||
SERVICE_START_QUEST = "start_quest"
|
||||
SERVICE_ACCEPT_QUEST = "accept_quest"
|
||||
@ -32,6 +33,9 @@ SERVICE_CANCEL_QUEST = "cancel_quest"
|
||||
SERVICE_ABORT_QUEST = "abort_quest"
|
||||
SERVICE_REJECT_QUEST = "reject_quest"
|
||||
SERVICE_LEAVE_QUEST = "leave_quest"
|
||||
SERVICE_SCORE_HABIT = "score_habit"
|
||||
SERVICE_SCORE_REWARD = "score_reward"
|
||||
|
||||
WARRIOR = "warrior"
|
||||
ROGUE = "rogue"
|
||||
HEALER = "healer"
|
||||
|
@ -181,6 +181,12 @@
|
||||
},
|
||||
"start_quest": {
|
||||
"service": "mdi:script-text-key"
|
||||
},
|
||||
"score_habit": {
|
||||
"service": "mdi:counter"
|
||||
},
|
||||
"score_reward": {
|
||||
"service": "mdi:sack"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ from .const import (
|
||||
ATTR_ARGS,
|
||||
ATTR_CONFIG_ENTRY,
|
||||
ATTR_DATA,
|
||||
ATTR_DIRECTION,
|
||||
ATTR_PATH,
|
||||
ATTR_SKILL,
|
||||
ATTR_TASK,
|
||||
@ -37,6 +38,8 @@ from .const import (
|
||||
SERVICE_CAST_SKILL,
|
||||
SERVICE_LEAVE_QUEST,
|
||||
SERVICE_REJECT_QUEST,
|
||||
SERVICE_SCORE_HABIT,
|
||||
SERVICE_SCORE_REWARD,
|
||||
SERVICE_START_QUEST,
|
||||
)
|
||||
from .types import HabiticaConfigEntry
|
||||
@ -65,6 +68,13 @@ SERVICE_MANAGE_QUEST_SCHEMA = vol.Schema(
|
||||
vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector(),
|
||||
}
|
||||
)
|
||||
SERVICE_SCORE_TASK_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector(),
|
||||
vol.Required(ATTR_TASK): cv.string,
|
||||
vol.Optional(ATTR_DIRECTION): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def get_config_entry(hass: HomeAssistant, entry_id: str) -> HabiticaConfigEntry:
|
||||
@ -82,7 +92,7 @@ def get_config_entry(hass: HomeAssistant, entry_id: str) -> HabiticaConfigEntry:
|
||||
return entry
|
||||
|
||||
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
|
||||
"""Set up services for Habitica integration."""
|
||||
|
||||
async def handle_api_call(call: ServiceCall) -> None:
|
||||
@ -223,6 +233,53 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
|
||||
async def score_task(call: ServiceCall) -> ServiceResponse:
|
||||
"""Score a task action."""
|
||||
entry = get_config_entry(hass, call.data[ATTR_CONFIG_ENTRY])
|
||||
coordinator = entry.runtime_data
|
||||
try:
|
||||
task_id, task_value = next(
|
||||
(task["id"], task.get("value"))
|
||||
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.tasks[task_id]
|
||||
.score[call.data.get(ATTR_DIRECTION, "up")]
|
||||
.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 and task_value is not None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="not_enough_gold",
|
||||
translation_placeholders={
|
||||
"gold": f"{coordinator.data.user["stats"]["gp"]:.2f} GP",
|
||||
"cost": f"{task_value} GP",
|
||||
},
|
||||
) 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_API_CALL,
|
||||
@ -237,3 +294,18 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
schema=SERVICE_CAST_SKILL_SCHEMA,
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SCORE_HABIT,
|
||||
score_task,
|
||||
schema=SERVICE_SCORE_TASK_SCHEMA,
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SCORE_REWARD,
|
||||
score_task,
|
||||
schema=SERVICE_SCORE_TASK_SCHEMA,
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
|
@ -33,7 +33,7 @@ cast_skill:
|
||||
- "fireball"
|
||||
mode: dropdown
|
||||
translation_key: "skill_select"
|
||||
task:
|
||||
task: &task
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
@ -55,3 +55,20 @@ abort_quest:
|
||||
leave_quest:
|
||||
fields:
|
||||
config_entry: *config_entry
|
||||
score_habit:
|
||||
fields:
|
||||
config_entry: *config_entry
|
||||
task: *task
|
||||
direction:
|
||||
required: true
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- value: up
|
||||
label: "➕"
|
||||
- value: down
|
||||
label: "➖"
|
||||
score_reward:
|
||||
fields:
|
||||
config_entry: *config_entry
|
||||
task: *task
|
||||
|
@ -301,6 +301,9 @@
|
||||
"not_enough_mana": {
|
||||
"message": "Unable to cast skill, not enough mana. Your character has {mana}, but the skill costs {cost}."
|
||||
},
|
||||
"not_enough_gold": {
|
||||
"message": "Unable to buy reward, not enough gold. Your character has {gold}, but the reward costs {cost}."
|
||||
},
|
||||
"skill_not_found": {
|
||||
"message": "Unable to cast skill, your character does not have the skill or spell {skill}."
|
||||
},
|
||||
@ -311,7 +314,7 @@
|
||||
"message": "The selected character is currently not loaded or disabled in Home Assistant."
|
||||
},
|
||||
"task_not_found": {
|
||||
"message": "Unable to cast skill, could not find the task {task}"
|
||||
"message": "Unable to complete action, could not find the task {task}"
|
||||
},
|
||||
"quest_action_unallowed": {
|
||||
"message": "Action not allowed, only quest leader or group leader can perform this action"
|
||||
@ -350,7 +353,7 @@
|
||||
"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",
|
||||
"name": "[%key:component::habitica::common::config_entry_name%]",
|
||||
"description": "Choose the Habitica character to cast the skill."
|
||||
},
|
||||
"skill": {
|
||||
@ -422,6 +425,38 @@
|
||||
"description": "[%key:component::habitica::services::accept_quest::fields::config_entry::description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"score_habit": {
|
||||
"name": "Track a habit",
|
||||
"description": "Increase the positive or negative streak of a habit to track its progress.",
|
||||
"fields": {
|
||||
"config_entry": {
|
||||
"name": "[%key:component::habitica::common::config_entry_name%]",
|
||||
"description": "Select the Habitica character tracking your habit."
|
||||
},
|
||||
"task": {
|
||||
"name": "Habit name",
|
||||
"description": "The name (or task ID) of the Habitica habit."
|
||||
},
|
||||
"direction": {
|
||||
"name": "Reward or loss",
|
||||
"description": "Is it positive or negative progress you want to track for your habit."
|
||||
}
|
||||
}
|
||||
},
|
||||
"score_reward": {
|
||||
"name": "Buy a reward",
|
||||
"description": "Reward yourself and buy one of your custom rewards with gold earned by fulfilling tasks.",
|
||||
"fields": {
|
||||
"config_entry": {
|
||||
"name": "[%key:component::habitica::common::config_entry_name%]",
|
||||
"description": "Select the Habitica character buying the reward."
|
||||
},
|
||||
"task": {
|
||||
"name": "Reward name",
|
||||
"description": "The name (or task ID) of the custom reward."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
|
@ -34,7 +34,7 @@ def mock_called_with(
|
||||
(
|
||||
call
|
||||
for call in mock_client.mock_calls
|
||||
if call[0] == method.upper() and call[1] == URL(url)
|
||||
if call[0].upper() == method.upper() and call[1] == URL(url)
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
@ -121,7 +121,8 @@
|
||||
"createdAt": "2024-07-07T17:51:53.264Z",
|
||||
"updatedAt": "2024-07-12T09:58:45.438Z",
|
||||
"userId": "5f359083-ef78-4af0-985a-0b2c6d05797c",
|
||||
"id": "e97659e0-2c42-4599-a7bb-00282adc410d"
|
||||
"id": "e97659e0-2c42-4599-a7bb-00282adc410d",
|
||||
"alias": "create_a_task"
|
||||
},
|
||||
{
|
||||
"_id": "564b9ac9-c53d-4638-9e7f-1cd96fe19baa",
|
||||
|
@ -9,6 +9,7 @@ import pytest
|
||||
|
||||
from homeassistant.components.habitica.const import (
|
||||
ATTR_CONFIG_ENTRY,
|
||||
ATTR_DIRECTION,
|
||||
ATTR_SKILL,
|
||||
ATTR_TASK,
|
||||
DEFAULT_URL,
|
||||
@ -19,6 +20,8 @@ from homeassistant.components.habitica.const import (
|
||||
SERVICE_CAST_SKILL,
|
||||
SERVICE_LEAVE_QUEST,
|
||||
SERVICE_REJECT_QUEST,
|
||||
SERVICE_SCORE_HABIT,
|
||||
SERVICE_SCORE_REWARD,
|
||||
SERVICE_START_QUEST,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
@ -168,7 +171,7 @@ async def test_cast_skill(
|
||||
},
|
||||
HTTPStatus.OK,
|
||||
ServiceValidationError,
|
||||
"Unable to cast skill, could not find the task 'task-not-found",
|
||||
"Unable to complete action, could not find the task 'task-not-found'",
|
||||
),
|
||||
(
|
||||
{
|
||||
@ -377,3 +380,169 @@ async def test_handle_quests_exceptions(
|
||||
return_response=True,
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service", "service_data", "task_id"),
|
||||
[
|
||||
(
|
||||
SERVICE_SCORE_HABIT,
|
||||
{
|
||||
ATTR_TASK: "e97659e0-2c42-4599-a7bb-00282adc410d",
|
||||
ATTR_DIRECTION: "up",
|
||||
},
|
||||
"e97659e0-2c42-4599-a7bb-00282adc410d",
|
||||
),
|
||||
(
|
||||
SERVICE_SCORE_HABIT,
|
||||
{
|
||||
ATTR_TASK: "e97659e0-2c42-4599-a7bb-00282adc410d",
|
||||
ATTR_DIRECTION: "down",
|
||||
},
|
||||
"e97659e0-2c42-4599-a7bb-00282adc410d",
|
||||
),
|
||||
(
|
||||
SERVICE_SCORE_REWARD,
|
||||
{
|
||||
ATTR_TASK: "5e2ea1df-f6e6-4ba3-bccb-97c5ec63e99b",
|
||||
},
|
||||
"5e2ea1df-f6e6-4ba3-bccb-97c5ec63e99b",
|
||||
),
|
||||
(
|
||||
SERVICE_SCORE_HABIT,
|
||||
{
|
||||
ATTR_TASK: "Füge eine Aufgabe zu Habitica hinzu",
|
||||
ATTR_DIRECTION: "up",
|
||||
},
|
||||
"e97659e0-2c42-4599-a7bb-00282adc410d",
|
||||
),
|
||||
(
|
||||
SERVICE_SCORE_HABIT,
|
||||
{
|
||||
ATTR_TASK: "create_a_task",
|
||||
ATTR_DIRECTION: "up",
|
||||
},
|
||||
"e97659e0-2c42-4599-a7bb-00282adc410d",
|
||||
),
|
||||
],
|
||||
ids=[
|
||||
"habit score up",
|
||||
"habit score down",
|
||||
"buy reward",
|
||||
"match task by name",
|
||||
"match task by alias",
|
||||
],
|
||||
)
|
||||
async def test_score_task(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_habitica: AiohttpClientMocker,
|
||||
service: str,
|
||||
service_data: dict[str, Any],
|
||||
task_id: str,
|
||||
) -> None:
|
||||
"""Test Habitica score task action."""
|
||||
|
||||
mock_habitica.post(
|
||||
f"{DEFAULT_URL}/api/v3/tasks/{task_id}/score/{service_data.get(ATTR_DIRECTION, "up")}",
|
||||
json={"success": True, "data": {}},
|
||||
)
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
service,
|
||||
service_data={
|
||||
ATTR_CONFIG_ENTRY: config_entry.entry_id,
|
||||
**service_data,
|
||||
},
|
||||
return_response=True,
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_called_with(
|
||||
mock_habitica,
|
||||
"post",
|
||||
f"{DEFAULT_URL}/api/v3/tasks/{task_id}/score/{service_data.get(ATTR_DIRECTION, "up")}",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"service_data",
|
||||
"http_status",
|
||||
"expected_exception",
|
||||
"expected_exception_msg",
|
||||
),
|
||||
[
|
||||
(
|
||||
{
|
||||
ATTR_TASK: "task does not exist",
|
||||
ATTR_DIRECTION: "up",
|
||||
},
|
||||
HTTPStatus.OK,
|
||||
ServiceValidationError,
|
||||
"Unable to complete action, could not find the task 'task does not exist'",
|
||||
),
|
||||
(
|
||||
{
|
||||
ATTR_TASK: "e97659e0-2c42-4599-a7bb-00282adc410d",
|
||||
ATTR_DIRECTION: "up",
|
||||
},
|
||||
HTTPStatus.TOO_MANY_REQUESTS,
|
||||
ServiceValidationError,
|
||||
RATE_LIMIT_EXCEPTION_MSG,
|
||||
),
|
||||
(
|
||||
{
|
||||
ATTR_TASK: "e97659e0-2c42-4599-a7bb-00282adc410d",
|
||||
ATTR_DIRECTION: "up",
|
||||
},
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
HomeAssistantError,
|
||||
REQUEST_EXCEPTION_MSG,
|
||||
),
|
||||
(
|
||||
{
|
||||
ATTR_TASK: "5e2ea1df-f6e6-4ba3-bccb-97c5ec63e99b",
|
||||
ATTR_DIRECTION: "up",
|
||||
},
|
||||
HTTPStatus.UNAUTHORIZED,
|
||||
HomeAssistantError,
|
||||
"Unable to buy reward, not enough gold. Your character has 137.63 GP, but the reward costs 10 GP",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_habitica")
|
||||
async def test_score_task_exceptions(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_habitica: AiohttpClientMocker,
|
||||
service_data: dict[str, Any],
|
||||
http_status: HTTPStatus,
|
||||
expected_exception: Exception,
|
||||
expected_exception_msg: str,
|
||||
) -> None:
|
||||
"""Test Habitica score task action exceptions."""
|
||||
|
||||
mock_habitica.post(
|
||||
f"{DEFAULT_URL}/api/v3/tasks/e97659e0-2c42-4599-a7bb-00282adc410d/score/up",
|
||||
json={"success": True, "data": {}},
|
||||
status=http_status,
|
||||
)
|
||||
mock_habitica.post(
|
||||
f"{DEFAULT_URL}/api/v3/tasks/5e2ea1df-f6e6-4ba3-bccb-97c5ec63e99b/score/up",
|
||||
json={"success": True, "data": {}},
|
||||
status=http_status,
|
||||
)
|
||||
|
||||
with pytest.raises(expected_exception, match=expected_exception_msg):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SCORE_HABIT,
|
||||
service_data={
|
||||
ATTR_CONFIG_ENTRY: config_entry.entry_id,
|
||||
**service_data,
|
||||
},
|
||||
return_response=True,
|
||||
blocking=True,
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user