1 """Actions for the Habitica integration."""
3 from __future__
import annotations
5 from http
import HTTPStatus
9 from aiohttp
import ClientResponseError
10 import voluptuous
as vol
36 EVENT_API_CALL_SUCCESS,
47 SERVICE_TRANSFORMATION,
49 from .types
import HabiticaConfigEntry
51 _LOGGER = logging.getLogger(__name__)
54 SERVICE_API_CALL_SCHEMA = vol.Schema(
56 vol.Required(ATTR_NAME): str,
57 vol.Required(ATTR_PATH): vol.All(cv.ensure_list, [str]),
58 vol.Optional(ATTR_ARGS): dict,
62 SERVICE_CAST_SKILL_SCHEMA = vol.Schema(
65 vol.Required(ATTR_SKILL): cv.string,
66 vol.Optional(ATTR_TASK): cv.string,
70 SERVICE_MANAGE_QUEST_SCHEMA = vol.Schema(
75 SERVICE_SCORE_TASK_SCHEMA = vol.Schema(
78 vol.Required(ATTR_TASK): cv.string,
79 vol.Optional(ATTR_DIRECTION): cv.string,
83 SERVICE_TRANSFORMATION_SCHEMA = vol.Schema(
86 vol.Required(ATTR_ITEM): cv.string,
87 vol.Required(ATTR_TARGET): cv.string,
93 """Return config entry or raise if not found or not loaded."""
94 if not (entry := hass.config_entries.async_get_entry(entry_id)):
96 translation_domain=DOMAIN,
97 translation_key=
"entry_not_found",
99 if entry.state
is not ConfigEntryState.LOADED:
101 translation_domain=DOMAIN,
102 translation_key=
"entry_not_loaded",
108 """Set up services for Habitica integration."""
110 async
def handle_api_call(call: ServiceCall) ->
None:
114 "deprecated_api_call",
115 breaks_in_ha_version=
"2025.6.0",
117 severity=IssueSeverity.WARNING,
118 translation_key=
"deprecated_api_call",
121 "Deprecated action called: 'habitica.api_call' is deprecated and will be removed in Home Assistant version 2025.6.0"
124 name = call.data[ATTR_NAME]
125 path = call.data[ATTR_PATH]
126 entries = hass.config_entries.async_entries(DOMAIN)
129 for entry
in entries:
130 if entry.data[CONF_NAME] == name:
131 api = entry.runtime_data.api
134 _LOGGER.error(
"API_CALL: User '%s' not configured", name)
141 "API_CALL: Path %s is invalid for API on '{%s}' element", path, element
144 kwargs = call.data.get(ATTR_ARGS, {})
145 data = await
api(**kwargs)
147 EVENT_API_CALL_SUCCESS, {ATTR_NAME: name, ATTR_PATH: path, ATTR_DATA: data}
150 async
def cast_skill(call: ServiceCall) -> ServiceResponse:
153 coordinator = entry.runtime_data
155 "pickpocket": {
"spellId":
"pickPocket",
"cost":
"10 MP"},
156 "backstab": {
"spellId":
"backStab",
"cost":
"15 MP"},
157 "smash": {
"spellId":
"smash",
"cost":
"10 MP"},
158 "fireball": {
"spellId":
"fireball",
"cost":
"10 MP"},
163 for task
in coordinator.data.tasks
164 if call.data[ATTR_TASK]
in (task[
"id"], task.get(
"alias"))
165 or call.data[ATTR_TASK] == task[
"text"]
167 except StopIteration
as e:
169 translation_domain=DOMAIN,
170 translation_key=
"task_not_found",
171 translation_placeholders={
"task": f
"'{call.data[ATTR_TASK]}'"},
175 response: dict[str, Any] = await coordinator.api.user.class_.cast[
176 skill[call.data[ATTR_SKILL]][
"spellId"]
177 ].
post(targetId=task_id)
178 except ClientResponseError
as e:
179 if e.status == HTTPStatus.TOO_MANY_REQUESTS:
181 translation_domain=DOMAIN,
182 translation_key=
"setup_rate_limit_exception",
184 if e.status == HTTPStatus.UNAUTHORIZED:
186 translation_domain=DOMAIN,
187 translation_key=
"not_enough_mana",
188 translation_placeholders={
189 "cost": skill[call.data[ATTR_SKILL]][
"cost"],
190 "mana": f
"{int(coordinator.data.user.get("stats
", {}).get("mp
", 0))} MP",
193 if e.status == HTTPStatus.NOT_FOUND:
198 translation_domain=DOMAIN,
199 translation_key=
"skill_not_found",
200 translation_placeholders={
"skill": call.data[ATTR_SKILL]},
203 translation_domain=DOMAIN,
204 translation_key=
"service_call_exception",
207 await coordinator.async_request_refresh()
210 async
def manage_quests(call: ServiceCall) -> ServiceResponse:
211 """Accept, reject, start, leave or cancel quests."""
213 coordinator = entry.runtime_data
216 SERVICE_ABORT_QUEST:
"abort",
217 SERVICE_ACCEPT_QUEST:
"accept",
218 SERVICE_CANCEL_QUEST:
"cancel",
219 SERVICE_LEAVE_QUEST:
"leave",
220 SERVICE_REJECT_QUEST:
"reject",
221 SERVICE_START_QUEST:
"force-start",
224 return await coordinator.api.groups.party.quests[
225 COMMAND_MAP[call.service]
227 except ClientResponseError
as e:
228 if e.status == HTTPStatus.TOO_MANY_REQUESTS:
230 translation_domain=DOMAIN,
231 translation_key=
"setup_rate_limit_exception",
233 if e.status == HTTPStatus.UNAUTHORIZED:
235 translation_domain=DOMAIN, translation_key=
"quest_action_unallowed"
237 if e.status == HTTPStatus.NOT_FOUND:
239 translation_domain=DOMAIN, translation_key=
"quest_not_found"
242 translation_domain=DOMAIN, translation_key=
"service_call_exception"
247 SERVICE_ACCEPT_QUEST,
248 SERVICE_CANCEL_QUEST,
250 SERVICE_REJECT_QUEST,
253 hass.services.async_register(
257 schema=SERVICE_MANAGE_QUEST_SCHEMA,
258 supports_response=SupportsResponse.ONLY,
261 async
def score_task(call: ServiceCall) -> ServiceResponse:
262 """Score a task action."""
264 coordinator = entry.runtime_data
266 task_id, task_value = next(
267 (task[
"id"], task.get(
"value"))
268 for task
in coordinator.data.tasks
269 if call.data[ATTR_TASK]
in (task[
"id"], task.get(
"alias"))
270 or call.data[ATTR_TASK] == task[
"text"]
272 except StopIteration
as e:
274 translation_domain=DOMAIN,
275 translation_key=
"task_not_found",
276 translation_placeholders={
"task": f
"'{call.data[ATTR_TASK]}'"},
280 response: dict[str, Any] = (
281 await coordinator.api.tasks[task_id]
282 .score[call.data.get(ATTR_DIRECTION,
"up")]
285 except ClientResponseError
as e:
286 if e.status == HTTPStatus.TOO_MANY_REQUESTS:
288 translation_domain=DOMAIN,
289 translation_key=
"setup_rate_limit_exception",
291 if e.status == HTTPStatus.UNAUTHORIZED
and task_value
is not None:
293 translation_domain=DOMAIN,
294 translation_key=
"not_enough_gold",
295 translation_placeholders={
296 "gold": f
"{coordinator.data.user["stats
"]["gp
"]:.2f} GP",
297 "cost": f
"{task_value} GP",
301 translation_domain=DOMAIN,
302 translation_key=
"service_call_exception",
305 await coordinator.async_request_refresh()
308 async
def transformation(call: ServiceCall) -> ServiceResponse:
309 """User a transformation item on a player character."""
312 coordinator = entry.runtime_data
314 "snowball": {
"itemId":
"snowball"},
315 "spooky_sparkles": {
"itemId":
"spookySparkles"},
316 "seafoam": {
"itemId":
"seafoam"},
317 "shiny_seed": {
"itemId":
"shinySeed"},
320 if call.data[ATTR_TARGET]
in (
321 coordinator.data.user[
"id"],
322 coordinator.data.user[
"profile"][
"name"],
323 coordinator.data.user[
"auth"][
"local"][
"username"],
325 target_id = coordinator.data.user[
"id"]
329 party = await coordinator.api.groups.party.members.get()
330 except ClientResponseError
as e:
331 if e.status == HTTPStatus.TOO_MANY_REQUESTS:
333 translation_domain=DOMAIN,
334 translation_key=
"setup_rate_limit_exception",
336 if e.status == HTTPStatus.NOT_FOUND:
338 translation_domain=DOMAIN,
339 translation_key=
"party_not_found",
342 translation_domain=DOMAIN,
343 translation_key=
"service_call_exception",
349 if call.data[ATTR_TARGET].lower()
352 member[
"auth"][
"local"][
"username"].lower(),
353 member[
"profile"][
"name"].lower(),
356 except StopIteration
as e:
358 translation_domain=DOMAIN,
359 translation_key=
"target_not_found",
360 translation_placeholders={
"target": f
"'{call.data[ATTR_TARGET]}'"},
363 response: dict[str, Any] = await coordinator.api.user.class_.cast[
364 ITEMID_MAP[call.data[ATTR_ITEM]][
"itemId"]
365 ].
post(targetId=target_id)
366 except ClientResponseError
as e:
367 if e.status == HTTPStatus.TOO_MANY_REQUESTS:
369 translation_domain=DOMAIN,
370 translation_key=
"setup_rate_limit_exception",
372 if e.status == HTTPStatus.UNAUTHORIZED:
374 translation_domain=DOMAIN,
375 translation_key=
"item_not_found",
376 translation_placeholders={
"item": call.data[ATTR_ITEM]},
379 translation_domain=DOMAIN,
380 translation_key=
"service_call_exception",
385 hass.services.async_register(
389 schema=SERVICE_API_CALL_SCHEMA,
392 hass.services.async_register(
396 schema=SERVICE_CAST_SKILL_SCHEMA,
397 supports_response=SupportsResponse.ONLY,
400 hass.services.async_register(
404 schema=SERVICE_SCORE_TASK_SCHEMA,
405 supports_response=SupportsResponse.ONLY,
407 hass.services.async_register(
409 SERVICE_SCORE_REWARD,
411 schema=SERVICE_SCORE_TASK_SCHEMA,
412 supports_response=SupportsResponse.ONLY,
415 hass.services.async_register(
417 SERVICE_TRANSFORMATION,
419 schema=SERVICE_TRANSFORMATION_SCHEMA,
420 supports_response=SupportsResponse.ONLY,
web.Response post(self, web.Request request, str config_key)
HabiticaConfigEntry get_config_entry(HomeAssistant hass, str entry_id)
None async_setup_services(HomeAssistant hass)
None async_create_issue(HomeAssistant hass, str entry_id)