Add get_tasks action to Habitica integration (#127687)

Add get_tasks action
This commit is contained in:
Manu 2025-01-03 11:53:30 +01:00 committed by GitHub
parent add401ffcf
commit 5726d090b0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 6545 additions and 8 deletions

View File

@ -31,6 +31,11 @@ ATTR_TASK = "task"
ATTR_DIRECTION = "direction"
ATTR_TARGET = "target"
ATTR_ITEM = "item"
ATTR_TYPE = "type"
ATTR_PRIORITY = "priority"
ATTR_TAG = "tag"
ATTR_KEYWORD = "keyword"
SERVICE_CAST_SKILL = "cast_skill"
SERVICE_START_QUEST = "start_quest"
SERVICE_ACCEPT_QUEST = "accept_quest"
@ -38,6 +43,8 @@ SERVICE_CANCEL_QUEST = "cancel_quest"
SERVICE_ABORT_QUEST = "abort_quest"
SERVICE_REJECT_QUEST = "reject_quest"
SERVICE_LEAVE_QUEST = "leave_quest"
SERVICE_GET_TASKS = "get_tasks"
SERVICE_SCORE_HABIT = "score_habit"
SERVICE_SCORE_REWARD = "score_reward"

View File

@ -196,6 +196,12 @@
},
"transformation": {
"service": "mdi:flask-round-bottom"
},
"get_tasks": {
"service": "mdi:calendar-export",
"sections": {
"filter": "mdi:calendar-filter"
}
}
}
}

View File

@ -4,7 +4,7 @@ from __future__ import annotations
from dataclasses import asdict
import logging
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any
from aiohttp import ClientError
from habiticalib import (
@ -13,6 +13,9 @@ from habiticalib import (
NotAuthorizedError,
NotFoundError,
Skill,
TaskData,
TaskPriority,
TaskType,
TooManyRequestsError,
)
import voluptuous as vol
@ -36,10 +39,14 @@ from .const import (
ATTR_DATA,
ATTR_DIRECTION,
ATTR_ITEM,
ATTR_KEYWORD,
ATTR_PATH,
ATTR_PRIORITY,
ATTR_SKILL,
ATTR_TAG,
ATTR_TARGET,
ATTR_TASK,
ATTR_TYPE,
DOMAIN,
EVENT_API_CALL_SUCCESS,
SERVICE_ABORT_QUEST,
@ -47,6 +54,7 @@ from .const import (
SERVICE_API_CALL,
SERVICE_CANCEL_QUEST,
SERVICE_CAST_SKILL,
SERVICE_GET_TASKS,
SERVICE_LEAVE_QUEST,
SERVICE_REJECT_QUEST,
SERVICE_SCORE_HABIT,
@ -96,6 +104,21 @@ SERVICE_TRANSFORMATION_SCHEMA = vol.Schema(
}
)
SERVICE_GET_TASKS_SCHEMA = vol.Schema(
{
vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector(),
vol.Optional(ATTR_TYPE): vol.All(
cv.ensure_list, [vol.All(vol.Upper, vol.In({x.name for x in TaskType}))]
),
vol.Optional(ATTR_PRIORITY): vol.All(
cv.ensure_list, [vol.All(vol.Upper, vol.In({x.name for x in TaskPriority}))]
),
vol.Optional(ATTR_TASK): vol.All(cv.ensure_list, [str]),
vol.Optional(ATTR_TAG): vol.All(cv.ensure_list, [str]),
vol.Optional(ATTR_KEYWORD): cv.string,
}
)
SKILL_MAP = {
"pickpocket": Skill.PICKPOCKET,
"backstab": Skill.BACKSTAB,
@ -403,6 +426,52 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
else:
return asdict(response.data)
async def get_tasks(call: ServiceCall) -> ServiceResponse:
"""Get tasks action."""
entry = get_config_entry(hass, call.data[ATTR_CONFIG_ENTRY])
coordinator = entry.runtime_data
response: list[TaskData] = coordinator.data.tasks
if types := {TaskType[x] for x in call.data.get(ATTR_TYPE, [])}:
response = [task for task in response if task.Type in types]
if priority := {TaskPriority[x] for x in call.data.get(ATTR_PRIORITY, [])}:
response = [task for task in response if task.priority in priority]
if tasks := call.data.get(ATTR_TASK):
response = [
task
for task in response
if str(task.id) in tasks or task.alias in tasks or task.text in tasks
]
if tags := call.data.get(ATTR_TAG):
tag_ids = {
tag.id
for tag in coordinator.data.user.tags
if (tag.name and tag.name.lower())
in (tag.lower() for tag in tags) # Case-insensitive matching
and tag.id
}
response = [
task
for task in response
if any(tag_id in task.tags for tag_id in tag_ids if task.tags)
]
if keyword := call.data.get(ATTR_KEYWORD):
keyword = keyword.lower()
response = [
task
for task in response
if (task.text and keyword in task.text.lower())
or (task.notes and keyword in task.notes.lower())
or any(keyword in item.text.lower() for item in task.checklist)
]
result: dict[str, Any] = {"tasks": response}
return result
hass.services.async_register(
DOMAIN,
SERVICE_API_CALL,
@ -440,3 +509,10 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
schema=SERVICE_TRANSFORMATION_SCHEMA,
supports_response=SupportsResponse.ONLY,
)
hass.services.async_register(
DOMAIN,
SERVICE_GET_TASKS,
get_tasks,
schema=SERVICE_GET_TASKS_SCHEMA,
supports_response=SupportsResponse.ONLY,
)

View File

@ -94,3 +94,49 @@ transformation:
required: true
selector:
text:
get_tasks:
fields:
config_entry: *config_entry
filter:
collapsed: true
fields:
type:
required: false
selector:
select:
options:
- "habit"
- "daily"
- "todo"
- "reward"
mode: dropdown
translation_key: "type"
multiple: true
sort: true
priority:
required: false
selector:
select:
options:
- "trivial"
- "easy"
- "medium"
- "hard"
mode: dropdown
translation_key: "priority"
multiple: true
sort: false
task:
required: false
selector:
text:
multiple: true
tag:
required: false
selector:
text:
multiple: true
keyword:
required: false
selector:
text:

View File

@ -3,6 +3,7 @@
"todos": "To-Do's",
"dailies": "Dailies",
"config_entry_name": "Select character",
"task_name": "Task name",
"unit_tasks": "tasks",
"unit_health_points": "HP",
"unit_mana_points": "MP",
@ -444,7 +445,7 @@
"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",
"name": "[%key:component::habitica::common::task_name%]",
"description": "The name (or task ID) of the task you want to target with the skill or spell."
}
}
@ -558,6 +559,42 @@
"description": "The name of the character you want to use the transformation item on. You can also specify the players username or user ID."
}
}
},
"get_tasks": {
"name": "Get tasks",
"description": "Retrieve tasks from your Habitica character.",
"fields": {
"config_entry": {
"name": "[%key:component::habitica::common::config_entry_name%]",
"description": "Choose the Habitica character to retrieve tasks from."
},
"type": {
"name": "Task type",
"description": "Filter tasks by type."
},
"priority": {
"name": "Difficulty",
"description": "Filter tasks by difficulty."
},
"task": {
"name": "[%key:component::habitica::common::task_name%]",
"description": "Select tasks by matching their name (or task ID)."
},
"tag": {
"name": "Tag",
"description": "Filter tasks that have one or more of the selected tags."
},
"keyword": {
"name": "Keyword",
"description": "Filter tasks by keyword, searching across titles, notes, and checklists."
}
},
"sections": {
"filter": {
"name": "Filter options",
"description": "Use the optional filters to narrow the returned tasks."
}
}
}
},
"selector": {
@ -576,6 +613,22 @@
"seafoam": "Seafoam",
"shiny_seed": "Shiny seed"
}
},
"type": {
"options": {
"daily": "Daily",
"habit": "Habit",
"todo": "To-do",
"reward": "Reward"
}
},
"priority": {
"options": {
"trivial": "Trivial",
"easy": "Easy",
"medium": "Medium",
"hard": "Hard"
}
}
}
}

View File

@ -233,13 +233,20 @@
"startDate": "2024-07-06T22:00:00.000Z",
"daysOfMonth": [],
"weeksOfMonth": [],
"checklist": [],
"checklist": [
{
"completed": false,
"id": "c8662c16-8cd3-4104-a3b2-b1e54f61b8ca",
"text": "Checklist-item1"
}
],
"reminders": [],
"createdAt": "2024-07-07T17:51:53.268Z",
"updatedAt": "2024-09-21T22:24:20.154Z",
"userId": "5f359083-ef78-4af0-985a-0b2c6d05797c",
"isDue": true,
"id": "564b9ac9-c53d-4638-9e7f-1cd96fe19baa"
"id": "564b9ac9-c53d-4638-9e7f-1cd96fe19baa",
"alias": "alias_zahnseide_benutzen"
},
{
"_id": "f2c85972-1a19-4426-bc6d-ce3337b9d99f",
@ -386,11 +393,17 @@
"history": [],
"completed": false,
"collapseChecklist": false,
"checklist": [],
"checklist": [
{
"completed": true,
"id": "c8662c16-8cd3-4104-a3b2-b1e54f61b8ca",
"text": "Checklist-item1"
}
],
"type": "daily",
"text": "Fitnessstudio besuchen",
"notes": "Ein einstündiges Workout im Fitnessstudio absolvieren.",
"tags": ["51076966-2970-4b40-b6ba-d58c6a756dd7"],
"tags": ["6aa65cbb-dc08-4fdd-9a66-7dedb7ba4cab"],
"value": 0,
"priority": 2,
"attribute": "str",
@ -416,7 +429,10 @@
"type": "todo",
"text": "Buch zu Ende lesen",
"notes": "Das Buch, das du angefangen hast, bis zum Wochenende fertig lesen.",
"tags": [],
"tags": [
"20409521-c096-447f-9a90-23e8da615710",
"8515e4ae-2f4b-455a-b4a4-8939e04b1bfd"
],
"value": 0,
"priority": 1,
"attribute": "str",

View File

@ -67,6 +67,36 @@
},
"_id": "94cd398c-2240-4320-956e-6d345cf2c0de"
},
"tags": [
{
"id": "8515e4ae-2f4b-455a-b4a4-8939e04b1bfd",
"name": "Arbeit"
},
{
"id": "6aa65cbb-dc08-4fdd-9a66-7dedb7ba4cab",
"name": "Training"
},
{
"id": "20409521-c096-447f-9a90-23e8da615710",
"name": "Gesundheit + Wohlbefinden"
},
{
"id": "2ac458af-0833-4f3f-bf04-98a0c33ef60b",
"name": "Schule"
},
{
"id": "1bcb1a0f-4d05-4087-8223-5ea779e258b0",
"name": "Teams"
},
{
"id": "b2780f82-b3b5-49a3-a677-48f2c8c7e3bb",
"name": "Hausarbeiten"
},
{
"id": "3450351f-1323-4c7e-9fd2-0cdff25b3ce0",
"name": "Kreativität"
}
],
"needsCron": true,
"lastCron": "2024-09-21T22:01:55.586Z",
"id": "a380546a-94be-4b8e-8a0b-23e0d5c03303",

View File

@ -198,7 +198,7 @@
}),
'start_date': '2024-09-21T22:00:00+00:00',
'tags': list([
'51076966-2970-4b40-b6ba-d58c6a756dd7',
'6aa65cbb-dc08-4fdd-9a66-7dedb7ba4cab',
]),
'text': 'Fitnessstudio besuchen',
'type': 'daily',
@ -1553,6 +1553,10 @@
'th': False,
'w': True,
}),
'tags': list([
'20409521-c096-447f-9a90-23e8da615710',
'8515e4ae-2f4b-455a-b4a4-8939e04b1bfd',
]),
'text': 'Buch zu Ende lesen',
'type': 'todo',
}),

File diff suppressed because it is too large Load Diff

View File

@ -7,19 +7,25 @@ from uuid import UUID
from habiticalib import Direction, Skill
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.habitica.const import (
ATTR_CONFIG_ENTRY,
ATTR_DIRECTION,
ATTR_ITEM,
ATTR_KEYWORD,
ATTR_PRIORITY,
ATTR_SKILL,
ATTR_TAG,
ATTR_TARGET,
ATTR_TASK,
ATTR_TYPE,
DOMAIN,
SERVICE_ABORT_QUEST,
SERVICE_ACCEPT_QUEST,
SERVICE_CANCEL_QUEST,
SERVICE_CAST_SKILL,
SERVICE_GET_TASKS,
SERVICE_LEAVE_QUEST,
SERVICE_REJECT_QUEST,
SERVICE_SCORE_HABIT,
@ -775,3 +781,67 @@ async def test_transformation_exceptions(
return_response=True,
blocking=True,
)
@pytest.mark.parametrize(
("service_data"),
[
{},
{ATTR_TYPE: ["daily"]},
{ATTR_TYPE: ["habit"]},
{ATTR_TYPE: ["todo"]},
{ATTR_TYPE: ["reward"]},
{ATTR_TYPE: ["daily", "habit"]},
{ATTR_TYPE: ["todo", "reward"]},
{ATTR_PRIORITY: "trivial"},
{ATTR_PRIORITY: "easy"},
{ATTR_PRIORITY: "medium"},
{ATTR_PRIORITY: "hard"},
{ATTR_TASK: ["Zahnseide benutzen", "Eine kurze Pause machen"]},
{ATTR_TASK: ["f2c85972-1a19-4426-bc6d-ce3337b9d99f"]},
{ATTR_TASK: ["alias_zahnseide_benutzen"]},
{ATTR_TAG: ["Training", "Gesundheit + Wohlbefinden"]},
{ATTR_KEYWORD: "gewohnheit"},
{ATTR_TAG: ["Home Assistant"]},
],
ids=[
"all_tasks",
"only dailies",
"only habits",
"only todos",
"only rewards",
"only dailies and habits",
"only todos and rewards",
"trivial tasks",
"easy tasks",
"medium tasks",
"hard tasks",
"by task name",
"by task ID",
"by alias",
"by tag",
"by keyword",
"empty result",
],
)
@pytest.mark.usefixtures("habitica")
async def test_get_tasks(
hass: HomeAssistant,
config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
service_data: dict[str, Any],
) -> None:
"""Test Habitica get_tasks action."""
response = await hass.services.async_call(
DOMAIN,
SERVICE_GET_TASKS,
service_data={
ATTR_CONFIG_ENTRY: config_entry.entry_id,
**service_data,
},
return_response=True,
blocking=True,
)
assert response == snapshot