mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 04:07:08 +00:00
Add create_reward action to Habitica integration (#139304)
Add create_reward action to Habitica
This commit is contained in:
parent
2cce1b024e
commit
3588784f1e
@ -56,6 +56,7 @@ SERVICE_SCORE_REWARD = "score_reward"
|
||||
SERVICE_TRANSFORMATION = "transformation"
|
||||
|
||||
SERVICE_UPDATE_REWARD = "update_reward"
|
||||
SERVICE_CREATE_REWARD = "create_reward"
|
||||
|
||||
DEVELOPER_ID = "4c4ca53f-c059-4ffa-966e-9d29dd405daf"
|
||||
X_CLIENT = f"{DEVELOPER_ID} - {APPLICATION_NAME} {__version__}"
|
||||
|
@ -224,6 +224,12 @@
|
||||
"tag_options": "mdi:tag",
|
||||
"developer_options": "mdi:test-tube"
|
||||
}
|
||||
},
|
||||
"create_reward": {
|
||||
"service": "mdi:treasure-chest-outline",
|
||||
"sections": {
|
||||
"developer_options": "mdi:test-tube"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -61,6 +61,7 @@ from .const import (
|
||||
SERVICE_API_CALL,
|
||||
SERVICE_CANCEL_QUEST,
|
||||
SERVICE_CAST_SKILL,
|
||||
SERVICE_CREATE_REWARD,
|
||||
SERVICE_GET_TASKS,
|
||||
SERVICE_LEAVE_QUEST,
|
||||
SERVICE_REJECT_QUEST,
|
||||
@ -112,18 +113,29 @@ SERVICE_TRANSFORMATION_SCHEMA = vol.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
SERVICE_UPDATE_TASK_SCHEMA = vol.Schema(
|
||||
BASE_TASK_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector(),
|
||||
vol.Required(ATTR_TASK): cv.string,
|
||||
vol.Optional(ATTR_RENAME): cv.string,
|
||||
vol.Optional(ATTR_NOTES): cv.string,
|
||||
vol.Optional(ATTR_TAG): vol.All(cv.ensure_list, [str]),
|
||||
vol.Optional(ATTR_REMOVE_TAG): vol.All(cv.ensure_list, [str]),
|
||||
vol.Optional(ATTR_ALIAS): vol.All(
|
||||
cv.string, cv.matches_regex("^[a-zA-Z0-9-_]*$")
|
||||
),
|
||||
vol.Optional(ATTR_COST): vol.Coerce(float),
|
||||
vol.Optional(ATTR_COST): vol.All(vol.Coerce(float), vol.Range(0)),
|
||||
}
|
||||
)
|
||||
|
||||
SERVICE_UPDATE_TASK_SCHEMA = BASE_TASK_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(ATTR_TASK): cv.string,
|
||||
vol.Optional(ATTR_REMOVE_TAG): vol.All(cv.ensure_list, [str]),
|
||||
}
|
||||
)
|
||||
|
||||
SERVICE_CREATE_TASK_SCHEMA = BASE_TASK_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(ATTR_NAME): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
@ -539,33 +551,36 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
|
||||
|
||||
return result
|
||||
|
||||
async def update_task(call: ServiceCall) -> ServiceResponse:
|
||||
"""Update task action."""
|
||||
async def create_or_update_task(call: ServiceCall) -> ServiceResponse:
|
||||
"""Create or update task action."""
|
||||
entry = get_config_entry(hass, call.data[ATTR_CONFIG_ENTRY])
|
||||
coordinator = entry.runtime_data
|
||||
await coordinator.async_refresh()
|
||||
is_update = call.service == SERVICE_UPDATE_REWARD
|
||||
current_task = None
|
||||
|
||||
try:
|
||||
current_task = next(
|
||||
task
|
||||
for task in coordinator.data.tasks
|
||||
if call.data[ATTR_TASK] in (str(task.id), task.alias, task.text)
|
||||
and task.Type is TaskType.REWARD
|
||||
)
|
||||
except StopIteration as e:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="task_not_found",
|
||||
translation_placeholders={"task": f"'{call.data[ATTR_TASK]}'"},
|
||||
) from e
|
||||
if is_update:
|
||||
try:
|
||||
current_task = next(
|
||||
task
|
||||
for task in coordinator.data.tasks
|
||||
if call.data[ATTR_TASK] in (str(task.id), task.alias, task.text)
|
||||
and task.Type is TaskType.REWARD
|
||||
)
|
||||
except StopIteration as e:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="task_not_found",
|
||||
translation_placeholders={"task": f"'{call.data[ATTR_TASK]}'"},
|
||||
) from e
|
||||
|
||||
task_id = current_task.id
|
||||
if TYPE_CHECKING:
|
||||
assert task_id
|
||||
data = Task()
|
||||
|
||||
if rename := call.data.get(ATTR_RENAME):
|
||||
data["text"] = rename
|
||||
if not is_update:
|
||||
data["type"] = TaskType.REWARD
|
||||
|
||||
if (text := call.data.get(ATTR_RENAME)) or (text := call.data.get(ATTR_NAME)):
|
||||
data["text"] = text
|
||||
|
||||
if (notes := call.data.get(ATTR_NOTES)) is not None:
|
||||
data["notes"] = notes
|
||||
@ -574,7 +589,7 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
|
||||
remove_tags = cast(list[str], call.data.get(ATTR_REMOVE_TAG))
|
||||
|
||||
if tags or remove_tags:
|
||||
update_tags = set(current_task.tags)
|
||||
update_tags = set(current_task.tags) if current_task else set()
|
||||
user_tags = {
|
||||
tag.name.lower(): tag.id
|
||||
for tag in coordinator.data.user.tags
|
||||
@ -634,7 +649,13 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
|
||||
data["value"] = cost
|
||||
|
||||
try:
|
||||
response = await coordinator.habitica.update_task(task_id, data)
|
||||
if is_update:
|
||||
if TYPE_CHECKING:
|
||||
assert current_task
|
||||
assert current_task.id
|
||||
response = await coordinator.habitica.update_task(current_task.id, data)
|
||||
else:
|
||||
response = await coordinator.habitica.create_task(data)
|
||||
except TooManyRequestsError as e:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
@ -659,10 +680,17 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_UPDATE_REWARD,
|
||||
update_task,
|
||||
create_or_update_task,
|
||||
schema=SERVICE_UPDATE_TASK_SCHEMA,
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_CREATE_REWARD,
|
||||
create_or_update_task,
|
||||
schema=SERVICE_CREATE_TASK_SCHEMA,
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_API_CALL,
|
||||
|
@ -147,14 +147,14 @@ update_reward:
|
||||
rename:
|
||||
selector:
|
||||
text:
|
||||
notes:
|
||||
notes: ¬es
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
multiline: true
|
||||
cost:
|
||||
required: false
|
||||
selector:
|
||||
selector: &cost_selector
|
||||
number:
|
||||
min: 0
|
||||
step: 0.01
|
||||
@ -163,7 +163,7 @@ update_reward:
|
||||
tag_options:
|
||||
collapsed: true
|
||||
fields:
|
||||
tag:
|
||||
tag: &tag
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
@ -173,10 +173,23 @@ update_reward:
|
||||
selector:
|
||||
text:
|
||||
multiple: true
|
||||
developer_options:
|
||||
developer_options: &developer_options
|
||||
collapsed: true
|
||||
fields:
|
||||
alias:
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
create_reward:
|
||||
fields:
|
||||
config_entry: *config_entry
|
||||
name:
|
||||
required: true
|
||||
selector:
|
||||
text:
|
||||
notes: *notes
|
||||
cost:
|
||||
required: true
|
||||
selector: *cost_selector
|
||||
tag: *tag
|
||||
developer_options: *developer_options
|
||||
|
@ -23,7 +23,9 @@
|
||||
"developer_options_name": "Advanced settings",
|
||||
"developer_options_description": "Additional features available in developer mode.",
|
||||
"tag_options_name": "Tags",
|
||||
"tag_options_description": "Add or remove tags from a task."
|
||||
"tag_options_description": "Add or remove tags from a task.",
|
||||
"name_description": "The title for the Habitica task.",
|
||||
"cost_name": "Cost"
|
||||
},
|
||||
"config": {
|
||||
"abort": {
|
||||
@ -707,7 +709,7 @@
|
||||
"description": "[%key:component::habitica::common::alias_description%]"
|
||||
},
|
||||
"cost": {
|
||||
"name": "Cost",
|
||||
"name": "[%key:component::habitica::common::cost_name%]",
|
||||
"description": "Update the cost of a reward."
|
||||
}
|
||||
},
|
||||
@ -721,6 +723,42 @@
|
||||
"description": "[%key:component::habitica::common::developer_options_description%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"create_reward": {
|
||||
"name": "Create reward",
|
||||
"description": "Adds a new custom reward.",
|
||||
"fields": {
|
||||
"config_entry": {
|
||||
"name": "[%key:component::habitica::common::config_entry_name%]",
|
||||
"description": "Select the Habitica account to create a reward."
|
||||
},
|
||||
"name": {
|
||||
"name": "[%key:component::habitica::common::task_name%]",
|
||||
"description": "[%key:component::habitica::common::name_description%]"
|
||||
},
|
||||
"notes": {
|
||||
"name": "[%key:component::habitica::common::notes_name%]",
|
||||
"description": "[%key:component::habitica::common::notes_description%]"
|
||||
},
|
||||
"tag": {
|
||||
"name": "[%key:component::habitica::common::tag_name%]",
|
||||
"description": "[%key:component::habitica::common::tag_description%]"
|
||||
},
|
||||
"alias": {
|
||||
"name": "[%key:component::habitica::common::alias_name%]",
|
||||
"description": "[%key:component::habitica::common::alias_description%]"
|
||||
},
|
||||
"cost": {
|
||||
"name": "[%key:component::habitica::common::cost_name%]",
|
||||
"description": "The cost of the reward."
|
||||
}
|
||||
},
|
||||
"sections": {
|
||||
"developer_options": {
|
||||
"name": "[%key:component::habitica::common::developer_options_name%]",
|
||||
"description": "[%key:component::habitica::common::developer_options_description%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
|
@ -151,6 +151,9 @@ async def mock_habiticalib() -> Generator[AsyncMock]:
|
||||
client.create_tag.return_value = HabiticaTagResponse.from_json(
|
||||
load_fixture("create_tag.json", DOMAIN)
|
||||
)
|
||||
client.create_task.return_value = HabiticaTaskResponse.from_json(
|
||||
load_fixture("task.json", DOMAIN)
|
||||
)
|
||||
client.habitipy.return_value = {
|
||||
"tasks": {
|
||||
"user": {
|
||||
|
@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, patch
|
||||
from uuid import UUID
|
||||
|
||||
from aiohttp import ClientError
|
||||
from habiticalib import Direction, HabiticaTaskResponse, Skill, Task
|
||||
from habiticalib import Direction, HabiticaTaskResponse, Skill, Task, TaskType
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
@ -30,6 +30,7 @@ from homeassistant.components.habitica.const import (
|
||||
SERVICE_ACCEPT_QUEST,
|
||||
SERVICE_CANCEL_QUEST,
|
||||
SERVICE_CAST_SKILL,
|
||||
SERVICE_CREATE_REWARD,
|
||||
SERVICE_GET_TASKS,
|
||||
SERVICE_LEAVE_QUEST,
|
||||
SERVICE_REJECT_QUEST,
|
||||
@ -41,6 +42,7 @@ from homeassistant.components.habitica.const import (
|
||||
)
|
||||
from homeassistant.components.todo import ATTR_RENAME
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import ATTR_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
|
||||
@ -943,6 +945,51 @@ async def test_update_task_exceptions(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "expected_exception", "exception_msg"),
|
||||
[
|
||||
(
|
||||
ERROR_TOO_MANY_REQUESTS,
|
||||
HomeAssistantError,
|
||||
RATE_LIMIT_EXCEPTION_MSG,
|
||||
),
|
||||
(
|
||||
ERROR_BAD_REQUEST,
|
||||
HomeAssistantError,
|
||||
REQUEST_EXCEPTION_MSG,
|
||||
),
|
||||
(
|
||||
ClientError,
|
||||
HomeAssistantError,
|
||||
"Unable to connect to Habitica: ",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("habitica")
|
||||
async def test_create_task_exceptions(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
habitica: AsyncMock,
|
||||
exception: Exception,
|
||||
expected_exception: Exception,
|
||||
exception_msg: str,
|
||||
) -> None:
|
||||
"""Test Habitica task create action exceptions."""
|
||||
|
||||
habitica.create_task.side_effect = exception
|
||||
with pytest.raises(expected_exception, match=exception_msg):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_CREATE_REWARD,
|
||||
service_data={
|
||||
ATTR_CONFIG_ENTRY: config_entry.entry_id,
|
||||
ATTR_NAME: "TITLE",
|
||||
},
|
||||
return_response=True,
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("habitica")
|
||||
async def test_task_not_found(
|
||||
hass: HomeAssistant,
|
||||
@ -1024,6 +1071,60 @@ async def test_update_reward(
|
||||
habitica.update_task.assert_awaited_with(UUID(task_id), call_args)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service_data", "call_args"),
|
||||
[
|
||||
(
|
||||
{
|
||||
ATTR_NAME: "TITLE",
|
||||
ATTR_COST: 100,
|
||||
},
|
||||
Task(type=TaskType.REWARD, text="TITLE", value=100),
|
||||
),
|
||||
(
|
||||
{
|
||||
ATTR_NAME: "TITLE",
|
||||
},
|
||||
Task(type=TaskType.REWARD, text="TITLE"),
|
||||
),
|
||||
(
|
||||
{
|
||||
ATTR_NAME: "TITLE",
|
||||
ATTR_NOTES: "NOTES",
|
||||
},
|
||||
Task(type=TaskType.REWARD, text="TITLE", notes="NOTES"),
|
||||
),
|
||||
(
|
||||
{
|
||||
ATTR_NAME: "TITLE",
|
||||
ATTR_ALIAS: "ALIAS",
|
||||
},
|
||||
Task(type=TaskType.REWARD, text="TITLE", alias="ALIAS"),
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_create_reward(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
habitica: AsyncMock,
|
||||
service_data: dict[str, Any],
|
||||
call_args: Task,
|
||||
) -> None:
|
||||
"""Test Habitica create_reward action."""
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_CREATE_REWARD,
|
||||
service_data={
|
||||
ATTR_CONFIG_ENTRY: config_entry.entry_id,
|
||||
**service_data,
|
||||
},
|
||||
return_response=True,
|
||||
blocking=True,
|
||||
)
|
||||
habitica.create_task.assert_awaited_with(call_args)
|
||||
|
||||
|
||||
async def test_tags(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
|
Loading…
x
Reference in New Issue
Block a user