From 3ccb7d80f3d4e636e023fc808ecccb39d45a12f6 Mon Sep 17 00:00:00 2001
From: Manu <4445816+tr4nt0r@users.noreply.github.com>
Date: Fri, 7 Mar 2025 19:40:17 +0100
Subject: [PATCH] Add `update_todo` action to Habitica (#139799)

* update_todo action

* fix strings
---
 homeassistant/components/habitica/const.py    |   9 +
 homeassistant/components/habitica/icons.json  |  10 +
 homeassistant/components/habitica/services.py | 117 +++++++++--
 .../components/habitica/services.yaml         |  66 ++++++
 .../components/habitica/strings.json          | 130 +++++++++++-
 tests/components/habitica/conftest.py         |  13 +-
 tests/components/habitica/fixtures/tasks.json |  13 +-
 .../habitica/snapshots/test_services.ambr     |  50 +++++
 tests/components/habitica/test_services.py    | 193 +++++++++++++++++-
 9 files changed, 573 insertions(+), 28 deletions(-)

diff --git a/homeassistant/components/habitica/const.py b/homeassistant/components/habitica/const.py
index 049f2beb370..c33edc0161d 100644
--- a/homeassistant/components/habitica/const.py
+++ b/homeassistant/components/habitica/const.py
@@ -44,6 +44,14 @@ ATTR_UP_DOWN = "up_down"
 ATTR_FREQUENCY = "frequency"
 ATTR_COUNTER_UP = "counter_up"
 ATTR_COUNTER_DOWN = "counter_down"
+ATTR_ADD_CHECKLIST_ITEM = "add_checklist_item"
+ATTR_REMOVE_CHECKLIST_ITEM = "remove_checklist_item"
+ATTR_SCORE_CHECKLIST_ITEM = "score_checklist_item"
+ATTR_UNSCORE_CHECKLIST_ITEM = "unscore_checklist_item"
+ATTR_REMINDER = "reminder"
+ATTR_REMOVE_REMINDER = "remove_reminder"
+ATTR_CLEAR_REMINDER = "clear_reminder"
+ATTR_CLEAR_DATE = "clear_date"
 
 SERVICE_CAST_SKILL = "cast_skill"
 SERVICE_START_QUEST = "start_quest"
@@ -63,6 +71,7 @@ SERVICE_UPDATE_REWARD = "update_reward"
 SERVICE_CREATE_REWARD = "create_reward"
 SERVICE_UPDATE_HABIT = "update_habit"
 SERVICE_CREATE_HABIT = "create_habit"
+SERVICE_UPDATE_TODO = "update_todo"
 
 DEVELOPER_ID = "4c4ca53f-c059-4ffa-966e-9d29dd405daf"
 X_CLIENT = f"{DEVELOPER_ID} - {APPLICATION_NAME} {__version__}"
diff --git a/homeassistant/components/habitica/icons.json b/homeassistant/components/habitica/icons.json
index af4a20acab6..f4f045523d4 100644
--- a/homeassistant/components/habitica/icons.json
+++ b/homeassistant/components/habitica/icons.json
@@ -243,6 +243,16 @@
       "sections": {
         "developer_options": "mdi:test-tube"
       }
+    },
+    "update_todo": {
+      "service": "mdi:pencil-box-outline",
+      "sections": {
+        "checklist_options": "mdi:format-list-checks",
+        "tag_options": "mdi:tag",
+        "developer_options": "mdi:test-tube",
+        "duedate_options": "mdi:calendar-blank",
+        "reminder_options": "mdi:reminder"
+      }
     }
   }
 }
diff --git a/homeassistant/components/habitica/services.py b/homeassistant/components/habitica/services.py
index 78f3002c89d..f1e92d863ca 100644
--- a/homeassistant/components/habitica/services.py
+++ b/homeassistant/components/habitica/services.py
@@ -3,17 +3,20 @@
 from __future__ import annotations
 
 from dataclasses import asdict
+from datetime import datetime, time
 import logging
 from typing import TYPE_CHECKING, Any, cast
-from uuid import UUID
+from uuid import UUID, uuid4
 
 from aiohttp import ClientError
 from habiticalib import (
+    Checklist,
     Direction,
     Frequency,
     HabiticaException,
     NotAuthorizedError,
     NotFoundError,
+    Reminders,
     Skill,
     Task,
     TaskData,
@@ -25,7 +28,7 @@ import voluptuous as vol
 
 from homeassistant.components.todo import ATTR_RENAME
 from homeassistant.config_entries import ConfigEntryState
-from homeassistant.const import ATTR_NAME, CONF_NAME
+from homeassistant.const import ATTR_DATE, ATTR_NAME, CONF_NAME
 from homeassistant.core import (
     HomeAssistant,
     ServiceCall,
@@ -38,8 +41,11 @@ from homeassistant.helpers.issue_registry import IssueSeverity, async_create_iss
 from homeassistant.helpers.selector import ConfigEntrySelector
 
 from .const import (
+    ATTR_ADD_CHECKLIST_ITEM,
     ATTR_ALIAS,
     ATTR_ARGS,
+    ATTR_CLEAR_DATE,
+    ATTR_CLEAR_REMINDER,
     ATTR_CONFIG_ENTRY,
     ATTR_COST,
     ATTR_COUNTER_DOWN,
@@ -52,12 +58,17 @@ from .const import (
     ATTR_NOTES,
     ATTR_PATH,
     ATTR_PRIORITY,
+    ATTR_REMINDER,
+    ATTR_REMOVE_CHECKLIST_ITEM,
+    ATTR_REMOVE_REMINDER,
     ATTR_REMOVE_TAG,
+    ATTR_SCORE_CHECKLIST_ITEM,
     ATTR_SKILL,
     ATTR_TAG,
     ATTR_TARGET,
     ATTR_TASK,
     ATTR_TYPE,
+    ATTR_UNSCORE_CHECKLIST_ITEM,
     ATTR_UP_DOWN,
     DOMAIN,
     EVENT_API_CALL_SUCCESS,
@@ -77,6 +88,7 @@ from .const import (
     SERVICE_TRANSFORMATION,
     SERVICE_UPDATE_HABIT,
     SERVICE_UPDATE_REWARD,
+    SERVICE_UPDATE_TODO,
 )
 from .coordinator import HabiticaConfigEntry
 
@@ -137,6 +149,15 @@ BASE_TASK_SCHEMA = vol.Schema(
         vol.Optional(ATTR_COUNTER_UP): vol.All(int, vol.Range(0)),
         vol.Optional(ATTR_COUNTER_DOWN): vol.All(int, vol.Range(0)),
         vol.Optional(ATTR_FREQUENCY): vol.Coerce(Frequency),
+        vol.Optional(ATTR_DATE): cv.date,
+        vol.Optional(ATTR_CLEAR_DATE): cv.boolean,
+        vol.Optional(ATTR_REMINDER): vol.All(cv.ensure_list, [cv.datetime]),
+        vol.Optional(ATTR_REMOVE_REMINDER): vol.All(cv.ensure_list, [cv.datetime]),
+        vol.Optional(ATTR_CLEAR_REMINDER): cv.boolean,
+        vol.Optional(ATTR_ADD_CHECKLIST_ITEM): vol.All(cv.ensure_list, [str]),
+        vol.Optional(ATTR_REMOVE_CHECKLIST_ITEM): vol.All(cv.ensure_list, [str]),
+        vol.Optional(ATTR_SCORE_CHECKLIST_ITEM): vol.All(cv.ensure_list, [str]),
+        vol.Optional(ATTR_UNSCORE_CHECKLIST_ITEM): vol.All(cv.ensure_list, [str]),
     }
 )
 
@@ -192,6 +213,7 @@ SERVICE_TASK_TYPE_MAP = {
     SERVICE_CREATE_REWARD: TaskType.REWARD,
     SERVICE_UPDATE_HABIT: TaskType.HABIT,
     SERVICE_CREATE_HABIT: TaskType.HABIT,
+    SERVICE_UPDATE_TODO: TaskType.TODO,
 }
 
 
@@ -577,7 +599,11 @@ def async_setup_services(hass: HomeAssistant) -> None:  # noqa: C901
         entry = get_config_entry(hass, call.data[ATTR_CONFIG_ENTRY])
         coordinator = entry.runtime_data
         await coordinator.async_refresh()
-        is_update = call.service in (SERVICE_UPDATE_REWARD, SERVICE_UPDATE_HABIT)
+        is_update = call.service in (
+            SERVICE_UPDATE_HABIT,
+            SERVICE_UPDATE_REWARD,
+            SERVICE_UPDATE_TODO,
+        )
         current_task = None
 
         if is_update:
@@ -685,6 +711,69 @@ def async_setup_services(hass: HomeAssistant) -> None:  # noqa: C901
         if counter_down := call.data.get(ATTR_COUNTER_DOWN):
             data["counterDown"] = counter_down
 
+        if due_date := call.data.get(ATTR_DATE):
+            data["date"] = datetime.combine(due_date, time())
+
+        if call.data.get(ATTR_CLEAR_DATE):
+            data["date"] = None
+
+        checklist = current_task.checklist if current_task else []
+
+        if add_checklist_item := call.data.get(ATTR_ADD_CHECKLIST_ITEM):
+            checklist.extend(
+                Checklist(completed=False, id=uuid4(), text=item)
+                for item in add_checklist_item
+                if not any(i.text == item for i in checklist)
+            )
+        if remove_checklist_item := call.data.get(ATTR_REMOVE_CHECKLIST_ITEM):
+            checklist = [
+                item for item in checklist if item.text not in remove_checklist_item
+            ]
+
+        if score_checklist_item := call.data.get(ATTR_SCORE_CHECKLIST_ITEM):
+            for item in checklist:
+                if item.text in score_checklist_item:
+                    item.completed = True
+
+        if unscore_checklist_item := call.data.get(ATTR_UNSCORE_CHECKLIST_ITEM):
+            for item in checklist:
+                if item.text in unscore_checklist_item:
+                    item.completed = False
+        if (
+            add_checklist_item
+            or remove_checklist_item
+            or score_checklist_item
+            or unscore_checklist_item
+        ):
+            data["checklist"] = checklist
+
+        reminders = current_task.reminders if current_task else []
+
+        if add_reminders := call.data.get(ATTR_REMINDER):
+            existing_reminder_datetimes = {
+                r.time.replace(tzinfo=None) for r in reminders
+            }
+
+            reminders.extend(
+                Reminders(id=uuid4(), time=r)
+                for r in add_reminders
+                if r not in existing_reminder_datetimes
+            )
+
+        if remove_reminder := call.data.get(ATTR_REMOVE_REMINDER):
+            reminders = list(
+                filter(
+                    lambda r: r.time.replace(tzinfo=None) not in remove_reminder,
+                    reminders,
+                )
+            )
+
+        if clear_reminders := call.data.get(ATTR_CLEAR_REMINDER):
+            reminders = []
+
+        if add_reminders or remove_reminder or clear_reminders:
+            data["reminders"] = reminders
+
         try:
             if is_update:
                 if TYPE_CHECKING:
@@ -714,20 +803,14 @@ def async_setup_services(hass: HomeAssistant) -> None:  # noqa: C901
         else:
             return response.data.to_dict(omit_none=True)
 
-    hass.services.async_register(
-        DOMAIN,
-        SERVICE_UPDATE_REWARD,
-        create_or_update_task,
-        schema=SERVICE_UPDATE_TASK_SCHEMA,
-        supports_response=SupportsResponse.ONLY,
-    )
-    hass.services.async_register(
-        DOMAIN,
-        SERVICE_UPDATE_HABIT,
-        create_or_update_task,
-        schema=SERVICE_UPDATE_TASK_SCHEMA,
-        supports_response=SupportsResponse.ONLY,
-    )
+    for service in (SERVICE_UPDATE_TODO, SERVICE_UPDATE_REWARD, SERVICE_UPDATE_HABIT):
+        hass.services.async_register(
+            DOMAIN,
+            service,
+            create_or_update_task,
+            schema=SERVICE_UPDATE_TASK_SCHEMA,
+            supports_response=SupportsResponse.ONLY,
+        )
     hass.services.async_register(
         DOMAIN,
         SERVICE_CREATE_REWARD,
diff --git a/homeassistant/components/habitica/services.yaml b/homeassistant/components/habitica/services.yaml
index ed3ae4516e5..2464b39529b 100644
--- a/homeassistant/components/habitica/services.yaml
+++ b/homeassistant/components/habitica/services.yaml
@@ -262,3 +262,69 @@ create_habit:
     frequency: *frequency
     tag: *tag
     developer_options: *developer_options
+update_todo:
+  fields:
+    config_entry: *config_entry
+    task: *task
+    rename: *rename
+    notes: *notes
+    checklist_options:
+      collapsed: true
+      fields:
+        add_checklist_item:
+          required: false
+          selector:
+            text:
+              multiple: true
+        remove_checklist_item:
+          required: false
+          selector:
+            text:
+              multiple: true
+        score_checklist_item:
+          required: false
+          selector:
+            text:
+              multiple: true
+        unscore_checklist_item:
+          required: false
+          selector:
+            text:
+              multiple: true
+    priority: *priority
+    duedate_options:
+      collapsed: true
+      fields:
+        date:
+          required: false
+          selector:
+            date:
+        clear_date:
+          required: false
+          selector:
+            constant:
+              value: true
+              label: "🗑️"
+    reminder_options:
+      collapsed: true
+      fields:
+        reminder:
+          required: false
+          selector:
+            text:
+              type: datetime-local
+              multiple: true
+        remove_reminder:
+          required: false
+          selector:
+            text:
+              type: datetime-local
+              multiple: true
+        clear_reminder:
+          required: false
+          selector:
+            constant:
+              value: true
+              label: "🗑️"
+    tag_options: *tag_options
+    developer_options: *developer_options
diff --git a/homeassistant/components/habitica/strings.json b/homeassistant/components/habitica/strings.json
index 1f9424eafe1..d77bbd6f2be 100644
--- a/homeassistant/components/habitica/strings.json
+++ b/homeassistant/components/habitica/strings.json
@@ -26,12 +26,30 @@
     "tag_options_description": "Add or remove tags from a task.",
     "name_description": "The title for the Habitica task.",
     "cost_name": "Cost",
-    "difficulty_name": "Difficulty",
-    "difficulty_description": "The difficulty of the task.",
+    "priority_name": "Difficulty",
+    "priority_description": "The difficulty of the task.",
     "frequency_name": "Counter reset",
     "frequency_description": "The frequency at which the habit's counter resets: daily at the start of a new day, weekly after Sunday night, or monthly at the beginning of a new month.",
     "up_down_name": "Rewards or losses",
-    "up_down_description": "Whether the habit is good and rewarding (positive), bad and penalizing (negative), or both."
+    "up_down_description": "Whether the habit is good and rewarding (positive), bad and penalizing (negative), or both.",
+    "add_checklist_item_name": "Add checklist items",
+    "add_checklist_item_description": "The items to add to a task's checklist.",
+    "remove_checklist_item_name": "Delete items",
+    "remove_checklist_item_description": "Remove items from a task's checklist.",
+    "score_checklist_item_name": "Complete items",
+    "score_checklist_item_description": "Mark items from a task's checklist as completed.",
+    "unscore_checklist_item_name": "Uncomplete items",
+    "unscore_checklist_item_description": "Undo completion of items of a task's checklist.",
+    "checklist_options_name": "Checklist",
+    "checklist_options_description": "Add, remove, or update status of an item on a task's checklist.",
+    "reminder_name": "Add reminders",
+    "reminder_description": "Add reminders to a Habitica task.",
+    "remove_reminder_name": "Remove reminders",
+    "remove_reminder_description": "Remove specific reminders from a Habitica task.",
+    "clear_reminder_name": "Clear all reminders",
+    "clear_reminder_description": "Remove all reminders from a Habitica task.",
+    "reminder_options_name": "Reminders",
+    "reminder_options_description": "Add, remove or clear reminders of a Habitica task."
   },
   "config": {
     "abort": {
@@ -659,7 +677,7 @@
           "description": "Filter tasks by type."
         },
         "priority": {
-          "name": "Difficulty",
+          "name": "[%key:component::habitica::common::priority_name%]",
           "description": "Filter tasks by difficulty."
         },
         "task": {
@@ -799,8 +817,8 @@
           "description": "[%key:component::habitica::common::alias_description%]"
         },
         "priority": {
-          "name": "[%key:component::habitica::common::difficulty_name%]",
-          "description": "[%key:component::habitica::common::difficulty_description%]"
+          "name": "[%key:component::habitica::common::priority_name%]",
+          "description": "[%key:component::habitica::common::priority_description%]"
         },
         "frequency": {
           "name": "[%key:component::habitica::common::frequency_name%]",
@@ -855,8 +873,8 @@
           "description": "[%key:component::habitica::common::alias_description%]"
         },
         "priority": {
-          "name": "[%key:component::habitica::common::difficulty_name%]",
-          "description": "[%key:component::habitica::common::difficulty_description%]"
+          "name": "[%key:component::habitica::common::priority_name%]",
+          "description": "[%key:component::habitica::common::priority_description%]"
         },
         "frequency": {
           "name": "[%key:component::habitica::common::frequency_name%]",
@@ -873,6 +891,102 @@
           "description": "[%key:component::habitica::common::developer_options_description%]"
         }
       }
+    },
+    "update_todo": {
+      "name": "Update a to-do",
+      "description": "Updates a specific to-do for a selected Habitica character",
+      "fields": {
+        "config_entry": {
+          "name": "[%key:component::habitica::common::config_entry_name%]",
+          "description": "[%key:component::habitica::common::config_entry_description%]"
+        },
+        "task": {
+          "name": "[%key:component::habitica::common::task_name%]",
+          "description": "The name (or task ID) of the to-do you want to update."
+        },
+        "rename": {
+          "name": "[%key:component::habitica::common::rename_name%]",
+          "description": "[%key:component::habitica::common::rename_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%]"
+        },
+        "remove_tag": {
+          "name": "[%key:component::habitica::common::remove_tag_name%]",
+          "description": "[%key:component::habitica::common::remove_tag_description%]"
+        },
+        "alias": {
+          "name": "[%key:component::habitica::common::alias_name%]",
+          "description": "[%key:component::habitica::common::alias_description%]"
+        },
+        "priority": {
+          "name": "[%key:component::habitica::common::priority_name%]",
+          "description": "[%key:component::habitica::common::priority_description%]"
+        },
+        "date": {
+          "name": "Due date",
+          "description": "The to-do's due date."
+        },
+        "clear_date": {
+          "name": "Clear due date",
+          "description": "Remove the due date from the to-do."
+        },
+        "reminder": {
+          "name": "[%key:component::habitica::common::reminder_name%]",
+          "description": "[%key:component::habitica::common::reminder_description%]"
+        },
+        "remove_reminder": {
+          "name": "[%key:component::habitica::common::remove_reminder_name%]",
+          "description": "[%key:component::habitica::common::remove_reminder_description%]"
+        },
+        "clear_reminder": {
+          "name": "[%key:component::habitica::common::clear_reminder_name%]",
+          "description": "[%key:component::habitica::common::clear_reminder_description%]"
+        },
+        "add_checklist_item": {
+          "name": "[%key:component::habitica::common::add_checklist_item_name%]",
+          "description": "[%key:component::habitica::common::add_checklist_item_description%]"
+        },
+        "remove_checklist_item": {
+          "name": "[%key:component::habitica::common::remove_checklist_item_name%]",
+          "description": "[%key:component::habitica::common::remove_checklist_item_description%]"
+        },
+        "score_checklist_item": {
+          "name": "[%key:component::habitica::common::score_checklist_item_name%]",
+          "description": "[%key:component::habitica::common::score_checklist_item_description%]"
+        },
+        "unscore_checklist_item": {
+          "name": "[%key:component::habitica::common::unscore_checklist_item_name%]",
+          "description": "[%key:component::habitica::common::unscore_checklist_item_description%]"
+        }
+      },
+      "sections": {
+        "checklist_options": {
+          "name": "[%key:component::habitica::common::checklist_options_name%]",
+          "description": "[%key:component::habitica::common::checklist_options_description%]"
+        },
+        "duedate_options": {
+          "name": "Due date",
+          "description": "Set, update or remove due dates of a to-do."
+        },
+        "reminder_options": {
+          "name": "[%key:component::habitica::common::reminder_options_name%]",
+          "description": "[%key:component::habitica::common::reminder_options_description%]"
+        },
+        "tag_options": {
+          "name": "[%key:component::habitica::common::tag_options_name%]",
+          "description": "[%key:component::habitica::common::tag_options_description%]"
+        },
+        "developer_options": {
+          "name": "[%key:component::habitica::common::developer_options_name%]",
+          "description": "[%key:component::habitica::common::developer_options_description%]"
+        }
+      }
     }
   },
   "selector": {
diff --git a/tests/components/habitica/conftest.py b/tests/components/habitica/conftest.py
index efb4f7300bf..4ef14699e0b 100644
--- a/tests/components/habitica/conftest.py
+++ b/tests/components/habitica/conftest.py
@@ -1,7 +1,8 @@
 """Tests for the habitica component."""
 
 from collections.abc import Generator
-from unittest.mock import AsyncMock, patch
+from unittest.mock import AsyncMock, MagicMock, patch
+from uuid import UUID
 
 from habiticalib import (
     BadRequestError,
@@ -176,3 +177,13 @@ def mock_setup_entry() -> Generator[AsyncMock]:
         "homeassistant.components.habitica.async_setup_entry", return_value=True
     ) as mock_setup_entry:
         yield mock_setup_entry
+
+
+@pytest.fixture
+def mock_uuid4() -> Generator[MagicMock]:
+    """Mock uuid4."""
+    with patch(
+        "homeassistant.components.habitica.services.uuid4", autospec=True
+    ) as mock_uuid4:
+        mock_uuid4.return_value = UUID("12345678-1234-5678-1234-567812345678")
+        yield mock_uuid4
diff --git a/tests/components/habitica/fixtures/tasks.json b/tests/components/habitica/fixtures/tasks.json
index 378652138bc..3dff57bdd51 100644
--- a/tests/components/habitica/fixtures/tasks.json
+++ b/tests/components/habitica/fixtures/tasks.json
@@ -425,7 +425,18 @@
       "date": "2024-09-27T22:17:00.000Z",
       "completed": false,
       "collapseChecklist": false,
-      "checklist": [],
+      "checklist": [
+        {
+          "completed": false,
+          "id": "fccc26f2-1e2b-4bf8-9dd0-a405be261036",
+          "text": "Checklist-item1"
+        },
+        {
+          "completed": true,
+          "id": "5a897af4-ea94-456a-a2bd-f336bcd79509",
+          "text": "Checklist-item2"
+        }
+      ],
       "type": "todo",
       "text": "Buch zu Ende lesen",
       "notes": "Das Buch, das du angefangen hast, bis zum Wochenende fertig lesen.",
diff --git a/tests/components/habitica/snapshots/test_services.ambr b/tests/components/habitica/snapshots/test_services.ambr
index 79c9e3eab66..af0ec76f3a4 100644
--- a/tests/components/habitica/snapshots/test_services.ambr
+++ b/tests/components/habitica/snapshots/test_services.ambr
@@ -736,6 +736,16 @@
           'winner': None,
         }),
         'checklist': list([
+          dict({
+            'completed': False,
+            'id': 'fccc26f2-1e2b-4bf8-9dd0-a405be261036',
+            'text': 'Checklist-item1',
+          }),
+          dict({
+            'completed': True,
+            'id': '5a897af4-ea94-456a-a2bd-f336bcd79509',
+            'text': 'Checklist-item2',
+          }),
         ]),
         'collapseChecklist': False,
         'completed': False,
@@ -1834,6 +1844,16 @@
           'winner': None,
         }),
         'checklist': list([
+          dict({
+            'completed': False,
+            'id': 'fccc26f2-1e2b-4bf8-9dd0-a405be261036',
+            'text': 'Checklist-item1',
+          }),
+          dict({
+            'completed': True,
+            'id': '5a897af4-ea94-456a-a2bd-f336bcd79509',
+            'text': 'Checklist-item2',
+          }),
         ]),
         'collapseChecklist': False,
         'completed': False,
@@ -2978,6 +2998,16 @@
           'winner': None,
         }),
         'checklist': list([
+          dict({
+            'completed': False,
+            'id': 'fccc26f2-1e2b-4bf8-9dd0-a405be261036',
+            'text': 'Checklist-item1',
+          }),
+          dict({
+            'completed': True,
+            'id': '5a897af4-ea94-456a-a2bd-f336bcd79509',
+            'text': 'Checklist-item2',
+          }),
         ]),
         'collapseChecklist': False,
         'completed': False,
@@ -5615,6 +5645,16 @@
           'winner': None,
         }),
         'checklist': list([
+          dict({
+            'completed': False,
+            'id': 'fccc26f2-1e2b-4bf8-9dd0-a405be261036',
+            'text': 'Checklist-item1',
+          }),
+          dict({
+            'completed': True,
+            'id': '5a897af4-ea94-456a-a2bd-f336bcd79509',
+            'text': 'Checklist-item2',
+          }),
         ]),
         'collapseChecklist': False,
         'completed': False,
@@ -6137,6 +6177,16 @@
           'winner': None,
         }),
         'checklist': list([
+          dict({
+            'completed': False,
+            'id': 'fccc26f2-1e2b-4bf8-9dd0-a405be261036',
+            'text': 'Checklist-item1',
+          }),
+          dict({
+            'completed': True,
+            'id': '5a897af4-ea94-456a-a2bd-f336bcd79509',
+            'text': 'Checklist-item2',
+          }),
         ]),
         'collapseChecklist': False,
         'completed': False,
diff --git a/tests/components/habitica/test_services.py b/tests/components/habitica/test_services.py
index 00ad7e6b2e9..3fd477f6858 100644
--- a/tests/components/habitica/test_services.py
+++ b/tests/components/habitica/test_services.py
@@ -1,15 +1,18 @@
 """Test Habitica actions."""
 
 from collections.abc import Generator
+from datetime import datetime
 from typing import Any
 from unittest.mock import AsyncMock, patch
 from uuid import UUID
 
 from aiohttp import ClientError
 from habiticalib import (
+    Checklist,
     Direction,
     Frequency,
     HabiticaTaskResponse,
+    Reminders,
     Skill,
     Task,
     TaskPriority,
@@ -19,7 +22,10 @@ import pytest
 from syrupy.assertion import SnapshotAssertion
 
 from homeassistant.components.habitica.const import (
+    ATTR_ADD_CHECKLIST_ITEM,
     ATTR_ALIAS,
+    ATTR_CLEAR_DATE,
+    ATTR_CLEAR_REMINDER,
     ATTR_CONFIG_ENTRY,
     ATTR_COST,
     ATTR_COUNTER_DOWN,
@@ -30,12 +36,17 @@ from homeassistant.components.habitica.const import (
     ATTR_KEYWORD,
     ATTR_NOTES,
     ATTR_PRIORITY,
+    ATTR_REMINDER,
+    ATTR_REMOVE_CHECKLIST_ITEM,
+    ATTR_REMOVE_REMINDER,
     ATTR_REMOVE_TAG,
+    ATTR_SCORE_CHECKLIST_ITEM,
     ATTR_SKILL,
     ATTR_TAG,
     ATTR_TARGET,
     ATTR_TASK,
     ATTR_TYPE,
+    ATTR_UNSCORE_CHECKLIST_ITEM,
     ATTR_UP_DOWN,
     DOMAIN,
     SERVICE_ABORT_QUEST,
@@ -53,10 +64,11 @@ from homeassistant.components.habitica.const import (
     SERVICE_TRANSFORMATION,
     SERVICE_UPDATE_HABIT,
     SERVICE_UPDATE_REWARD,
+    SERVICE_UPDATE_TODO,
 )
 from homeassistant.components.todo import ATTR_RENAME
 from homeassistant.config_entries import ConfigEntryState
-from homeassistant.const import ATTR_NAME
+from homeassistant.const import ATTR_DATE, ATTR_NAME
 from homeassistant.core import HomeAssistant
 from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
 
@@ -938,6 +950,7 @@ async def test_get_tasks(
     [
         (SERVICE_UPDATE_REWARD, "5e2ea1df-f6e6-4ba3-bccb-97c5ec63e99b"),
         (SERVICE_UPDATE_HABIT, "f21fa608-cfc6-4413-9fc7-0eb1b48ca43a"),
+        (SERVICE_UPDATE_TODO, "88de7cd9-af2b-49ce-9afd-bf941d87336b"),
     ],
 )
 @pytest.mark.usefixtures("habitica")
@@ -1318,6 +1331,184 @@ async def test_create_habit(
     habitica.create_task.assert_awaited_with(call_args)
 
 
+@pytest.mark.parametrize(
+    ("service_data", "call_args"),
+    [
+        (
+            {
+                ATTR_RENAME: "RENAME",
+            },
+            Task(text="RENAME"),
+        ),
+        (
+            {
+                ATTR_NOTES: "NOTES",
+            },
+            Task(notes="NOTES"),
+        ),
+        (
+            {
+                ATTR_ADD_CHECKLIST_ITEM: "Checklist-item",
+            },
+            Task(
+                {
+                    "checklist": [
+                        Checklist(
+                            id=UUID("fccc26f2-1e2b-4bf8-9dd0-a405be261036"),
+                            text="Checklist-item1",
+                            completed=False,
+                        ),
+                        Checklist(
+                            id=UUID("5a897af4-ea94-456a-a2bd-f336bcd79509"),
+                            text="Checklist-item2",
+                            completed=True,
+                        ),
+                        Checklist(
+                            id=UUID("12345678-1234-5678-1234-567812345678"),
+                            text="Checklist-item",
+                            completed=False,
+                        ),
+                    ]
+                }
+            ),
+        ),
+        (
+            {
+                ATTR_REMOVE_CHECKLIST_ITEM: "Checklist-item1",
+            },
+            Task(
+                {
+                    "checklist": [
+                        Checklist(
+                            id=UUID("5a897af4-ea94-456a-a2bd-f336bcd79509"),
+                            text="Checklist-item2",
+                            completed=True,
+                        ),
+                    ]
+                }
+            ),
+        ),
+        (
+            {
+                ATTR_SCORE_CHECKLIST_ITEM: "Checklist-item1",
+            },
+            Task(
+                {
+                    "checklist": [
+                        Checklist(
+                            id=UUID("fccc26f2-1e2b-4bf8-9dd0-a405be261036"),
+                            text="Checklist-item1",
+                            completed=True,
+                        ),
+                        Checklist(
+                            id=UUID("5a897af4-ea94-456a-a2bd-f336bcd79509"),
+                            text="Checklist-item2",
+                            completed=True,
+                        ),
+                    ]
+                }
+            ),
+        ),
+        (
+            {
+                ATTR_UNSCORE_CHECKLIST_ITEM: "Checklist-item2",
+            },
+            Task(
+                {
+                    "checklist": [
+                        Checklist(
+                            id=UUID("fccc26f2-1e2b-4bf8-9dd0-a405be261036"),
+                            text="Checklist-item1",
+                            completed=False,
+                        ),
+                        Checklist(
+                            id=UUID("5a897af4-ea94-456a-a2bd-f336bcd79509"),
+                            text="Checklist-item2",
+                            completed=False,
+                        ),
+                    ]
+                }
+            ),
+        ),
+        (
+            {
+                ATTR_PRIORITY: "trivial",
+            },
+            Task(priority=TaskPriority.TRIVIAL),
+        ),
+        (
+            {
+                ATTR_DATE: "2025-03-05",
+            },
+            Task(date=datetime(2025, 3, 5)),
+        ),
+        (
+            {
+                ATTR_CLEAR_DATE: True,
+            },
+            Task(date=None),
+        ),
+        (
+            {
+                ATTR_REMINDER: ["2025-02-25T00:00"],
+            },
+            Task(
+                {
+                    "reminders": [
+                        Reminders(
+                            id=UUID("12345678-1234-5678-1234-567812345678"),
+                            time=datetime(2025, 2, 25, 0, 0),
+                            startDate=None,
+                        )
+                    ]
+                }
+            ),
+        ),
+        (
+            {
+                ATTR_REMOVE_REMINDER: ["2025-02-25T00:00"],
+            },
+            Task({"reminders": []}),
+        ),
+        (
+            {
+                ATTR_CLEAR_REMINDER: True,
+            },
+            Task({"reminders": []}),
+        ),
+        (
+            {
+                ATTR_ALIAS: "ALIAS",
+            },
+            Task(alias="ALIAS"),
+        ),
+    ],
+)
+@pytest.mark.usefixtures("mock_uuid4")
+async def test_update_todo(
+    hass: HomeAssistant,
+    config_entry: MockConfigEntry,
+    habitica: AsyncMock,
+    service_data: dict[str, Any],
+    call_args: Task,
+) -> None:
+    """Test Habitica update todo action."""
+    task_id = "88de7cd9-af2b-49ce-9afd-bf941d87336b"
+
+    await hass.services.async_call(
+        DOMAIN,
+        SERVICE_UPDATE_TODO,
+        service_data={
+            ATTR_CONFIG_ENTRY: config_entry.entry_id,
+            ATTR_TASK: task_id,
+            **service_data,
+        },
+        return_response=True,
+        blocking=True,
+    )
+    habitica.update_task.assert_awaited_with(UUID(task_id), call_args)
+
+
 async def test_tags(
     hass: HomeAssistant,
     config_entry: MockConfigEntry,
-- 
GitLab