mirror of
https://github.com/home-assistant/core.git
synced 2025-07-17 10:17:09 +00:00
Add update_todo
action to Habitica (#139799)
* update_todo action * fix strings
This commit is contained in:
parent
fe34e6beee
commit
3ccb7d80f3
@ -44,6 +44,14 @@ ATTR_UP_DOWN = "up_down"
|
|||||||
ATTR_FREQUENCY = "frequency"
|
ATTR_FREQUENCY = "frequency"
|
||||||
ATTR_COUNTER_UP = "counter_up"
|
ATTR_COUNTER_UP = "counter_up"
|
||||||
ATTR_COUNTER_DOWN = "counter_down"
|
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_CAST_SKILL = "cast_skill"
|
||||||
SERVICE_START_QUEST = "start_quest"
|
SERVICE_START_QUEST = "start_quest"
|
||||||
@ -63,6 +71,7 @@ SERVICE_UPDATE_REWARD = "update_reward"
|
|||||||
SERVICE_CREATE_REWARD = "create_reward"
|
SERVICE_CREATE_REWARD = "create_reward"
|
||||||
SERVICE_UPDATE_HABIT = "update_habit"
|
SERVICE_UPDATE_HABIT = "update_habit"
|
||||||
SERVICE_CREATE_HABIT = "create_habit"
|
SERVICE_CREATE_HABIT = "create_habit"
|
||||||
|
SERVICE_UPDATE_TODO = "update_todo"
|
||||||
|
|
||||||
DEVELOPER_ID = "4c4ca53f-c059-4ffa-966e-9d29dd405daf"
|
DEVELOPER_ID = "4c4ca53f-c059-4ffa-966e-9d29dd405daf"
|
||||||
X_CLIENT = f"{DEVELOPER_ID} - {APPLICATION_NAME} {__version__}"
|
X_CLIENT = f"{DEVELOPER_ID} - {APPLICATION_NAME} {__version__}"
|
||||||
|
@ -243,6 +243,16 @@
|
|||||||
"sections": {
|
"sections": {
|
||||||
"developer_options": "mdi:test-tube"
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,17 +3,20 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import asdict
|
from dataclasses import asdict
|
||||||
|
from datetime import datetime, time
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any, cast
|
from typing import TYPE_CHECKING, Any, cast
|
||||||
from uuid import UUID
|
from uuid import UUID, uuid4
|
||||||
|
|
||||||
from aiohttp import ClientError
|
from aiohttp import ClientError
|
||||||
from habiticalib import (
|
from habiticalib import (
|
||||||
|
Checklist,
|
||||||
Direction,
|
Direction,
|
||||||
Frequency,
|
Frequency,
|
||||||
HabiticaException,
|
HabiticaException,
|
||||||
NotAuthorizedError,
|
NotAuthorizedError,
|
||||||
NotFoundError,
|
NotFoundError,
|
||||||
|
Reminders,
|
||||||
Skill,
|
Skill,
|
||||||
Task,
|
Task,
|
||||||
TaskData,
|
TaskData,
|
||||||
@ -25,7 +28,7 @@ import voluptuous as vol
|
|||||||
|
|
||||||
from homeassistant.components.todo import ATTR_RENAME
|
from homeassistant.components.todo import ATTR_RENAME
|
||||||
from homeassistant.config_entries import ConfigEntryState
|
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 (
|
from homeassistant.core import (
|
||||||
HomeAssistant,
|
HomeAssistant,
|
||||||
ServiceCall,
|
ServiceCall,
|
||||||
@ -38,8 +41,11 @@ from homeassistant.helpers.issue_registry import IssueSeverity, async_create_iss
|
|||||||
from homeassistant.helpers.selector import ConfigEntrySelector
|
from homeassistant.helpers.selector import ConfigEntrySelector
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
ATTR_ADD_CHECKLIST_ITEM,
|
||||||
ATTR_ALIAS,
|
ATTR_ALIAS,
|
||||||
ATTR_ARGS,
|
ATTR_ARGS,
|
||||||
|
ATTR_CLEAR_DATE,
|
||||||
|
ATTR_CLEAR_REMINDER,
|
||||||
ATTR_CONFIG_ENTRY,
|
ATTR_CONFIG_ENTRY,
|
||||||
ATTR_COST,
|
ATTR_COST,
|
||||||
ATTR_COUNTER_DOWN,
|
ATTR_COUNTER_DOWN,
|
||||||
@ -52,12 +58,17 @@ from .const import (
|
|||||||
ATTR_NOTES,
|
ATTR_NOTES,
|
||||||
ATTR_PATH,
|
ATTR_PATH,
|
||||||
ATTR_PRIORITY,
|
ATTR_PRIORITY,
|
||||||
|
ATTR_REMINDER,
|
||||||
|
ATTR_REMOVE_CHECKLIST_ITEM,
|
||||||
|
ATTR_REMOVE_REMINDER,
|
||||||
ATTR_REMOVE_TAG,
|
ATTR_REMOVE_TAG,
|
||||||
|
ATTR_SCORE_CHECKLIST_ITEM,
|
||||||
ATTR_SKILL,
|
ATTR_SKILL,
|
||||||
ATTR_TAG,
|
ATTR_TAG,
|
||||||
ATTR_TARGET,
|
ATTR_TARGET,
|
||||||
ATTR_TASK,
|
ATTR_TASK,
|
||||||
ATTR_TYPE,
|
ATTR_TYPE,
|
||||||
|
ATTR_UNSCORE_CHECKLIST_ITEM,
|
||||||
ATTR_UP_DOWN,
|
ATTR_UP_DOWN,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
EVENT_API_CALL_SUCCESS,
|
EVENT_API_CALL_SUCCESS,
|
||||||
@ -77,6 +88,7 @@ from .const import (
|
|||||||
SERVICE_TRANSFORMATION,
|
SERVICE_TRANSFORMATION,
|
||||||
SERVICE_UPDATE_HABIT,
|
SERVICE_UPDATE_HABIT,
|
||||||
SERVICE_UPDATE_REWARD,
|
SERVICE_UPDATE_REWARD,
|
||||||
|
SERVICE_UPDATE_TODO,
|
||||||
)
|
)
|
||||||
from .coordinator import HabiticaConfigEntry
|
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_UP): vol.All(int, vol.Range(0)),
|
||||||
vol.Optional(ATTR_COUNTER_DOWN): 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_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_CREATE_REWARD: TaskType.REWARD,
|
||||||
SERVICE_UPDATE_HABIT: TaskType.HABIT,
|
SERVICE_UPDATE_HABIT: TaskType.HABIT,
|
||||||
SERVICE_CREATE_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])
|
entry = get_config_entry(hass, call.data[ATTR_CONFIG_ENTRY])
|
||||||
coordinator = entry.runtime_data
|
coordinator = entry.runtime_data
|
||||||
await coordinator.async_refresh()
|
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
|
current_task = None
|
||||||
|
|
||||||
if is_update:
|
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):
|
if counter_down := call.data.get(ATTR_COUNTER_DOWN):
|
||||||
data["counterDown"] = 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:
|
try:
|
||||||
if is_update:
|
if is_update:
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@ -714,20 +803,14 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
|
|||||||
else:
|
else:
|
||||||
return response.data.to_dict(omit_none=True)
|
return response.data.to_dict(omit_none=True)
|
||||||
|
|
||||||
hass.services.async_register(
|
for service in (SERVICE_UPDATE_TODO, SERVICE_UPDATE_REWARD, SERVICE_UPDATE_HABIT):
|
||||||
DOMAIN,
|
hass.services.async_register(
|
||||||
SERVICE_UPDATE_REWARD,
|
DOMAIN,
|
||||||
create_or_update_task,
|
service,
|
||||||
schema=SERVICE_UPDATE_TASK_SCHEMA,
|
create_or_update_task,
|
||||||
supports_response=SupportsResponse.ONLY,
|
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,
|
|
||||||
)
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_CREATE_REWARD,
|
SERVICE_CREATE_REWARD,
|
||||||
|
@ -262,3 +262,69 @@ create_habit:
|
|||||||
frequency: *frequency
|
frequency: *frequency
|
||||||
tag: *tag
|
tag: *tag
|
||||||
developer_options: *developer_options
|
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
|
||||||
|
@ -26,12 +26,30 @@
|
|||||||
"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.",
|
"name_description": "The title for the Habitica task.",
|
||||||
"cost_name": "Cost",
|
"cost_name": "Cost",
|
||||||
"difficulty_name": "Difficulty",
|
"priority_name": "Difficulty",
|
||||||
"difficulty_description": "The difficulty of the task.",
|
"priority_description": "The difficulty of the task.",
|
||||||
"frequency_name": "Counter reset",
|
"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.",
|
"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_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": {
|
"config": {
|
||||||
"abort": {
|
"abort": {
|
||||||
@ -659,7 +677,7 @@
|
|||||||
"description": "Filter tasks by type."
|
"description": "Filter tasks by type."
|
||||||
},
|
},
|
||||||
"priority": {
|
"priority": {
|
||||||
"name": "Difficulty",
|
"name": "[%key:component::habitica::common::priority_name%]",
|
||||||
"description": "Filter tasks by difficulty."
|
"description": "Filter tasks by difficulty."
|
||||||
},
|
},
|
||||||
"task": {
|
"task": {
|
||||||
@ -799,8 +817,8 @@
|
|||||||
"description": "[%key:component::habitica::common::alias_description%]"
|
"description": "[%key:component::habitica::common::alias_description%]"
|
||||||
},
|
},
|
||||||
"priority": {
|
"priority": {
|
||||||
"name": "[%key:component::habitica::common::difficulty_name%]",
|
"name": "[%key:component::habitica::common::priority_name%]",
|
||||||
"description": "[%key:component::habitica::common::difficulty_description%]"
|
"description": "[%key:component::habitica::common::priority_description%]"
|
||||||
},
|
},
|
||||||
"frequency": {
|
"frequency": {
|
||||||
"name": "[%key:component::habitica::common::frequency_name%]",
|
"name": "[%key:component::habitica::common::frequency_name%]",
|
||||||
@ -855,8 +873,8 @@
|
|||||||
"description": "[%key:component::habitica::common::alias_description%]"
|
"description": "[%key:component::habitica::common::alias_description%]"
|
||||||
},
|
},
|
||||||
"priority": {
|
"priority": {
|
||||||
"name": "[%key:component::habitica::common::difficulty_name%]",
|
"name": "[%key:component::habitica::common::priority_name%]",
|
||||||
"description": "[%key:component::habitica::common::difficulty_description%]"
|
"description": "[%key:component::habitica::common::priority_description%]"
|
||||||
},
|
},
|
||||||
"frequency": {
|
"frequency": {
|
||||||
"name": "[%key:component::habitica::common::frequency_name%]",
|
"name": "[%key:component::habitica::common::frequency_name%]",
|
||||||
@ -873,6 +891,102 @@
|
|||||||
"description": "[%key:component::habitica::common::developer_options_description%]"
|
"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": {
|
"selector": {
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
"""Tests for the habitica component."""
|
"""Tests for the habitica component."""
|
||||||
|
|
||||||
from collections.abc import Generator
|
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 (
|
from habiticalib import (
|
||||||
BadRequestError,
|
BadRequestError,
|
||||||
@ -176,3 +177,13 @@ def mock_setup_entry() -> Generator[AsyncMock]:
|
|||||||
"homeassistant.components.habitica.async_setup_entry", return_value=True
|
"homeassistant.components.habitica.async_setup_entry", return_value=True
|
||||||
) as mock_setup_entry:
|
) as mock_setup_entry:
|
||||||
yield 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
|
||||||
|
@ -425,7 +425,18 @@
|
|||||||
"date": "2024-09-27T22:17:00.000Z",
|
"date": "2024-09-27T22:17:00.000Z",
|
||||||
"completed": false,
|
"completed": false,
|
||||||
"collapseChecklist": 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",
|
"type": "todo",
|
||||||
"text": "Buch zu Ende lesen",
|
"text": "Buch zu Ende lesen",
|
||||||
"notes": "Das Buch, das du angefangen hast, bis zum Wochenende fertig lesen.",
|
"notes": "Das Buch, das du angefangen hast, bis zum Wochenende fertig lesen.",
|
||||||
|
@ -736,6 +736,16 @@
|
|||||||
'winner': None,
|
'winner': None,
|
||||||
}),
|
}),
|
||||||
'checklist': list([
|
'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,
|
'collapseChecklist': False,
|
||||||
'completed': False,
|
'completed': False,
|
||||||
@ -1834,6 +1844,16 @@
|
|||||||
'winner': None,
|
'winner': None,
|
||||||
}),
|
}),
|
||||||
'checklist': list([
|
'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,
|
'collapseChecklist': False,
|
||||||
'completed': False,
|
'completed': False,
|
||||||
@ -2978,6 +2998,16 @@
|
|||||||
'winner': None,
|
'winner': None,
|
||||||
}),
|
}),
|
||||||
'checklist': list([
|
'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,
|
'collapseChecklist': False,
|
||||||
'completed': False,
|
'completed': False,
|
||||||
@ -5615,6 +5645,16 @@
|
|||||||
'winner': None,
|
'winner': None,
|
||||||
}),
|
}),
|
||||||
'checklist': list([
|
'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,
|
'collapseChecklist': False,
|
||||||
'completed': False,
|
'completed': False,
|
||||||
@ -6137,6 +6177,16 @@
|
|||||||
'winner': None,
|
'winner': None,
|
||||||
}),
|
}),
|
||||||
'checklist': list([
|
'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,
|
'collapseChecklist': False,
|
||||||
'completed': False,
|
'completed': False,
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
"""Test Habitica actions."""
|
"""Test Habitica actions."""
|
||||||
|
|
||||||
from collections.abc import Generator
|
from collections.abc import Generator
|
||||||
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import AsyncMock, patch
|
from unittest.mock import AsyncMock, patch
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from aiohttp import ClientError
|
from aiohttp import ClientError
|
||||||
from habiticalib import (
|
from habiticalib import (
|
||||||
|
Checklist,
|
||||||
Direction,
|
Direction,
|
||||||
Frequency,
|
Frequency,
|
||||||
HabiticaTaskResponse,
|
HabiticaTaskResponse,
|
||||||
|
Reminders,
|
||||||
Skill,
|
Skill,
|
||||||
Task,
|
Task,
|
||||||
TaskPriority,
|
TaskPriority,
|
||||||
@ -19,7 +22,10 @@ import pytest
|
|||||||
from syrupy.assertion import SnapshotAssertion
|
from syrupy.assertion import SnapshotAssertion
|
||||||
|
|
||||||
from homeassistant.components.habitica.const import (
|
from homeassistant.components.habitica.const import (
|
||||||
|
ATTR_ADD_CHECKLIST_ITEM,
|
||||||
ATTR_ALIAS,
|
ATTR_ALIAS,
|
||||||
|
ATTR_CLEAR_DATE,
|
||||||
|
ATTR_CLEAR_REMINDER,
|
||||||
ATTR_CONFIG_ENTRY,
|
ATTR_CONFIG_ENTRY,
|
||||||
ATTR_COST,
|
ATTR_COST,
|
||||||
ATTR_COUNTER_DOWN,
|
ATTR_COUNTER_DOWN,
|
||||||
@ -30,12 +36,17 @@ from homeassistant.components.habitica.const import (
|
|||||||
ATTR_KEYWORD,
|
ATTR_KEYWORD,
|
||||||
ATTR_NOTES,
|
ATTR_NOTES,
|
||||||
ATTR_PRIORITY,
|
ATTR_PRIORITY,
|
||||||
|
ATTR_REMINDER,
|
||||||
|
ATTR_REMOVE_CHECKLIST_ITEM,
|
||||||
|
ATTR_REMOVE_REMINDER,
|
||||||
ATTR_REMOVE_TAG,
|
ATTR_REMOVE_TAG,
|
||||||
|
ATTR_SCORE_CHECKLIST_ITEM,
|
||||||
ATTR_SKILL,
|
ATTR_SKILL,
|
||||||
ATTR_TAG,
|
ATTR_TAG,
|
||||||
ATTR_TARGET,
|
ATTR_TARGET,
|
||||||
ATTR_TASK,
|
ATTR_TASK,
|
||||||
ATTR_TYPE,
|
ATTR_TYPE,
|
||||||
|
ATTR_UNSCORE_CHECKLIST_ITEM,
|
||||||
ATTR_UP_DOWN,
|
ATTR_UP_DOWN,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_ABORT_QUEST,
|
SERVICE_ABORT_QUEST,
|
||||||
@ -53,10 +64,11 @@ from homeassistant.components.habitica.const import (
|
|||||||
SERVICE_TRANSFORMATION,
|
SERVICE_TRANSFORMATION,
|
||||||
SERVICE_UPDATE_HABIT,
|
SERVICE_UPDATE_HABIT,
|
||||||
SERVICE_UPDATE_REWARD,
|
SERVICE_UPDATE_REWARD,
|
||||||
|
SERVICE_UPDATE_TODO,
|
||||||
)
|
)
|
||||||
from homeassistant.components.todo import ATTR_RENAME
|
from homeassistant.components.todo import ATTR_RENAME
|
||||||
from homeassistant.config_entries import ConfigEntryState
|
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.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
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_REWARD, "5e2ea1df-f6e6-4ba3-bccb-97c5ec63e99b"),
|
||||||
(SERVICE_UPDATE_HABIT, "f21fa608-cfc6-4413-9fc7-0eb1b48ca43a"),
|
(SERVICE_UPDATE_HABIT, "f21fa608-cfc6-4413-9fc7-0eb1b48ca43a"),
|
||||||
|
(SERVICE_UPDATE_TODO, "88de7cd9-af2b-49ce-9afd-bf941d87336b"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@pytest.mark.usefixtures("habitica")
|
@pytest.mark.usefixtures("habitica")
|
||||||
@ -1318,6 +1331,184 @@ async def test_create_habit(
|
|||||||
habitica.create_task.assert_awaited_with(call_args)
|
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(
|
async def test_tags(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: MockConfigEntry,
|
config_entry: MockConfigEntry,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user