789 lines
27 KiB
Python
789 lines
27 KiB
Python
"""Actions for the Habitica integration."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import asdict
|
|
import logging
|
|
from typing import TYPE_CHECKING, Any, cast
|
|
from uuid import UUID
|
|
|
|
from aiohttp import ClientError
|
|
from habiticalib import (
|
|
Direction,
|
|
Frequency,
|
|
HabiticaException,
|
|
NotAuthorizedError,
|
|
NotFoundError,
|
|
Skill,
|
|
Task,
|
|
TaskData,
|
|
TaskPriority,
|
|
TaskType,
|
|
TooManyRequestsError,
|
|
)
|
|
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.core import (
|
|
HomeAssistant,
|
|
ServiceCall,
|
|
ServiceResponse,
|
|
SupportsResponse,
|
|
)
|
|
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
|
from homeassistant.helpers import config_validation as cv
|
|
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
|
from homeassistant.helpers.selector import ConfigEntrySelector
|
|
|
|
from .const import (
|
|
ATTR_ALIAS,
|
|
ATTR_ARGS,
|
|
ATTR_CONFIG_ENTRY,
|
|
ATTR_COST,
|
|
ATTR_COUNTER_DOWN,
|
|
ATTR_COUNTER_UP,
|
|
ATTR_DATA,
|
|
ATTR_DIRECTION,
|
|
ATTR_FREQUENCY,
|
|
ATTR_ITEM,
|
|
ATTR_KEYWORD,
|
|
ATTR_NOTES,
|
|
ATTR_PATH,
|
|
ATTR_PRIORITY,
|
|
ATTR_REMOVE_TAG,
|
|
ATTR_SKILL,
|
|
ATTR_TAG,
|
|
ATTR_TARGET,
|
|
ATTR_TASK,
|
|
ATTR_TYPE,
|
|
ATTR_UP_DOWN,
|
|
DOMAIN,
|
|
EVENT_API_CALL_SUCCESS,
|
|
SERVICE_ABORT_QUEST,
|
|
SERVICE_ACCEPT_QUEST,
|
|
SERVICE_API_CALL,
|
|
SERVICE_CANCEL_QUEST,
|
|
SERVICE_CAST_SKILL,
|
|
SERVICE_CREATE_HABIT,
|
|
SERVICE_CREATE_REWARD,
|
|
SERVICE_GET_TASKS,
|
|
SERVICE_LEAVE_QUEST,
|
|
SERVICE_REJECT_QUEST,
|
|
SERVICE_SCORE_HABIT,
|
|
SERVICE_SCORE_REWARD,
|
|
SERVICE_START_QUEST,
|
|
SERVICE_TRANSFORMATION,
|
|
SERVICE_UPDATE_HABIT,
|
|
SERVICE_UPDATE_REWARD,
|
|
)
|
|
from .coordinator import HabiticaConfigEntry
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
SERVICE_API_CALL_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Required(ATTR_NAME): str,
|
|
vol.Required(ATTR_PATH): vol.All(cv.ensure_list, [str]),
|
|
vol.Optional(ATTR_ARGS): dict,
|
|
}
|
|
)
|
|
|
|
SERVICE_CAST_SKILL_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector({"integration": DOMAIN}),
|
|
vol.Required(ATTR_SKILL): cv.string,
|
|
vol.Optional(ATTR_TASK): cv.string,
|
|
}
|
|
)
|
|
|
|
SERVICE_MANAGE_QUEST_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector({"integration": DOMAIN}),
|
|
}
|
|
)
|
|
SERVICE_SCORE_TASK_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector({"integration": DOMAIN}),
|
|
vol.Required(ATTR_TASK): cv.string,
|
|
vol.Optional(ATTR_DIRECTION): cv.string,
|
|
}
|
|
)
|
|
|
|
SERVICE_TRANSFORMATION_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector({"integration": DOMAIN}),
|
|
vol.Required(ATTR_ITEM): cv.string,
|
|
vol.Required(ATTR_TARGET): cv.string,
|
|
}
|
|
)
|
|
|
|
BASE_TASK_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector(),
|
|
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_ALIAS): vol.All(
|
|
cv.string, cv.matches_regex("^[a-zA-Z0-9-_]*$")
|
|
),
|
|
vol.Optional(ATTR_COST): vol.All(vol.Coerce(float), vol.Range(0)),
|
|
vol.Optional(ATTR_PRIORITY): vol.All(
|
|
vol.Upper, vol.In(TaskPriority._member_names_)
|
|
),
|
|
vol.Optional(ATTR_UP_DOWN): vol.All(cv.ensure_list, [str]),
|
|
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),
|
|
}
|
|
)
|
|
|
|
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,
|
|
}
|
|
)
|
|
|
|
SERVICE_GET_TASKS_SCHEMA = vol.Schema(
|
|
{
|
|
vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector({"integration": DOMAIN}),
|
|
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,
|
|
"smash": Skill.BRUTAL_SMASH,
|
|
"fireball": Skill.BURST_OF_FLAMES,
|
|
}
|
|
COST_MAP = {
|
|
"pickpocket": "10 MP",
|
|
"backstab": "15 MP",
|
|
"smash": "10 MP",
|
|
"fireball": "10 MP",
|
|
}
|
|
ITEMID_MAP = {
|
|
"snowball": Skill.SNOWBALL,
|
|
"spooky_sparkles": Skill.SPOOKY_SPARKLES,
|
|
"seafoam": Skill.SEAFOAM,
|
|
"shiny_seed": Skill.SHINY_SEED,
|
|
}
|
|
|
|
SERVICE_TASK_TYPE_MAP = {
|
|
SERVICE_UPDATE_REWARD: TaskType.REWARD,
|
|
SERVICE_CREATE_REWARD: TaskType.REWARD,
|
|
SERVICE_UPDATE_HABIT: TaskType.HABIT,
|
|
SERVICE_CREATE_HABIT: TaskType.HABIT,
|
|
}
|
|
|
|
|
|
def get_config_entry(hass: HomeAssistant, entry_id: str) -> HabiticaConfigEntry:
|
|
"""Return config entry or raise if not found or not loaded."""
|
|
if not (entry := hass.config_entries.async_get_entry(entry_id)):
|
|
raise ServiceValidationError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="entry_not_found",
|
|
)
|
|
if entry.state is not ConfigEntryState.LOADED:
|
|
raise ServiceValidationError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="entry_not_loaded",
|
|
)
|
|
return entry
|
|
|
|
|
|
def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
|
|
"""Set up services for Habitica integration."""
|
|
|
|
async def handle_api_call(call: ServiceCall) -> None:
|
|
async_create_issue(
|
|
hass,
|
|
DOMAIN,
|
|
"deprecated_api_call",
|
|
breaks_in_ha_version="2025.6.0",
|
|
is_fixable=False,
|
|
severity=IssueSeverity.WARNING,
|
|
translation_key="deprecated_api_call",
|
|
)
|
|
_LOGGER.warning(
|
|
"Deprecated action called: 'habitica.api_call' is deprecated and will be removed in Home Assistant version 2025.6.0"
|
|
)
|
|
|
|
name = call.data[ATTR_NAME]
|
|
path = call.data[ATTR_PATH]
|
|
entries: list[HabiticaConfigEntry] = hass.config_entries.async_entries(DOMAIN)
|
|
|
|
api = None
|
|
for entry in entries:
|
|
if entry.data[CONF_NAME] == name:
|
|
api = await entry.runtime_data.habitica.habitipy()
|
|
break
|
|
if api is None:
|
|
_LOGGER.error("API_CALL: User '%s' not configured", name)
|
|
return
|
|
try:
|
|
for element in path:
|
|
api = api[element]
|
|
except KeyError:
|
|
_LOGGER.error(
|
|
"API_CALL: Path %s is invalid for API on '{%s}' element", path, element
|
|
)
|
|
return
|
|
kwargs = call.data.get(ATTR_ARGS, {})
|
|
data = await api(**kwargs)
|
|
hass.bus.async_fire(
|
|
EVENT_API_CALL_SUCCESS, {ATTR_NAME: name, ATTR_PATH: path, ATTR_DATA: data}
|
|
)
|
|
|
|
async def cast_skill(call: ServiceCall) -> ServiceResponse:
|
|
"""Skill action."""
|
|
entry = get_config_entry(hass, call.data[ATTR_CONFIG_ENTRY])
|
|
coordinator = entry.runtime_data
|
|
|
|
skill = SKILL_MAP[call.data[ATTR_SKILL]]
|
|
cost = COST_MAP[call.data[ATTR_SKILL]]
|
|
|
|
try:
|
|
task_id = next(
|
|
task.id
|
|
for task in coordinator.data.tasks
|
|
if call.data[ATTR_TASK] in (str(task.id), task.alias, task.text)
|
|
)
|
|
except StopIteration as e:
|
|
raise ServiceValidationError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="task_not_found",
|
|
translation_placeholders={"task": f"'{call.data[ATTR_TASK]}'"},
|
|
) from e
|
|
|
|
try:
|
|
response = await coordinator.habitica.cast_skill(skill, task_id)
|
|
except TooManyRequestsError as e:
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="setup_rate_limit_exception",
|
|
translation_placeholders={"retry_after": str(e.retry_after)},
|
|
) from e
|
|
except NotAuthorizedError as e:
|
|
raise ServiceValidationError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="not_enough_mana",
|
|
translation_placeholders={
|
|
"cost": cost,
|
|
"mana": f"{int(coordinator.data.user.stats.mp or 0)} MP",
|
|
},
|
|
) from e
|
|
except NotFoundError as e:
|
|
# could also be task not found, but the task is looked up
|
|
# before the request, so most likely wrong skill selected
|
|
# or the skill hasn't been unlocked yet.
|
|
raise ServiceValidationError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="skill_not_found",
|
|
translation_placeholders={"skill": call.data[ATTR_SKILL]},
|
|
) from e
|
|
except HabiticaException as e:
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="service_call_exception",
|
|
translation_placeholders={"reason": str(e.error.message)},
|
|
) from e
|
|
except ClientError as e:
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="service_call_exception",
|
|
translation_placeholders={"reason": str(e)},
|
|
) from e
|
|
else:
|
|
await coordinator.async_request_refresh()
|
|
return asdict(response.data)
|
|
|
|
async def manage_quests(call: ServiceCall) -> ServiceResponse:
|
|
"""Accept, reject, start, leave or cancel quests."""
|
|
entry = get_config_entry(hass, call.data[ATTR_CONFIG_ENTRY])
|
|
coordinator = entry.runtime_data
|
|
|
|
FUNC_MAP = {
|
|
SERVICE_ABORT_QUEST: coordinator.habitica.abort_quest,
|
|
SERVICE_ACCEPT_QUEST: coordinator.habitica.accept_quest,
|
|
SERVICE_CANCEL_QUEST: coordinator.habitica.cancel_quest,
|
|
SERVICE_LEAVE_QUEST: coordinator.habitica.leave_quest,
|
|
SERVICE_REJECT_QUEST: coordinator.habitica.reject_quest,
|
|
SERVICE_START_QUEST: coordinator.habitica.start_quest,
|
|
}
|
|
|
|
func = FUNC_MAP[call.service]
|
|
|
|
try:
|
|
response = await func()
|
|
except TooManyRequestsError as e:
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="setup_rate_limit_exception",
|
|
translation_placeholders={"retry_after": str(e.retry_after)},
|
|
) from e
|
|
except NotAuthorizedError as e:
|
|
raise ServiceValidationError(
|
|
translation_domain=DOMAIN, translation_key="quest_action_unallowed"
|
|
) from e
|
|
except NotFoundError as e:
|
|
raise ServiceValidationError(
|
|
translation_domain=DOMAIN, translation_key="quest_not_found"
|
|
) from e
|
|
except HabiticaException as e:
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="service_call_exception",
|
|
translation_placeholders={"reason": str(e.error.message)},
|
|
) from e
|
|
except ClientError as e:
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="service_call_exception",
|
|
translation_placeholders={"reason": str(e)},
|
|
) from e
|
|
else:
|
|
return asdict(response.data)
|
|
|
|
for service in (
|
|
SERVICE_ABORT_QUEST,
|
|
SERVICE_ACCEPT_QUEST,
|
|
SERVICE_CANCEL_QUEST,
|
|
SERVICE_LEAVE_QUEST,
|
|
SERVICE_REJECT_QUEST,
|
|
SERVICE_START_QUEST,
|
|
):
|
|
hass.services.async_register(
|
|
DOMAIN,
|
|
service,
|
|
manage_quests,
|
|
schema=SERVICE_MANAGE_QUEST_SCHEMA,
|
|
supports_response=SupportsResponse.ONLY,
|
|
)
|
|
|
|
async def score_task(call: ServiceCall) -> ServiceResponse:
|
|
"""Score a task action."""
|
|
entry = get_config_entry(hass, call.data[ATTR_CONFIG_ENTRY])
|
|
coordinator = entry.runtime_data
|
|
|
|
direction = (
|
|
Direction.DOWN if call.data.get(ATTR_DIRECTION) == "down" else Direction.UP
|
|
)
|
|
try:
|
|
task_id, task_value = next(
|
|
(task.id, task.value)
|
|
for task in coordinator.data.tasks
|
|
if call.data[ATTR_TASK] in (str(task.id), task.alias, task.text)
|
|
)
|
|
except StopIteration as e:
|
|
raise ServiceValidationError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="task_not_found",
|
|
translation_placeholders={"task": f"'{call.data[ATTR_TASK]}'"},
|
|
) from e
|
|
|
|
if TYPE_CHECKING:
|
|
assert task_id
|
|
try:
|
|
response = await coordinator.habitica.update_score(task_id, direction)
|
|
except TooManyRequestsError as e:
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="setup_rate_limit_exception",
|
|
translation_placeholders={"retry_after": str(e.retry_after)},
|
|
) from e
|
|
except NotAuthorizedError as e:
|
|
if task_value is not None:
|
|
raise ServiceValidationError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="not_enough_gold",
|
|
translation_placeholders={
|
|
"gold": f"{(coordinator.data.user.stats.gp or 0):.2f} GP",
|
|
"cost": f"{task_value:.2f} GP",
|
|
},
|
|
) from e
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="service_call_exception",
|
|
translation_placeholders={"reason": e.error.message},
|
|
) from e
|
|
except HabiticaException as e:
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="service_call_exception",
|
|
translation_placeholders={"reason": str(e.error.message)},
|
|
) from e
|
|
except ClientError as e:
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="service_call_exception",
|
|
translation_placeholders={"reason": str(e)},
|
|
) from e
|
|
else:
|
|
await coordinator.async_request_refresh()
|
|
return asdict(response.data)
|
|
|
|
async def transformation(call: ServiceCall) -> ServiceResponse:
|
|
"""User a transformation item on a player character."""
|
|
|
|
entry = get_config_entry(hass, call.data[ATTR_CONFIG_ENTRY])
|
|
coordinator = entry.runtime_data
|
|
|
|
item = ITEMID_MAP[call.data[ATTR_ITEM]]
|
|
# check if target is self
|
|
if call.data[ATTR_TARGET] in (
|
|
str(coordinator.data.user.id),
|
|
coordinator.data.user.profile.name,
|
|
coordinator.data.user.auth.local.username,
|
|
):
|
|
target_id = coordinator.data.user.id
|
|
else:
|
|
# check if target is a party member
|
|
try:
|
|
party = await coordinator.habitica.get_group_members(public_fields=True)
|
|
except NotFoundError as e:
|
|
raise ServiceValidationError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="party_not_found",
|
|
) from e
|
|
except HabiticaException as e:
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="service_call_exception",
|
|
translation_placeholders={"reason": str(e.error.message)},
|
|
) from e
|
|
except ClientError as e:
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="service_call_exception",
|
|
translation_placeholders={"reason": str(e)},
|
|
) from e
|
|
try:
|
|
target_id = next(
|
|
member.id
|
|
for member in party.data
|
|
if member.id
|
|
and call.data[ATTR_TARGET].lower()
|
|
in (
|
|
str(member.id),
|
|
str(member.auth.local.username).lower(),
|
|
str(member.profile.name).lower(),
|
|
)
|
|
)
|
|
except StopIteration as e:
|
|
raise ServiceValidationError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="target_not_found",
|
|
translation_placeholders={"target": f"'{call.data[ATTR_TARGET]}'"},
|
|
) from e
|
|
try:
|
|
response = await coordinator.habitica.cast_skill(item, target_id)
|
|
except TooManyRequestsError as e:
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="setup_rate_limit_exception",
|
|
translation_placeholders={"retry_after": str(e.retry_after)},
|
|
) from e
|
|
except NotAuthorizedError as e:
|
|
raise ServiceValidationError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="item_not_found",
|
|
translation_placeholders={"item": call.data[ATTR_ITEM]},
|
|
) from e
|
|
except HabiticaException as e:
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="service_call_exception",
|
|
translation_placeholders={"reason": str(e.error.message)},
|
|
) from e
|
|
except ClientError as e:
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="service_call_exception",
|
|
translation_placeholders={"reason": str(e)},
|
|
) from e
|
|
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": [task.to_dict(omit_none=False) for task in response]
|
|
}
|
|
|
|
return result
|
|
|
|
async def create_or_update_task(call: ServiceCall) -> ServiceResponse: # noqa: C901
|
|
"""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 in (SERVICE_UPDATE_REWARD, SERVICE_UPDATE_HABIT)
|
|
current_task = None
|
|
|
|
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 SERVICE_TASK_TYPE_MAP[call.service]
|
|
)
|
|
except StopIteration as e:
|
|
raise ServiceValidationError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="task_not_found",
|
|
translation_placeholders={"task": f"'{call.data[ATTR_TASK]}'"},
|
|
) from e
|
|
|
|
data = Task()
|
|
|
|
if not is_update:
|
|
data["type"] = SERVICE_TASK_TYPE_MAP[call.service]
|
|
|
|
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
|
|
|
|
tags = cast(list[str], call.data.get(ATTR_TAG))
|
|
remove_tags = cast(list[str], call.data.get(ATTR_REMOVE_TAG))
|
|
|
|
if tags or remove_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
|
|
if tag.id and tag.name
|
|
}
|
|
|
|
if tags:
|
|
# Creates new tag if it doesn't exist
|
|
async def create_tag(tag_name: str) -> UUID:
|
|
tag_id = (await coordinator.habitica.create_tag(tag_name)).data.id
|
|
if TYPE_CHECKING:
|
|
assert tag_id
|
|
return tag_id
|
|
|
|
try:
|
|
update_tags.update(
|
|
{
|
|
user_tags.get(tag_name.lower())
|
|
or (await create_tag(tag_name))
|
|
for tag_name in tags
|
|
}
|
|
)
|
|
except TooManyRequestsError as e:
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="setup_rate_limit_exception",
|
|
translation_placeholders={"retry_after": str(e.retry_after)},
|
|
) from e
|
|
except HabiticaException as e:
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="service_call_exception",
|
|
translation_placeholders={"reason": str(e.error.message)},
|
|
) from e
|
|
except ClientError as e:
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="service_call_exception",
|
|
translation_placeholders={"reason": str(e)},
|
|
) from e
|
|
|
|
if remove_tags:
|
|
update_tags.difference_update(
|
|
{
|
|
user_tags[tag_name.lower()]
|
|
for tag_name in remove_tags
|
|
if tag_name.lower() in user_tags
|
|
}
|
|
)
|
|
|
|
data["tags"] = list(update_tags)
|
|
|
|
if (alias := call.data.get(ATTR_ALIAS)) is not None:
|
|
data["alias"] = alias
|
|
|
|
if (cost := call.data.get(ATTR_COST)) is not None:
|
|
data["value"] = cost
|
|
|
|
if priority := call.data.get(ATTR_PRIORITY):
|
|
data["priority"] = TaskPriority[priority]
|
|
|
|
if frequency := call.data.get(ATTR_FREQUENCY):
|
|
data["frequency"] = frequency
|
|
|
|
if up_down := call.data.get(ATTR_UP_DOWN):
|
|
data["up"] = "up" in up_down
|
|
data["down"] = "down" in up_down
|
|
|
|
if counter_up := call.data.get(ATTR_COUNTER_UP):
|
|
data["counterUp"] = counter_up
|
|
|
|
if counter_down := call.data.get(ATTR_COUNTER_DOWN):
|
|
data["counterDown"] = counter_down
|
|
|
|
try:
|
|
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,
|
|
translation_key="setup_rate_limit_exception",
|
|
translation_placeholders={"retry_after": str(e.retry_after)},
|
|
) from e
|
|
except HabiticaException as e:
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="service_call_exception",
|
|
translation_placeholders={"reason": str(e.error.message)},
|
|
) from e
|
|
except ClientError as e:
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="service_call_exception",
|
|
translation_placeholders={"reason": str(e)},
|
|
) from e
|
|
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,
|
|
)
|
|
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_CREATE_HABIT,
|
|
create_or_update_task,
|
|
schema=SERVICE_CREATE_TASK_SCHEMA,
|
|
supports_response=SupportsResponse.ONLY,
|
|
)
|
|
hass.services.async_register(
|
|
DOMAIN,
|
|
SERVICE_API_CALL,
|
|
handle_api_call,
|
|
schema=SERVICE_API_CALL_SCHEMA,
|
|
)
|
|
|
|
hass.services.async_register(
|
|
DOMAIN,
|
|
SERVICE_CAST_SKILL,
|
|
cast_skill,
|
|
schema=SERVICE_CAST_SKILL_SCHEMA,
|
|
supports_response=SupportsResponse.ONLY,
|
|
)
|
|
|
|
hass.services.async_register(
|
|
DOMAIN,
|
|
SERVICE_SCORE_HABIT,
|
|
score_task,
|
|
schema=SERVICE_SCORE_TASK_SCHEMA,
|
|
supports_response=SupportsResponse.ONLY,
|
|
)
|
|
hass.services.async_register(
|
|
DOMAIN,
|
|
SERVICE_SCORE_REWARD,
|
|
score_task,
|
|
schema=SERVICE_SCORE_TASK_SCHEMA,
|
|
supports_response=SupportsResponse.ONLY,
|
|
)
|
|
|
|
hass.services.async_register(
|
|
DOMAIN,
|
|
SERVICE_TRANSFORMATION,
|
|
transformation,
|
|
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,
|
|
)
|