mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Migrate Habitica integration to habiticalib (#131032)
* Migrate data to habiticalib * Add habiticalib to init and coordinator * Migrate Habitica config flow to habiticalib * migrate init to habiticalib * migrate buttons to habiticalib * migrate switch to habiticalib * update habiticalib * cast_skill action * migrate update_score * migrate transformation items action * migrate quest actions * fix fixture errors * Migrate coordinator data and content * bump habiticalib * Remove habitipy and use wrapper in habiticalub * changes * some fixes * minor refactoring * class_needed annotation * Update diagnostics * do integration setup in coordinator setup * small changes * raise HomeAssistantError for TooManyRequestsError * fix docstring * update tests * changes to tests/snapshots * fix update_todo_item
This commit is contained in:
parent
4717eb3142
commit
0db07a033b
@ -1,27 +1,15 @@
|
||||
"""The habitica integration."""
|
||||
|
||||
from http import HTTPStatus
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from habitipy.aio import HabitipyAsync
|
||||
from habiticalib import Habitica
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
APPLICATION_NAME,
|
||||
CONF_API_KEY,
|
||||
CONF_NAME,
|
||||
CONF_URL,
|
||||
CONF_VERIFY_SSL,
|
||||
Platform,
|
||||
__version__,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import CONF_API_USER, DEVELOPER_ID, DOMAIN
|
||||
from .const import CONF_API_USER, DOMAIN, X_CLIENT
|
||||
from .coordinator import HabiticaDataUpdateCoordinator
|
||||
from .services import async_setup_services
|
||||
from .types import HabiticaConfigEntry
|
||||
@ -51,47 +39,17 @@ async def async_setup_entry(
|
||||
) -> bool:
|
||||
"""Set up habitica from a config entry."""
|
||||
|
||||
class HAHabitipyAsync(HabitipyAsync):
|
||||
"""Closure API class to hold session."""
|
||||
|
||||
def __call__(self, **kwargs):
|
||||
return super().__call__(websession, **kwargs)
|
||||
|
||||
def _make_headers(self) -> dict[str, str]:
|
||||
headers = super()._make_headers()
|
||||
headers.update(
|
||||
{"x-client": f"{DEVELOPER_ID} - {APPLICATION_NAME} {__version__}"}
|
||||
)
|
||||
return headers
|
||||
|
||||
websession = async_get_clientsession(
|
||||
session = async_get_clientsession(
|
||||
hass, verify_ssl=config_entry.data.get(CONF_VERIFY_SSL, True)
|
||||
)
|
||||
|
||||
api = await hass.async_add_executor_job(
|
||||
HAHabitipyAsync,
|
||||
{
|
||||
"url": config_entry.data[CONF_URL],
|
||||
"login": config_entry.data[CONF_API_USER],
|
||||
"password": config_entry.data[CONF_API_KEY],
|
||||
},
|
||||
api = Habitica(
|
||||
session,
|
||||
api_user=config_entry.data[CONF_API_USER],
|
||||
api_key=config_entry.data[CONF_API_KEY],
|
||||
url=config_entry.data[CONF_URL],
|
||||
x_client=X_CLIENT,
|
||||
)
|
||||
try:
|
||||
user = await api.user.get(userFields="profile")
|
||||
except ClientResponseError as e:
|
||||
if e.status == HTTPStatus.TOO_MANY_REQUESTS:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="setup_rate_limit_exception",
|
||||
) from e
|
||||
raise ConfigEntryNotReady(e) from e
|
||||
|
||||
if not config_entry.data.get(CONF_NAME):
|
||||
name = user["profile"]["name"]
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry,
|
||||
data={**config_entry.data, CONF_NAME: name},
|
||||
)
|
||||
|
||||
coordinator = HabiticaDataUpdateCoordinator(hass, api)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
@ -5,7 +5,8 @@ from __future__ import annotations
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
from typing import Any
|
||||
|
||||
from habiticalib import UserData
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntity,
|
||||
@ -23,8 +24,8 @@ from .types import HabiticaConfigEntry
|
||||
class HabiticaBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""Habitica Binary Sensor Description."""
|
||||
|
||||
value_fn: Callable[[dict[str, Any]], bool | None]
|
||||
entity_picture: Callable[[dict[str, Any]], str | None]
|
||||
value_fn: Callable[[UserData], bool | None]
|
||||
entity_picture: Callable[[UserData], str | None]
|
||||
|
||||
|
||||
class HabiticaBinarySensor(StrEnum):
|
||||
@ -33,10 +34,10 @@ class HabiticaBinarySensor(StrEnum):
|
||||
PENDING_QUEST = "pending_quest"
|
||||
|
||||
|
||||
def get_scroll_image_for_pending_quest_invitation(user: dict[str, Any]) -> str | None:
|
||||
def get_scroll_image_for_pending_quest_invitation(user: UserData) -> str | None:
|
||||
"""Entity picture for pending quest invitation."""
|
||||
if user["party"]["quest"].get("key") and user["party"]["quest"]["RSVPNeeded"]:
|
||||
return f"inventory_quest_scroll_{user["party"]["quest"]["key"]}.png"
|
||||
if user.party.quest.key and user.party.quest.RSVPNeeded:
|
||||
return f"inventory_quest_scroll_{user.party.quest.key}.png"
|
||||
return None
|
||||
|
||||
|
||||
@ -44,7 +45,7 @@ BINARY_SENSOR_DESCRIPTIONS: tuple[HabiticaBinarySensorEntityDescription, ...] =
|
||||
HabiticaBinarySensorEntityDescription(
|
||||
key=HabiticaBinarySensor.PENDING_QUEST,
|
||||
translation_key=HabiticaBinarySensor.PENDING_QUEST,
|
||||
value_fn=lambda user: user["party"]["quest"]["RSVPNeeded"],
|
||||
value_fn=lambda user: user.party.quest.RSVPNeeded,
|
||||
entity_picture=get_scroll_image_for_pending_quest_invitation,
|
||||
),
|
||||
)
|
||||
|
@ -5,10 +5,17 @@ from __future__ import annotations
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
from http import HTTPStatus
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from aiohttp import ClientError
|
||||
from habiticalib import (
|
||||
HabiticaClass,
|
||||
HabiticaException,
|
||||
NotAuthorizedError,
|
||||
Skill,
|
||||
TaskType,
|
||||
TooManyRequestsError,
|
||||
)
|
||||
|
||||
from homeassistant.components.button import (
|
||||
DOMAIN as BUTTON_DOMAIN,
|
||||
@ -20,7 +27,7 @@ from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import ASSETS_URL, DOMAIN, HEALER, MAGE, ROGUE, WARRIOR
|
||||
from .const import ASSETS_URL, DOMAIN
|
||||
from .coordinator import HabiticaData, HabiticaDataUpdateCoordinator
|
||||
from .entity import HabiticaBase
|
||||
from .types import HabiticaConfigEntry
|
||||
@ -34,7 +41,7 @@ class HabiticaButtonEntityDescription(ButtonEntityDescription):
|
||||
|
||||
press_fn: Callable[[HabiticaDataUpdateCoordinator], Any]
|
||||
available_fn: Callable[[HabiticaData], bool]
|
||||
class_needed: str | None = None
|
||||
class_needed: HabiticaClass | None = None
|
||||
entity_picture: str | None = None
|
||||
|
||||
|
||||
@ -63,35 +70,33 @@ BUTTON_DESCRIPTIONS: tuple[HabiticaButtonEntityDescription, ...] = (
|
||||
HabiticaButtonEntityDescription(
|
||||
key=HabitipyButtonEntity.RUN_CRON,
|
||||
translation_key=HabitipyButtonEntity.RUN_CRON,
|
||||
press_fn=lambda coordinator: coordinator.api.cron.post(),
|
||||
available_fn=lambda data: data.user["needsCron"],
|
||||
press_fn=lambda coordinator: coordinator.habitica.run_cron(),
|
||||
available_fn=lambda data: data.user.needsCron is True,
|
||||
),
|
||||
HabiticaButtonEntityDescription(
|
||||
key=HabitipyButtonEntity.BUY_HEALTH_POTION,
|
||||
translation_key=HabitipyButtonEntity.BUY_HEALTH_POTION,
|
||||
press_fn=(
|
||||
lambda coordinator: coordinator.api["user"]["buy-health-potion"].post()
|
||||
),
|
||||
press_fn=lambda coordinator: coordinator.habitica.buy_health_potion(),
|
||||
available_fn=(
|
||||
lambda data: data.user["stats"]["gp"] >= 25
|
||||
and data.user["stats"]["hp"] < 50
|
||||
lambda data: (data.user.stats.gp or 0) >= 25
|
||||
and (data.user.stats.hp or 0) < 50
|
||||
),
|
||||
entity_picture="shop_potion.png",
|
||||
),
|
||||
HabiticaButtonEntityDescription(
|
||||
key=HabitipyButtonEntity.ALLOCATE_ALL_STAT_POINTS,
|
||||
translation_key=HabitipyButtonEntity.ALLOCATE_ALL_STAT_POINTS,
|
||||
press_fn=lambda coordinator: coordinator.api["user"]["allocate-now"].post(),
|
||||
press_fn=lambda coordinator: coordinator.habitica.allocate_stat_points(),
|
||||
available_fn=(
|
||||
lambda data: data.user["preferences"].get("automaticAllocation") is True
|
||||
and data.user["stats"]["points"] > 0
|
||||
lambda data: data.user.preferences.automaticAllocation is True
|
||||
and (data.user.stats.points or 0) > 0
|
||||
),
|
||||
),
|
||||
HabiticaButtonEntityDescription(
|
||||
key=HabitipyButtonEntity.REVIVE,
|
||||
translation_key=HabitipyButtonEntity.REVIVE,
|
||||
press_fn=lambda coordinator: coordinator.api["user"]["revive"].post(),
|
||||
available_fn=lambda data: data.user["stats"]["hp"] == 0,
|
||||
press_fn=lambda coordinator: coordinator.habitica.revive(),
|
||||
available_fn=lambda data: data.user.stats.hp == 0,
|
||||
),
|
||||
)
|
||||
|
||||
@ -100,166 +105,170 @@ CLASS_SKILLS: tuple[HabiticaButtonEntityDescription, ...] = (
|
||||
HabiticaButtonEntityDescription(
|
||||
key=HabitipyButtonEntity.MPHEAL,
|
||||
translation_key=HabitipyButtonEntity.MPHEAL,
|
||||
press_fn=lambda coordinator: coordinator.api.user.class_.cast["mpheal"].post(),
|
||||
available_fn=(
|
||||
lambda data: data.user["stats"]["lvl"] >= 12
|
||||
and data.user["stats"]["mp"] >= 30
|
||||
press_fn=(
|
||||
lambda coordinator: coordinator.habitica.cast_skill(Skill.ETHEREAL_SURGE)
|
||||
),
|
||||
class_needed=MAGE,
|
||||
available_fn=(
|
||||
lambda data: (data.user.stats.lvl or 0) >= 12
|
||||
and (data.user.stats.mp or 0) >= 30
|
||||
),
|
||||
class_needed=HabiticaClass.MAGE,
|
||||
entity_picture="shop_mpheal.png",
|
||||
),
|
||||
HabiticaButtonEntityDescription(
|
||||
key=HabitipyButtonEntity.EARTH,
|
||||
translation_key=HabitipyButtonEntity.EARTH,
|
||||
press_fn=lambda coordinator: coordinator.api.user.class_.cast["earth"].post(),
|
||||
press_fn=lambda coordinator: coordinator.habitica.cast_skill(Skill.EARTHQUAKE),
|
||||
available_fn=(
|
||||
lambda data: data.user["stats"]["lvl"] >= 13
|
||||
and data.user["stats"]["mp"] >= 35
|
||||
lambda data: (data.user.stats.lvl or 0) >= 13
|
||||
and (data.user.stats.mp or 0) >= 35
|
||||
),
|
||||
class_needed=MAGE,
|
||||
class_needed=HabiticaClass.MAGE,
|
||||
entity_picture="shop_earth.png",
|
||||
),
|
||||
HabiticaButtonEntityDescription(
|
||||
key=HabitipyButtonEntity.FROST,
|
||||
translation_key=HabitipyButtonEntity.FROST,
|
||||
press_fn=lambda coordinator: coordinator.api.user.class_.cast["frost"].post(),
|
||||
press_fn=(
|
||||
lambda coordinator: coordinator.habitica.cast_skill(Skill.CHILLING_FROST)
|
||||
),
|
||||
# chilling frost can only be cast once per day (streaks buff is false)
|
||||
available_fn=(
|
||||
lambda data: data.user["stats"]["lvl"] >= 14
|
||||
and data.user["stats"]["mp"] >= 40
|
||||
and not data.user["stats"]["buffs"]["streaks"]
|
||||
lambda data: (data.user.stats.lvl or 0) >= 14
|
||||
and (data.user.stats.mp or 0) >= 40
|
||||
and not data.user.stats.buffs.streaks
|
||||
),
|
||||
class_needed=MAGE,
|
||||
class_needed=HabiticaClass.MAGE,
|
||||
entity_picture="shop_frost.png",
|
||||
),
|
||||
HabiticaButtonEntityDescription(
|
||||
key=HabitipyButtonEntity.DEFENSIVE_STANCE,
|
||||
translation_key=HabitipyButtonEntity.DEFENSIVE_STANCE,
|
||||
press_fn=(
|
||||
lambda coordinator: coordinator.api.user.class_.cast[
|
||||
"defensiveStance"
|
||||
].post()
|
||||
lambda coordinator: coordinator.habitica.cast_skill(Skill.DEFENSIVE_STANCE)
|
||||
),
|
||||
available_fn=(
|
||||
lambda data: data.user["stats"]["lvl"] >= 12
|
||||
and data.user["stats"]["mp"] >= 25
|
||||
lambda data: (data.user.stats.lvl or 0) >= 12
|
||||
and (data.user.stats.mp or 0) >= 25
|
||||
),
|
||||
class_needed=WARRIOR,
|
||||
class_needed=HabiticaClass.WARRIOR,
|
||||
entity_picture="shop_defensiveStance.png",
|
||||
),
|
||||
HabiticaButtonEntityDescription(
|
||||
key=HabitipyButtonEntity.VALOROUS_PRESENCE,
|
||||
translation_key=HabitipyButtonEntity.VALOROUS_PRESENCE,
|
||||
press_fn=(
|
||||
lambda coordinator: coordinator.api.user.class_.cast[
|
||||
"valorousPresence"
|
||||
].post()
|
||||
lambda coordinator: coordinator.habitica.cast_skill(Skill.VALOROUS_PRESENCE)
|
||||
),
|
||||
available_fn=(
|
||||
lambda data: data.user["stats"]["lvl"] >= 13
|
||||
and data.user["stats"]["mp"] >= 20
|
||||
lambda data: (data.user.stats.lvl or 0) >= 13
|
||||
and (data.user.stats.mp or 0) >= 20
|
||||
),
|
||||
class_needed=WARRIOR,
|
||||
class_needed=HabiticaClass.WARRIOR,
|
||||
entity_picture="shop_valorousPresence.png",
|
||||
),
|
||||
HabiticaButtonEntityDescription(
|
||||
key=HabitipyButtonEntity.INTIMIDATE,
|
||||
translation_key=HabitipyButtonEntity.INTIMIDATE,
|
||||
press_fn=(
|
||||
lambda coordinator: coordinator.api.user.class_.cast["intimidate"].post()
|
||||
lambda coordinator: coordinator.habitica.cast_skill(Skill.INTIMIDATING_GAZE)
|
||||
),
|
||||
available_fn=(
|
||||
lambda data: data.user["stats"]["lvl"] >= 14
|
||||
and data.user["stats"]["mp"] >= 15
|
||||
lambda data: (data.user.stats.lvl or 0) >= 14
|
||||
and (data.user.stats.mp or 0) >= 15
|
||||
),
|
||||
class_needed=WARRIOR,
|
||||
class_needed=HabiticaClass.WARRIOR,
|
||||
entity_picture="shop_intimidate.png",
|
||||
),
|
||||
HabiticaButtonEntityDescription(
|
||||
key=HabitipyButtonEntity.TOOLS_OF_TRADE,
|
||||
translation_key=HabitipyButtonEntity.TOOLS_OF_TRADE,
|
||||
press_fn=(
|
||||
lambda coordinator: coordinator.api.user.class_.cast["toolsOfTrade"].post()
|
||||
lambda coordinator: coordinator.habitica.cast_skill(
|
||||
Skill.TOOLS_OF_THE_TRADE
|
||||
)
|
||||
),
|
||||
available_fn=(
|
||||
lambda data: data.user["stats"]["lvl"] >= 13
|
||||
and data.user["stats"]["mp"] >= 25
|
||||
lambda data: (data.user.stats.lvl or 0) >= 13
|
||||
and (data.user.stats.mp or 0) >= 25
|
||||
),
|
||||
class_needed=ROGUE,
|
||||
class_needed=HabiticaClass.ROGUE,
|
||||
entity_picture="shop_toolsOfTrade.png",
|
||||
),
|
||||
HabiticaButtonEntityDescription(
|
||||
key=HabitipyButtonEntity.STEALTH,
|
||||
translation_key=HabitipyButtonEntity.STEALTH,
|
||||
press_fn=(
|
||||
lambda coordinator: coordinator.api.user.class_.cast["stealth"].post()
|
||||
),
|
||||
press_fn=lambda coordinator: coordinator.habitica.cast_skill(Skill.STEALTH),
|
||||
# Stealth buffs stack and it can only be cast if the amount of
|
||||
# unfinished dailies is smaller than the amount of buffs
|
||||
# buffs is smaller than the amount of unfinished dailies
|
||||
available_fn=(
|
||||
lambda data: data.user["stats"]["lvl"] >= 14
|
||||
and data.user["stats"]["mp"] >= 45
|
||||
and data.user["stats"]["buffs"]["stealth"]
|
||||
lambda data: (data.user.stats.lvl or 0) >= 14
|
||||
and (data.user.stats.mp or 0) >= 45
|
||||
and (data.user.stats.buffs.stealth or 0)
|
||||
< len(
|
||||
[
|
||||
r
|
||||
for r in data.tasks
|
||||
if r.get("type") == "daily"
|
||||
and r.get("isDue") is True
|
||||
and r.get("completed") is False
|
||||
if r.Type is TaskType.DAILY
|
||||
and r.isDue is True
|
||||
and r.completed is False
|
||||
]
|
||||
)
|
||||
),
|
||||
class_needed=ROGUE,
|
||||
class_needed=HabiticaClass.ROGUE,
|
||||
entity_picture="shop_stealth.png",
|
||||
),
|
||||
HabiticaButtonEntityDescription(
|
||||
key=HabitipyButtonEntity.HEAL,
|
||||
translation_key=HabitipyButtonEntity.HEAL,
|
||||
press_fn=lambda coordinator: coordinator.api.user.class_.cast["heal"].post(),
|
||||
available_fn=(
|
||||
lambda data: data.user["stats"]["lvl"] >= 11
|
||||
and data.user["stats"]["mp"] >= 15
|
||||
and data.user["stats"]["hp"] < 50
|
||||
press_fn=(
|
||||
lambda coordinator: coordinator.habitica.cast_skill(Skill.HEALING_LIGHT)
|
||||
),
|
||||
class_needed=HEALER,
|
||||
available_fn=(
|
||||
lambda data: (data.user.stats.lvl or 0) >= 11
|
||||
and (data.user.stats.mp or 0) >= 15
|
||||
and (data.user.stats.hp or 0) < 50
|
||||
),
|
||||
class_needed=HabiticaClass.HEALER,
|
||||
entity_picture="shop_heal.png",
|
||||
),
|
||||
HabiticaButtonEntityDescription(
|
||||
key=HabitipyButtonEntity.BRIGHTNESS,
|
||||
translation_key=HabitipyButtonEntity.BRIGHTNESS,
|
||||
press_fn=(
|
||||
lambda coordinator: coordinator.api.user.class_.cast["brightness"].post()
|
||||
lambda coordinator: coordinator.habitica.cast_skill(
|
||||
Skill.SEARING_BRIGHTNESS
|
||||
)
|
||||
),
|
||||
available_fn=(
|
||||
lambda data: data.user["stats"]["lvl"] >= 12
|
||||
and data.user["stats"]["mp"] >= 15
|
||||
lambda data: (data.user.stats.lvl or 0) >= 12
|
||||
and (data.user.stats.mp or 0) >= 15
|
||||
),
|
||||
class_needed=HEALER,
|
||||
class_needed=HabiticaClass.HEALER,
|
||||
entity_picture="shop_brightness.png",
|
||||
),
|
||||
HabiticaButtonEntityDescription(
|
||||
key=HabitipyButtonEntity.PROTECT_AURA,
|
||||
translation_key=HabitipyButtonEntity.PROTECT_AURA,
|
||||
press_fn=(
|
||||
lambda coordinator: coordinator.api.user.class_.cast["protectAura"].post()
|
||||
lambda coordinator: coordinator.habitica.cast_skill(Skill.PROTECTIVE_AURA)
|
||||
),
|
||||
available_fn=(
|
||||
lambda data: data.user["stats"]["lvl"] >= 13
|
||||
and data.user["stats"]["mp"] >= 30
|
||||
lambda data: (data.user.stats.lvl or 0) >= 13
|
||||
and (data.user.stats.mp or 0) >= 30
|
||||
),
|
||||
class_needed=HEALER,
|
||||
class_needed=HabiticaClass.HEALER,
|
||||
entity_picture="shop_protectAura.png",
|
||||
),
|
||||
HabiticaButtonEntityDescription(
|
||||
key=HabitipyButtonEntity.HEAL_ALL,
|
||||
translation_key=HabitipyButtonEntity.HEAL_ALL,
|
||||
press_fn=lambda coordinator: coordinator.api.user.class_.cast["healAll"].post(),
|
||||
press_fn=lambda coordinator: coordinator.habitica.cast_skill(Skill.BLESSING),
|
||||
available_fn=(
|
||||
lambda data: data.user["stats"]["lvl"] >= 14
|
||||
and data.user["stats"]["mp"] >= 25
|
||||
lambda data: (data.user.stats.lvl or 0) >= 14
|
||||
and (data.user.stats.mp or 0) >= 25
|
||||
),
|
||||
class_needed=HEALER,
|
||||
class_needed=HabiticaClass.HEALER,
|
||||
entity_picture="shop_healAll.png",
|
||||
),
|
||||
)
|
||||
@ -285,10 +294,10 @@ async def async_setup_entry(
|
||||
|
||||
for description in CLASS_SKILLS:
|
||||
if (
|
||||
coordinator.data.user["stats"]["lvl"] >= 10
|
||||
and coordinator.data.user["flags"]["classSelected"]
|
||||
and not coordinator.data.user["preferences"]["disableClasses"]
|
||||
and description.class_needed == coordinator.data.user["stats"]["class"]
|
||||
(coordinator.data.user.stats.lvl or 0) >= 10
|
||||
and coordinator.data.user.flags.classSelected
|
||||
and not coordinator.data.user.preferences.disableClasses
|
||||
and description.class_needed is coordinator.data.user.stats.Class
|
||||
):
|
||||
if description.key not in skills_added:
|
||||
buttons.append(HabiticaButton(coordinator, description))
|
||||
@ -322,17 +331,17 @@ class HabiticaButton(HabiticaBase, ButtonEntity):
|
||||
"""Handle the button press."""
|
||||
try:
|
||||
await self.entity_description.press_fn(self.coordinator)
|
||||
except ClientResponseError as e:
|
||||
if e.status == HTTPStatus.TOO_MANY_REQUESTS:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="setup_rate_limit_exception",
|
||||
) from e
|
||||
if e.status == HTTPStatus.UNAUTHORIZED:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="service_call_unallowed",
|
||||
) from e
|
||||
except TooManyRequestsError as e:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="setup_rate_limit_exception",
|
||||
) from e
|
||||
except NotAuthorizedError as e:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="service_call_unallowed",
|
||||
) from e
|
||||
except (HabiticaException, ClientError) as e:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="service_call_exception",
|
||||
|
@ -5,8 +5,11 @@ from __future__ import annotations
|
||||
from abc import abstractmethod
|
||||
from datetime import date, datetime, timedelta
|
||||
from enum import StrEnum
|
||||
from typing import TYPE_CHECKING
|
||||
from uuid import UUID
|
||||
|
||||
from dateutil.rrule import rrule
|
||||
from habiticalib import TaskType
|
||||
|
||||
from homeassistant.components.calendar import (
|
||||
CalendarEntity,
|
||||
@ -20,7 +23,6 @@ from homeassistant.util import dt as dt_util
|
||||
from . import HabiticaConfigEntry
|
||||
from .coordinator import HabiticaDataUpdateCoordinator
|
||||
from .entity import HabiticaBase
|
||||
from .types import HabiticaTaskType
|
||||
from .util import build_rrule, get_recurrence_rule
|
||||
|
||||
|
||||
@ -83,9 +85,7 @@ class HabiticaCalendarEntity(HabiticaBase, CalendarEntity):
|
||||
@property
|
||||
def start_of_today(self) -> datetime:
|
||||
"""Habitica daystart."""
|
||||
return dt_util.start_of_local_day(
|
||||
datetime.fromisoformat(self.coordinator.data.user["lastCron"])
|
||||
)
|
||||
return dt_util.start_of_local_day(self.coordinator.data.user.lastCron)
|
||||
|
||||
def get_recurrence_dates(
|
||||
self, recurrences: rrule, start_date: datetime, end_date: datetime | None = None
|
||||
@ -115,13 +115,13 @@ class HabiticaTodosCalendarEntity(HabiticaCalendarEntity):
|
||||
events = []
|
||||
for task in self.coordinator.data.tasks:
|
||||
if not (
|
||||
task["type"] == HabiticaTaskType.TODO
|
||||
and not task["completed"]
|
||||
and task.get("date") # only if has due date
|
||||
task.Type is TaskType.TODO
|
||||
and not task.completed
|
||||
and task.date is not None # only if has due date
|
||||
):
|
||||
continue
|
||||
|
||||
start = dt_util.start_of_local_day(datetime.fromisoformat(task["date"]))
|
||||
start = dt_util.start_of_local_day(task.date)
|
||||
end = start + timedelta(days=1)
|
||||
# return current and upcoming events or events within the requested range
|
||||
|
||||
@ -132,21 +132,23 @@ class HabiticaTodosCalendarEntity(HabiticaCalendarEntity):
|
||||
if end_date and start > end_date:
|
||||
# Event starts after date range
|
||||
continue
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert task.text
|
||||
assert task.id
|
||||
events.append(
|
||||
CalendarEvent(
|
||||
start=start.date(),
|
||||
end=end.date(),
|
||||
summary=task["text"],
|
||||
description=task["notes"],
|
||||
uid=task["id"],
|
||||
summary=task.text,
|
||||
description=task.notes,
|
||||
uid=str(task.id),
|
||||
)
|
||||
)
|
||||
return sorted(
|
||||
events,
|
||||
key=lambda event: (
|
||||
event.start,
|
||||
self.coordinator.data.user["tasksOrder"]["todos"].index(event.uid),
|
||||
self.coordinator.data.user.tasksOrder.todos.index(UUID(event.uid)),
|
||||
),
|
||||
)
|
||||
|
||||
@ -189,7 +191,7 @@ class HabiticaDailiesCalendarEntity(HabiticaCalendarEntity):
|
||||
events = []
|
||||
for task in self.coordinator.data.tasks:
|
||||
# only dailies that that are not 'grey dailies'
|
||||
if not (task["type"] == HabiticaTaskType.DAILY and task["everyX"]):
|
||||
if not (task.Type is TaskType.DAILY and task.everyX):
|
||||
continue
|
||||
|
||||
recurrences = build_rrule(task)
|
||||
@ -199,19 +201,21 @@ class HabiticaDailiesCalendarEntity(HabiticaCalendarEntity):
|
||||
for recurrence in recurrence_dates:
|
||||
is_future_event = recurrence > self.start_of_today
|
||||
is_current_event = (
|
||||
recurrence <= self.start_of_today and not task["completed"]
|
||||
recurrence <= self.start_of_today and not task.completed
|
||||
)
|
||||
|
||||
if not is_future_event and not is_current_event:
|
||||
continue
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert task.text
|
||||
assert task.id
|
||||
events.append(
|
||||
CalendarEvent(
|
||||
start=recurrence.date(),
|
||||
end=self.end_date(recurrence, end_date),
|
||||
summary=task["text"],
|
||||
description=task["notes"],
|
||||
uid=task["id"],
|
||||
summary=task.text,
|
||||
description=task.notes,
|
||||
uid=str(task.id),
|
||||
rrule=get_recurrence_rule(recurrences),
|
||||
)
|
||||
)
|
||||
@ -219,7 +223,7 @@ class HabiticaDailiesCalendarEntity(HabiticaCalendarEntity):
|
||||
events,
|
||||
key=lambda event: (
|
||||
event.start,
|
||||
self.coordinator.data.user["tasksOrder"]["dailys"].index(event.uid),
|
||||
self.coordinator.data.user.tasksOrder.dailys.index(UUID(event.uid)),
|
||||
),
|
||||
)
|
||||
|
||||
@ -254,14 +258,14 @@ class HabiticaTodoRemindersCalendarEntity(HabiticaCalendarEntity):
|
||||
events = []
|
||||
|
||||
for task in self.coordinator.data.tasks:
|
||||
if task["type"] != HabiticaTaskType.TODO or task["completed"]:
|
||||
if task.Type is not TaskType.TODO or task.completed:
|
||||
continue
|
||||
|
||||
for reminder in task.get("reminders", []):
|
||||
for reminder in task.reminders:
|
||||
# reminders are returned by the API in local time but with wrong
|
||||
# timezone (UTC) and arbitrary added seconds/microseconds. When
|
||||
# creating reminders in Habitica only hours and minutes can be defined.
|
||||
start = datetime.fromisoformat(reminder["time"]).replace(
|
||||
start = reminder.time.replace(
|
||||
tzinfo=dt_util.DEFAULT_TIME_ZONE, second=0, microsecond=0
|
||||
)
|
||||
end = start + timedelta(hours=1)
|
||||
@ -273,14 +277,16 @@ class HabiticaTodoRemindersCalendarEntity(HabiticaCalendarEntity):
|
||||
if end_date and start > end_date:
|
||||
# Event starts after date range
|
||||
continue
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert task.text
|
||||
assert task.id
|
||||
events.append(
|
||||
CalendarEvent(
|
||||
start=start,
|
||||
end=end,
|
||||
summary=task["text"],
|
||||
description=task["notes"],
|
||||
uid=f"{task["id"]}_{reminder["id"]}",
|
||||
summary=task.text,
|
||||
description=task.notes,
|
||||
uid=f"{task.id}_{reminder.id}",
|
||||
)
|
||||
)
|
||||
|
||||
@ -298,7 +304,7 @@ class HabiticaDailyRemindersCalendarEntity(HabiticaCalendarEntity):
|
||||
translation_key=HabiticaCalendar.DAILY_REMINDERS,
|
||||
)
|
||||
|
||||
def start(self, reminder_time: str, reminder_date: date) -> datetime:
|
||||
def start(self, reminder_time: datetime, reminder_date: date) -> datetime:
|
||||
"""Generate reminder times for dailies.
|
||||
|
||||
Reminders for dailies have a datetime but the date part is arbitrary,
|
||||
@ -307,12 +313,10 @@ class HabiticaDailyRemindersCalendarEntity(HabiticaCalendarEntity):
|
||||
"""
|
||||
return datetime.combine(
|
||||
reminder_date,
|
||||
datetime.fromisoformat(reminder_time)
|
||||
.replace(
|
||||
reminder_time.replace(
|
||||
second=0,
|
||||
microsecond=0,
|
||||
)
|
||||
.time(),
|
||||
).time(),
|
||||
tzinfo=dt_util.DEFAULT_TIME_ZONE,
|
||||
)
|
||||
|
||||
@ -327,7 +331,7 @@ class HabiticaDailyRemindersCalendarEntity(HabiticaCalendarEntity):
|
||||
start_date = max(start_date, self.start_of_today)
|
||||
|
||||
for task in self.coordinator.data.tasks:
|
||||
if not (task["type"] == HabiticaTaskType.DAILY and task["everyX"]):
|
||||
if not (task.Type is TaskType.DAILY and task.everyX):
|
||||
continue
|
||||
|
||||
recurrences = build_rrule(task)
|
||||
@ -339,27 +343,30 @@ class HabiticaDailyRemindersCalendarEntity(HabiticaCalendarEntity):
|
||||
for recurrence in recurrence_dates:
|
||||
is_future_event = recurrence > self.start_of_today
|
||||
is_current_event = (
|
||||
recurrence <= self.start_of_today and not task["completed"]
|
||||
recurrence <= self.start_of_today and not task.completed
|
||||
)
|
||||
|
||||
if not is_future_event and not is_current_event:
|
||||
continue
|
||||
|
||||
for reminder in task.get("reminders", []):
|
||||
start = self.start(reminder["time"], recurrence)
|
||||
for reminder in task.reminders:
|
||||
start = self.start(reminder.time, recurrence)
|
||||
end = start + timedelta(hours=1)
|
||||
|
||||
if end < start_date:
|
||||
# Event ends before date range
|
||||
continue
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert task.id
|
||||
assert task.text
|
||||
events.append(
|
||||
CalendarEvent(
|
||||
start=start,
|
||||
end=end,
|
||||
summary=task["text"],
|
||||
description=task["notes"],
|
||||
uid=f"{task["id"]}_{reminder["id"]}",
|
||||
summary=task.text,
|
||||
description=task.notes,
|
||||
uid=f"{task.id}_{reminder.id}",
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -2,17 +2,17 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from habitipy.aio import HabitipyAsync
|
||||
from aiohttp import ClientError
|
||||
from habiticalib import Habitica, HabiticaException, NotAuthorizedError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_NAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_URL,
|
||||
CONF_USERNAME,
|
||||
@ -33,6 +33,7 @@ from .const import (
|
||||
HABITICANS_URL,
|
||||
SIGN_UP_URL,
|
||||
SITE_DATA_URL,
|
||||
X_CLIENT,
|
||||
)
|
||||
|
||||
STEP_ADVANCED_DATA_SCHEMA = vol.Schema(
|
||||
@ -93,39 +94,33 @@ class HabiticaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
session = async_get_clientsession(self.hass)
|
||||
api = Habitica(session=session, x_client=X_CLIENT)
|
||||
try:
|
||||
session = async_get_clientsession(self.hass)
|
||||
api = await self.hass.async_add_executor_job(
|
||||
HabitipyAsync,
|
||||
{
|
||||
"login": "",
|
||||
"password": "",
|
||||
"url": DEFAULT_URL,
|
||||
},
|
||||
)
|
||||
login_response = await api.user.auth.local.login.post(
|
||||
session=session,
|
||||
login = await api.login(
|
||||
username=user_input[CONF_USERNAME],
|
||||
password=user_input[CONF_PASSWORD],
|
||||
)
|
||||
user = await api.get_user(user_fields="profile")
|
||||
|
||||
except ClientResponseError as ex:
|
||||
if ex.status == HTTPStatus.UNAUTHORIZED:
|
||||
errors["base"] = "invalid_auth"
|
||||
else:
|
||||
errors["base"] = "cannot_connect"
|
||||
except NotAuthorizedError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except (HabiticaException, ClientError):
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(login_response["id"])
|
||||
await self.async_set_unique_id(str(login.data.id))
|
||||
self._abort_if_unique_id_configured()
|
||||
if TYPE_CHECKING:
|
||||
assert user.data.profile.name
|
||||
return self.async_create_entry(
|
||||
title=login_response["username"],
|
||||
title=user.data.profile.name,
|
||||
data={
|
||||
CONF_API_USER: login_response["id"],
|
||||
CONF_API_KEY: login_response["apiToken"],
|
||||
CONF_USERNAME: login_response["username"],
|
||||
CONF_API_USER: str(login.data.id),
|
||||
CONF_API_KEY: login.data.apiToken,
|
||||
CONF_NAME: user.data.profile.name, # needed for api_call action
|
||||
CONF_URL: DEFAULT_URL,
|
||||
CONF_VERIFY_SSL: True,
|
||||
},
|
||||
@ -150,36 +145,37 @@ class HabiticaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
session = async_get_clientsession(
|
||||
self.hass, verify_ssl=user_input[CONF_VERIFY_SSL]
|
||||
)
|
||||
try:
|
||||
session = async_get_clientsession(
|
||||
self.hass, verify_ssl=user_input.get(CONF_VERIFY_SSL, True)
|
||||
)
|
||||
api = await self.hass.async_add_executor_job(
|
||||
HabitipyAsync,
|
||||
{
|
||||
"login": user_input[CONF_API_USER],
|
||||
"password": user_input[CONF_API_KEY],
|
||||
"url": user_input.get(CONF_URL, DEFAULT_URL),
|
||||
},
|
||||
)
|
||||
api_response = await api.user.get(
|
||||
api = Habitica(
|
||||
session=session,
|
||||
userFields="auth",
|
||||
x_client=X_CLIENT,
|
||||
api_user=user_input[CONF_API_USER],
|
||||
api_key=user_input[CONF_API_KEY],
|
||||
url=user_input.get(CONF_URL, DEFAULT_URL),
|
||||
)
|
||||
except ClientResponseError as ex:
|
||||
if ex.status == HTTPStatus.UNAUTHORIZED:
|
||||
errors["base"] = "invalid_auth"
|
||||
else:
|
||||
errors["base"] = "cannot_connect"
|
||||
user = await api.get_user(user_fields="profile")
|
||||
except NotAuthorizedError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except (HabiticaException, ClientError):
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(user_input[CONF_API_USER])
|
||||
self._abort_if_unique_id_configured()
|
||||
user_input[CONF_USERNAME] = api_response["auth"]["local"]["username"]
|
||||
if TYPE_CHECKING:
|
||||
assert user.data.profile.name
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_USERNAME], data=user_input
|
||||
title=user.data.profile.name,
|
||||
data={
|
||||
**user_input,
|
||||
CONF_URL: user_input.get(CONF_URL, DEFAULT_URL),
|
||||
CONF_NAME: user.data.profile.name, # needed for api_call action
|
||||
},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""Constants for the habitica integration."""
|
||||
|
||||
from homeassistant.const import CONF_PATH
|
||||
from homeassistant.const import APPLICATION_NAME, CONF_PATH, __version__
|
||||
|
||||
CONF_API_USER = "api_user"
|
||||
|
||||
@ -44,9 +44,5 @@ SERVICE_SCORE_REWARD = "score_reward"
|
||||
SERVICE_TRANSFORMATION = "transformation"
|
||||
|
||||
|
||||
WARRIOR = "warrior"
|
||||
ROGUE = "rogue"
|
||||
HEALER = "healer"
|
||||
MAGE = "wizard"
|
||||
|
||||
DEVELOPER_ID = "4c4ca53f-c059-4ffa-966e-9d29dd405daf"
|
||||
X_CLIENT = f"{DEVELOPER_ID} - {APPLICATION_NAME} {__version__}"
|
||||
|
@ -5,16 +5,25 @@ from __future__ import annotations
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from habitipy.aio import HabitipyAsync
|
||||
from aiohttp import ClientError
|
||||
from habiticalib import (
|
||||
ContentData,
|
||||
Habitica,
|
||||
HabiticaException,
|
||||
NotAuthorizedError,
|
||||
TaskData,
|
||||
TaskFilter,
|
||||
TooManyRequestsError,
|
||||
UserData,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||
from homeassistant.helpers.debounce import Debouncer
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
@ -25,10 +34,10 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@dataclass
|
||||
class HabiticaData:
|
||||
"""Coordinator data class."""
|
||||
"""Habitica data."""
|
||||
|
||||
user: dict[str, Any]
|
||||
tasks: list[dict]
|
||||
user: UserData
|
||||
tasks: list[TaskData]
|
||||
|
||||
|
||||
class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]):
|
||||
@ -36,7 +45,7 @@ class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]):
|
||||
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, habitipy: HabitipyAsync) -> None:
|
||||
def __init__(self, hass: HomeAssistant, habitica: Habitica) -> None:
|
||||
"""Initialize the Habitica data coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
@ -50,25 +59,54 @@ class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]):
|
||||
immediate=False,
|
||||
),
|
||||
)
|
||||
self.api = habitipy
|
||||
self.content: dict[str, Any] = {}
|
||||
self.habitica = habitica
|
||||
self.content: ContentData
|
||||
|
||||
async def _async_setup(self) -> None:
|
||||
"""Set up Habitica integration."""
|
||||
|
||||
try:
|
||||
user = await self.habitica.get_user()
|
||||
self.content = (
|
||||
await self.habitica.get_content(user.data.preferences.language)
|
||||
).data
|
||||
except NotAuthorizedError as e:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_auth",
|
||||
translation_placeholders={"account": self.config_entry.title},
|
||||
) from e
|
||||
except TooManyRequestsError as e:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="setup_rate_limit_exception",
|
||||
) from e
|
||||
except (HabiticaException, ClientError) as e:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="service_call_exception",
|
||||
) from e
|
||||
|
||||
if not self.config_entry.data.get(CONF_NAME):
|
||||
self.hass.config_entries.async_update_entry(
|
||||
self.config_entry,
|
||||
data={**self.config_entry.data, CONF_NAME: user.data.profile.name},
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> HabiticaData:
|
||||
try:
|
||||
user_response = await self.api.user.get()
|
||||
tasks_response = await self.api.tasks.user.get()
|
||||
tasks_response.extend(await self.api.tasks.user.get(type="completedTodos"))
|
||||
if not self.content:
|
||||
self.content = await self.api.content.get(
|
||||
language=user_response["preferences"]["language"]
|
||||
)
|
||||
except ClientResponseError as error:
|
||||
if error.status == HTTPStatus.TOO_MANY_REQUESTS:
|
||||
_LOGGER.debug("Rate limit exceeded, will try again later")
|
||||
return self.data
|
||||
raise UpdateFailed(f"Unable to connect to Habitica: {error}") from error
|
||||
|
||||
return HabiticaData(user=user_response, tasks=tasks_response)
|
||||
user = (await self.habitica.get_user()).data
|
||||
tasks = (await self.habitica.get_tasks()).data
|
||||
completed_todos = (
|
||||
await self.habitica.get_tasks(TaskFilter.COMPLETED_TODOS)
|
||||
).data
|
||||
except TooManyRequestsError:
|
||||
_LOGGER.debug("Rate limit exceeded, will try again later")
|
||||
return self.data
|
||||
except (HabiticaException, ClientError) as e:
|
||||
raise UpdateFailed(f"Unable to connect to Habitica: {e}") from e
|
||||
else:
|
||||
return HabiticaData(user=user, tasks=tasks + completed_todos)
|
||||
|
||||
async def execute(
|
||||
self, func: Callable[[HabiticaDataUpdateCoordinator], Any]
|
||||
@ -77,12 +115,12 @@ class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]):
|
||||
|
||||
try:
|
||||
await func(self)
|
||||
except ClientResponseError as e:
|
||||
if e.status == HTTPStatus.TOO_MANY_REQUESTS:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="setup_rate_limit_exception",
|
||||
) from e
|
||||
except TooManyRequestsError as e:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="setup_rate_limit_exception",
|
||||
) from e
|
||||
except (HabiticaException, ClientError) as e:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="service_call_exception",
|
||||
|
@ -16,12 +16,12 @@ async def async_get_config_entry_diagnostics(
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
|
||||
habitica_data = await config_entry.runtime_data.api.user.anonymized.get()
|
||||
habitica_data = await config_entry.runtime_data.habitica.get_user_anonymized()
|
||||
|
||||
return {
|
||||
"config_entry_data": {
|
||||
CONF_URL: config_entry.data[CONF_URL],
|
||||
CONF_API_USER: config_entry.data[CONF_API_USER],
|
||||
},
|
||||
"habitica_data": habitica_data,
|
||||
"habitica_data": habitica_data.to_dict()["data"],
|
||||
}
|
||||
|
@ -5,6 +5,6 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/habitica",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["habitipy", "plumbum"],
|
||||
"requirements": ["habitipy==0.3.3"]
|
||||
"loggers": ["habiticalib"],
|
||||
"requirements": ["habiticalib==0.3.1"]
|
||||
}
|
||||
|
@ -3,11 +3,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Mapping
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import asdict, dataclass
|
||||
from enum import StrEnum
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from habiticalib import (
|
||||
ContentData,
|
||||
HabiticaClass,
|
||||
TaskData,
|
||||
TaskType,
|
||||
UserData,
|
||||
deserialize_task,
|
||||
)
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
DOMAIN as SENSOR_DOMAIN,
|
||||
SensorDeviceClass,
|
||||
@ -36,10 +45,10 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class HabitipySensorEntityDescription(SensorEntityDescription):
|
||||
"""Habitipy Sensor Description."""
|
||||
|
||||
value_fn: Callable[[dict[str, Any], dict[str, Any]], StateType]
|
||||
attributes_fn: (
|
||||
Callable[[dict[str, Any], dict[str, Any]], dict[str, Any] | None] | None
|
||||
) = None
|
||||
value_fn: Callable[[UserData, ContentData], StateType]
|
||||
attributes_fn: Callable[[UserData, ContentData], dict[str, Any] | None] | None = (
|
||||
None
|
||||
)
|
||||
entity_picture: str | None = None
|
||||
|
||||
|
||||
@ -47,7 +56,7 @@ class HabitipySensorEntityDescription(SensorEntityDescription):
|
||||
class HabitipyTaskSensorEntityDescription(SensorEntityDescription):
|
||||
"""Habitipy Task Sensor Description."""
|
||||
|
||||
value_fn: Callable[[list[dict[str, Any]]], list[dict[str, Any]]]
|
||||
value_fn: Callable[[list[TaskData]], list[TaskData]]
|
||||
|
||||
|
||||
class HabitipySensorEntity(StrEnum):
|
||||
@ -79,75 +88,70 @@ SENSOR_DESCRIPTIONS: tuple[HabitipySensorEntityDescription, ...] = (
|
||||
HabitipySensorEntityDescription(
|
||||
key=HabitipySensorEntity.DISPLAY_NAME,
|
||||
translation_key=HabitipySensorEntity.DISPLAY_NAME,
|
||||
value_fn=lambda user, _: user.get("profile", {}).get("name"),
|
||||
value_fn=lambda user, _: user.profile.name,
|
||||
),
|
||||
HabitipySensorEntityDescription(
|
||||
key=HabitipySensorEntity.HEALTH,
|
||||
translation_key=HabitipySensorEntity.HEALTH,
|
||||
suggested_display_precision=0,
|
||||
value_fn=lambda user, _: user.get("stats", {}).get("hp"),
|
||||
value_fn=lambda user, _: user.stats.hp,
|
||||
),
|
||||
HabitipySensorEntityDescription(
|
||||
key=HabitipySensorEntity.HEALTH_MAX,
|
||||
translation_key=HabitipySensorEntity.HEALTH_MAX,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda user, _: user.get("stats", {}).get("maxHealth"),
|
||||
value_fn=lambda user, _: 50,
|
||||
),
|
||||
HabitipySensorEntityDescription(
|
||||
key=HabitipySensorEntity.MANA,
|
||||
translation_key=HabitipySensorEntity.MANA,
|
||||
suggested_display_precision=0,
|
||||
value_fn=lambda user, _: user.get("stats", {}).get("mp"),
|
||||
value_fn=lambda user, _: user.stats.mp,
|
||||
),
|
||||
HabitipySensorEntityDescription(
|
||||
key=HabitipySensorEntity.MANA_MAX,
|
||||
translation_key=HabitipySensorEntity.MANA_MAX,
|
||||
value_fn=lambda user, _: user.get("stats", {}).get("maxMP"),
|
||||
value_fn=lambda user, _: user.stats.maxMP,
|
||||
),
|
||||
HabitipySensorEntityDescription(
|
||||
key=HabitipySensorEntity.EXPERIENCE,
|
||||
translation_key=HabitipySensorEntity.EXPERIENCE,
|
||||
value_fn=lambda user, _: user.get("stats", {}).get("exp"),
|
||||
value_fn=lambda user, _: user.stats.exp,
|
||||
),
|
||||
HabitipySensorEntityDescription(
|
||||
key=HabitipySensorEntity.EXPERIENCE_MAX,
|
||||
translation_key=HabitipySensorEntity.EXPERIENCE_MAX,
|
||||
value_fn=lambda user, _: user.get("stats", {}).get("toNextLevel"),
|
||||
value_fn=lambda user, _: user.stats.toNextLevel,
|
||||
),
|
||||
HabitipySensorEntityDescription(
|
||||
key=HabitipySensorEntity.LEVEL,
|
||||
translation_key=HabitipySensorEntity.LEVEL,
|
||||
value_fn=lambda user, _: user.get("stats", {}).get("lvl"),
|
||||
value_fn=lambda user, _: user.stats.lvl,
|
||||
),
|
||||
HabitipySensorEntityDescription(
|
||||
key=HabitipySensorEntity.GOLD,
|
||||
translation_key=HabitipySensorEntity.GOLD,
|
||||
suggested_display_precision=2,
|
||||
value_fn=lambda user, _: user.get("stats", {}).get("gp"),
|
||||
value_fn=lambda user, _: user.stats.gp,
|
||||
),
|
||||
HabitipySensorEntityDescription(
|
||||
key=HabitipySensorEntity.CLASS,
|
||||
translation_key=HabitipySensorEntity.CLASS,
|
||||
value_fn=lambda user, _: user.get("stats", {}).get("class"),
|
||||
value_fn=lambda user, _: user.stats.Class.value if user.stats.Class else None,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=["warrior", "healer", "wizard", "rogue"],
|
||||
options=[item.value for item in HabiticaClass],
|
||||
),
|
||||
HabitipySensorEntityDescription(
|
||||
key=HabitipySensorEntity.GEMS,
|
||||
translation_key=HabitipySensorEntity.GEMS,
|
||||
value_fn=lambda user, _: user.get("balance", 0) * 4,
|
||||
value_fn=lambda user, _: round(user.balance * 4) if user.balance else None,
|
||||
suggested_display_precision=0,
|
||||
entity_picture="shop_gem.png",
|
||||
),
|
||||
HabitipySensorEntityDescription(
|
||||
key=HabitipySensorEntity.TRINKETS,
|
||||
translation_key=HabitipySensorEntity.TRINKETS,
|
||||
value_fn=(
|
||||
lambda user, _: user.get("purchased", {})
|
||||
.get("plan", {})
|
||||
.get("consecutive", {})
|
||||
.get("trinkets", 0)
|
||||
),
|
||||
value_fn=lambda user, _: user.purchased.plan.consecutive.trinkets or 0,
|
||||
suggested_display_precision=0,
|
||||
native_unit_of_measurement="⧖",
|
||||
entity_picture="notif_subscriber_reward.png",
|
||||
@ -155,16 +159,16 @@ SENSOR_DESCRIPTIONS: tuple[HabitipySensorEntityDescription, ...] = (
|
||||
HabitipySensorEntityDescription(
|
||||
key=HabitipySensorEntity.STRENGTH,
|
||||
translation_key=HabitipySensorEntity.STRENGTH,
|
||||
value_fn=lambda user, content: get_attributes_total(user, content, "str"),
|
||||
attributes_fn=lambda user, content: get_attribute_points(user, content, "str"),
|
||||
value_fn=lambda user, content: get_attributes_total(user, content, "Str"),
|
||||
attributes_fn=lambda user, content: get_attribute_points(user, content, "Str"),
|
||||
suggested_display_precision=0,
|
||||
native_unit_of_measurement="STR",
|
||||
),
|
||||
HabitipySensorEntityDescription(
|
||||
key=HabitipySensorEntity.INTELLIGENCE,
|
||||
translation_key=HabitipySensorEntity.INTELLIGENCE,
|
||||
value_fn=lambda user, content: get_attributes_total(user, content, "int"),
|
||||
attributes_fn=lambda user, content: get_attribute_points(user, content, "int"),
|
||||
value_fn=lambda user, content: get_attributes_total(user, content, "Int"),
|
||||
attributes_fn=lambda user, content: get_attribute_points(user, content, "Int"),
|
||||
suggested_display_precision=0,
|
||||
native_unit_of_measurement="INT",
|
||||
),
|
||||
@ -203,7 +207,7 @@ TASKS_MAP = {
|
||||
"yester_daily": "yesterDaily",
|
||||
"completed": "completed",
|
||||
"collapse_checklist": "collapseChecklist",
|
||||
"type": "type",
|
||||
"type": "Type",
|
||||
"notes": "notes",
|
||||
"tags": "tags",
|
||||
"value": "value",
|
||||
@ -221,26 +225,28 @@ TASK_SENSOR_DESCRIPTION: tuple[HabitipyTaskSensorEntityDescription, ...] = (
|
||||
HabitipyTaskSensorEntityDescription(
|
||||
key=HabitipySensorEntity.HABITS,
|
||||
translation_key=HabitipySensorEntity.HABITS,
|
||||
value_fn=lambda tasks: [r for r in tasks if r.get("type") == "habit"],
|
||||
value_fn=lambda tasks: [r for r in tasks if r.Type is TaskType.HABIT],
|
||||
),
|
||||
HabitipyTaskSensorEntityDescription(
|
||||
key=HabitipySensorEntity.DAILIES,
|
||||
translation_key=HabitipySensorEntity.DAILIES,
|
||||
value_fn=lambda tasks: [r for r in tasks if r.get("type") == "daily"],
|
||||
value_fn=lambda tasks: [r for r in tasks if r.Type is TaskType.DAILY],
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
HabitipyTaskSensorEntityDescription(
|
||||
key=HabitipySensorEntity.TODOS,
|
||||
translation_key=HabitipySensorEntity.TODOS,
|
||||
value_fn=lambda tasks: [
|
||||
r for r in tasks if r.get("type") == "todo" and not r.get("completed")
|
||||
],
|
||||
value_fn=(
|
||||
lambda tasks: [
|
||||
r for r in tasks if r.Type is TaskType.TODO and not r.completed
|
||||
]
|
||||
),
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
HabitipyTaskSensorEntityDescription(
|
||||
key=HabitipySensorEntity.REWARDS,
|
||||
translation_key=HabitipySensorEntity.REWARDS,
|
||||
value_fn=lambda tasks: [r for r in tasks if r.get("type") == "reward"],
|
||||
value_fn=lambda tasks: [r for r in tasks if r.Type is TaskType.REWARD],
|
||||
),
|
||||
)
|
||||
|
||||
@ -309,15 +315,14 @@ class HabitipyTaskSensor(HabiticaBase, SensorEntity):
|
||||
attrs = {}
|
||||
|
||||
# Map tasks to TASKS_MAP
|
||||
for received_task in self.entity_description.value_fn(
|
||||
self.coordinator.data.tasks
|
||||
):
|
||||
for task_data in self.entity_description.value_fn(self.coordinator.data.tasks):
|
||||
received_task = deserialize_task(asdict(task_data))
|
||||
task_id = received_task[TASKS_MAP_ID]
|
||||
task = {}
|
||||
for map_key, map_value in TASKS_MAP.items():
|
||||
if value := received_task.get(map_value):
|
||||
task[map_key] = value
|
||||
attrs[task_id] = task
|
||||
attrs[str(task_id)] = task
|
||||
return attrs
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
|
@ -2,11 +2,19 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from http import HTTPStatus
|
||||
from dataclasses import asdict
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from aiohttp import ClientError
|
||||
from habiticalib import (
|
||||
Direction,
|
||||
HabiticaException,
|
||||
NotAuthorizedError,
|
||||
NotFoundError,
|
||||
Skill,
|
||||
TooManyRequestsError,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
@ -88,6 +96,25 @@ SERVICE_TRANSFORMATION_SCHEMA = vol.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
|
||||
def get_config_entry(hass: HomeAssistant, entry_id: str) -> HabiticaConfigEntry:
|
||||
"""Return config entry or raise if not found or not loaded."""
|
||||
@ -123,12 +150,12 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
|
||||
|
||||
name = call.data[ATTR_NAME]
|
||||
path = call.data[ATTR_PATH]
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
entries: list[HabiticaConfigEntry] = hass.config_entries.async_entries(DOMAIN)
|
||||
|
||||
api = None
|
||||
for entry in entries:
|
||||
if entry.data[CONF_NAME] == name:
|
||||
api = entry.runtime_data.api
|
||||
api = await entry.runtime_data.habitica.habitipy()
|
||||
break
|
||||
if api is None:
|
||||
_LOGGER.error("API_CALL: User '%s' not configured", name)
|
||||
@ -151,18 +178,15 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
|
||||
"""Skill action."""
|
||||
entry = get_config_entry(hass, call.data[ATTR_CONFIG_ENTRY])
|
||||
coordinator = entry.runtime_data
|
||||
skill = {
|
||||
"pickpocket": {"spellId": "pickPocket", "cost": "10 MP"},
|
||||
"backstab": {"spellId": "backStab", "cost": "15 MP"},
|
||||
"smash": {"spellId": "smash", "cost": "10 MP"},
|
||||
"fireball": {"spellId": "fireball", "cost": "10 MP"},
|
||||
}
|
||||
|
||||
skill = SKILL_MAP[call.data[ATTR_SKILL]]
|
||||
cost = COST_MAP[call.data[ATTR_SKILL]]
|
||||
|
||||
try:
|
||||
task_id = next(
|
||||
task["id"]
|
||||
task.id
|
||||
for task in coordinator.data.tasks
|
||||
if call.data[ATTR_TASK] in (task["id"], task.get("alias"))
|
||||
or call.data[ATTR_TASK] == task["text"]
|
||||
if call.data[ATTR_TASK] in (str(task.id), task.alias, task.text)
|
||||
)
|
||||
except StopIteration as e:
|
||||
raise ServiceValidationError(
|
||||
@ -172,75 +196,76 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
|
||||
) from e
|
||||
|
||||
try:
|
||||
response: dict[str, Any] = await coordinator.api.user.class_.cast[
|
||||
skill[call.data[ATTR_SKILL]]["spellId"]
|
||||
].post(targetId=task_id)
|
||||
except ClientResponseError as e:
|
||||
if e.status == HTTPStatus.TOO_MANY_REQUESTS:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="setup_rate_limit_exception",
|
||||
) from e
|
||||
if e.status == HTTPStatus.UNAUTHORIZED:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="not_enough_mana",
|
||||
translation_placeholders={
|
||||
"cost": skill[call.data[ATTR_SKILL]]["cost"],
|
||||
"mana": f"{int(coordinator.data.user.get("stats", {}).get("mp", 0))} MP",
|
||||
},
|
||||
) from e
|
||||
if e.status == HTTPStatus.NOT_FOUND:
|
||||
# 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
|
||||
response = await coordinator.habitica.cast_skill(skill, task_id)
|
||||
except TooManyRequestsError as e:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="setup_rate_limit_exception",
|
||||
) 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, ClientError) as e:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="service_call_exception",
|
||||
) from e
|
||||
else:
|
||||
await coordinator.async_request_refresh()
|
||||
return response
|
||||
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
|
||||
|
||||
COMMAND_MAP = {
|
||||
SERVICE_ABORT_QUEST: "abort",
|
||||
SERVICE_ACCEPT_QUEST: "accept",
|
||||
SERVICE_CANCEL_QUEST: "cancel",
|
||||
SERVICE_LEAVE_QUEST: "leave",
|
||||
SERVICE_REJECT_QUEST: "reject",
|
||||
SERVICE_START_QUEST: "force-start",
|
||||
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:
|
||||
return await coordinator.api.groups.party.quests[
|
||||
COMMAND_MAP[call.service]
|
||||
].post()
|
||||
except ClientResponseError as e:
|
||||
if e.status == HTTPStatus.TOO_MANY_REQUESTS:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="setup_rate_limit_exception",
|
||||
) from e
|
||||
if e.status == HTTPStatus.UNAUTHORIZED:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN, translation_key="quest_action_unallowed"
|
||||
) from e
|
||||
if e.status == HTTPStatus.NOT_FOUND:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN, translation_key="quest_not_found"
|
||||
) from e
|
||||
response = await func()
|
||||
except TooManyRequestsError as e:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="setup_rate_limit_exception",
|
||||
) 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, ClientError) as e:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="service_call_exception"
|
||||
) from e
|
||||
else:
|
||||
return asdict(response.data)
|
||||
|
||||
for service in (
|
||||
SERVICE_ABORT_QUEST,
|
||||
@ -262,12 +287,15 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
|
||||
"""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.get("value"))
|
||||
(task.id, task.value)
|
||||
for task in coordinator.data.tasks
|
||||
if call.data[ATTR_TASK] in (task["id"], task.get("alias"))
|
||||
or call.data[ATTR_TASK] == task["text"]
|
||||
if call.data[ATTR_TASK] in (str(task.id), task.alias, task.text)
|
||||
)
|
||||
except StopIteration as e:
|
||||
raise ServiceValidationError(
|
||||
@ -276,81 +304,76 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
|
||||
translation_placeholders={"task": f"'{call.data[ATTR_TASK]}'"},
|
||||
) from e
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert task_id
|
||||
try:
|
||||
response: dict[str, Any] = (
|
||||
await coordinator.api.tasks[task_id]
|
||||
.score[call.data.get(ATTR_DIRECTION, "up")]
|
||||
.post()
|
||||
)
|
||||
except ClientResponseError as e:
|
||||
if e.status == HTTPStatus.TOO_MANY_REQUESTS:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="setup_rate_limit_exception",
|
||||
) from e
|
||||
if e.status == HTTPStatus.UNAUTHORIZED and task_value is not None:
|
||||
response = await coordinator.habitica.update_score(task_id, direction)
|
||||
except TooManyRequestsError as e:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="setup_rate_limit_exception",
|
||||
) 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"]:.2f} GP",
|
||||
"cost": f"{task_value} GP",
|
||||
"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",
|
||||
) from e
|
||||
except (HabiticaException, ClientError) as e:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="service_call_exception",
|
||||
) from e
|
||||
else:
|
||||
await coordinator.async_request_refresh()
|
||||
return response
|
||||
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
|
||||
ITEMID_MAP = {
|
||||
"snowball": {"itemId": "snowball"},
|
||||
"spooky_sparkles": {"itemId": "spookySparkles"},
|
||||
"seafoam": {"itemId": "seafoam"},
|
||||
"shiny_seed": {"itemId": "shinySeed"},
|
||||
}
|
||||
|
||||
item = ITEMID_MAP[call.data[ATTR_ITEM]]
|
||||
# check if target is self
|
||||
if call.data[ATTR_TARGET] in (
|
||||
coordinator.data.user["id"],
|
||||
coordinator.data.user["profile"]["name"],
|
||||
coordinator.data.user["auth"]["local"]["username"],
|
||||
str(coordinator.data.user.id),
|
||||
coordinator.data.user.profile.name,
|
||||
coordinator.data.user.auth.local.username,
|
||||
):
|
||||
target_id = coordinator.data.user["id"]
|
||||
target_id = coordinator.data.user.id
|
||||
else:
|
||||
# check if target is a party member
|
||||
try:
|
||||
party = await coordinator.api.groups.party.members.get()
|
||||
except ClientResponseError as e:
|
||||
if e.status == HTTPStatus.TOO_MANY_REQUESTS:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="setup_rate_limit_exception",
|
||||
) from e
|
||||
if e.status == HTTPStatus.NOT_FOUND:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="party_not_found",
|
||||
) from e
|
||||
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 (ClientError, HabiticaException) as e:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="service_call_exception",
|
||||
) from e
|
||||
try:
|
||||
target_id = next(
|
||||
member["id"]
|
||||
for member in party
|
||||
if call.data[ATTR_TARGET].lower()
|
||||
member.id
|
||||
for member in party.data
|
||||
if member.id
|
||||
and call.data[ATTR_TARGET].lower()
|
||||
in (
|
||||
member["id"],
|
||||
member["auth"]["local"]["username"].lower(),
|
||||
member["profile"]["name"].lower(),
|
||||
str(member.id),
|
||||
str(member.auth.local.username).lower(),
|
||||
str(member.profile.name).lower(),
|
||||
)
|
||||
)
|
||||
except StopIteration as e:
|
||||
@ -360,27 +383,25 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
|
||||
translation_placeholders={"target": f"'{call.data[ATTR_TARGET]}'"},
|
||||
) from e
|
||||
try:
|
||||
response: dict[str, Any] = await coordinator.api.user.class_.cast[
|
||||
ITEMID_MAP[call.data[ATTR_ITEM]]["itemId"]
|
||||
].post(targetId=target_id)
|
||||
except ClientResponseError as e:
|
||||
if e.status == HTTPStatus.TOO_MANY_REQUESTS:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="setup_rate_limit_exception",
|
||||
) from e
|
||||
if e.status == HTTPStatus.UNAUTHORIZED:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="item_not_found",
|
||||
translation_placeholders={"item": call.data[ATTR_ITEM]},
|
||||
) from e
|
||||
response = await coordinator.habitica.cast_skill(item, target_id)
|
||||
except TooManyRequestsError as e:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="setup_rate_limit_exception",
|
||||
) 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, ClientError) as e:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="service_call_exception",
|
||||
) from e
|
||||
else:
|
||||
return response
|
||||
return asdict(response.data)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
|
@ -49,7 +49,8 @@
|
||||
"data_description": {
|
||||
"url": "URL of the Habitica installation to connect to. Defaults to `{default_url}`",
|
||||
"api_user": "User ID of your Habitica account",
|
||||
"api_key": "API Token of the Habitica account"
|
||||
"api_key": "API Token of the Habitica account",
|
||||
"verify_ssl": "Enable SSL certificate verification for secure connections. Disable only if connecting to a Habitica instance using a self-signed certificate"
|
||||
},
|
||||
"description": "You can retrieve your `User ID` and `API Token` from [**Settings -> Site Data**]({site_data}) on Habitica or the instance you want to connect to"
|
||||
}
|
||||
@ -365,6 +366,9 @@
|
||||
},
|
||||
"item_not_found": {
|
||||
"message": "Unable to use {item}, you don't own this item."
|
||||
},
|
||||
"invalid_auth": {
|
||||
"message": "Authentication failed for {account}."
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
|
@ -28,7 +28,7 @@ class HabiticaSwitchEntityDescription(SwitchEntityDescription):
|
||||
|
||||
turn_on_fn: Callable[[HabiticaDataUpdateCoordinator], Any]
|
||||
turn_off_fn: Callable[[HabiticaDataUpdateCoordinator], Any]
|
||||
is_on_fn: Callable[[HabiticaData], bool]
|
||||
is_on_fn: Callable[[HabiticaData], bool | None]
|
||||
|
||||
|
||||
class HabiticaSwitchEntity(StrEnum):
|
||||
@ -42,9 +42,9 @@ SWTICH_DESCRIPTIONS: tuple[HabiticaSwitchEntityDescription, ...] = (
|
||||
key=HabiticaSwitchEntity.SLEEP,
|
||||
translation_key=HabiticaSwitchEntity.SLEEP,
|
||||
device_class=SwitchDeviceClass.SWITCH,
|
||||
turn_on_fn=lambda coordinator: coordinator.api["user"]["sleep"].post(),
|
||||
turn_off_fn=lambda coordinator: coordinator.api["user"]["sleep"].post(),
|
||||
is_on_fn=lambda data: data.user["preferences"]["sleep"],
|
||||
turn_on_fn=lambda coordinator: coordinator.habitica.toggle_sleep(),
|
||||
turn_off_fn=lambda coordinator: coordinator.habitica.toggle_sleep(),
|
||||
is_on_fn=lambda data: data.user.preferences.sleep,
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -2,11 +2,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from enum import StrEnum
|
||||
from typing import TYPE_CHECKING
|
||||
from uuid import UUID
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from aiohttp import ClientError
|
||||
from habiticalib import Direction, HabiticaException, Task, TaskType
|
||||
|
||||
from homeassistant.components import persistent_notification
|
||||
from homeassistant.components.todo import (
|
||||
@ -24,7 +25,7 @@ from homeassistant.util import dt as dt_util
|
||||
from .const import ASSETS_URL, DOMAIN
|
||||
from .coordinator import HabiticaDataUpdateCoordinator
|
||||
from .entity import HabiticaBase
|
||||
from .types import HabiticaConfigEntry, HabiticaTaskType
|
||||
from .types import HabiticaConfigEntry
|
||||
from .util import next_due_date
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
@ -70,8 +71,8 @@ class BaseHabiticaListEntity(HabiticaBase, TodoListEntity):
|
||||
"""Delete Habitica tasks."""
|
||||
if len(uids) > 1 and self.entity_description.key is HabiticaTodoList.TODOS:
|
||||
try:
|
||||
await self.coordinator.api.tasks.clearCompletedTodos.post()
|
||||
except ClientResponseError as e:
|
||||
await self.coordinator.habitica.delete_completed_todos()
|
||||
except (HabiticaException, ClientError) as e:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="delete_completed_todos_failed",
|
||||
@ -79,8 +80,8 @@ class BaseHabiticaListEntity(HabiticaBase, TodoListEntity):
|
||||
else:
|
||||
for task_id in uids:
|
||||
try:
|
||||
await self.coordinator.api.tasks[task_id].delete()
|
||||
except ClientResponseError as e:
|
||||
await self.coordinator.habitica.delete_task(UUID(task_id))
|
||||
except (HabiticaException, ClientError) as e:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key=f"delete_{self.entity_description.key}_failed",
|
||||
@ -106,9 +107,8 @@ class BaseHabiticaListEntity(HabiticaBase, TodoListEntity):
|
||||
pos = 0
|
||||
|
||||
try:
|
||||
await self.coordinator.api.tasks[uid].move.to[str(pos)].post()
|
||||
|
||||
except ClientResponseError as e:
|
||||
await self.coordinator.habitica.reorder_task(UUID(uid), pos)
|
||||
except (HabiticaException, ClientError) as e:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key=f"move_{self.entity_description.key}_item_failed",
|
||||
@ -118,12 +118,14 @@ class BaseHabiticaListEntity(HabiticaBase, TodoListEntity):
|
||||
# move tasks in the coordinator until we have fresh data
|
||||
tasks = self.coordinator.data.tasks
|
||||
new_pos = (
|
||||
tasks.index(next(task for task in tasks if task["id"] == previous_uid))
|
||||
tasks.index(
|
||||
next(task for task in tasks if task.id == UUID(previous_uid))
|
||||
)
|
||||
+ 1
|
||||
if previous_uid
|
||||
else 0
|
||||
)
|
||||
old_pos = tasks.index(next(task for task in tasks if task["id"] == uid))
|
||||
old_pos = tasks.index(next(task for task in tasks if task.id == UUID(uid)))
|
||||
tasks.insert(new_pos, tasks.pop(old_pos))
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@ -138,14 +140,17 @@ class BaseHabiticaListEntity(HabiticaBase, TodoListEntity):
|
||||
if TYPE_CHECKING:
|
||||
assert item.uid
|
||||
assert current_item
|
||||
assert item.summary
|
||||
|
||||
task = Task(
|
||||
text=item.summary,
|
||||
notes=item.description or "",
|
||||
)
|
||||
|
||||
if (
|
||||
self.entity_description.key is HabiticaTodoList.TODOS
|
||||
and item.due is not None
|
||||
): # Only todos support a due date.
|
||||
date = item.due.isoformat()
|
||||
else:
|
||||
date = None
|
||||
task["date"] = item.due
|
||||
|
||||
if (
|
||||
item.summary != current_item.summary
|
||||
@ -153,13 +158,9 @@ class BaseHabiticaListEntity(HabiticaBase, TodoListEntity):
|
||||
or item.due != current_item.due
|
||||
):
|
||||
try:
|
||||
await self.coordinator.api.tasks[item.uid].put(
|
||||
text=item.summary,
|
||||
notes=item.description or "",
|
||||
date=date,
|
||||
)
|
||||
await self.coordinator.habitica.update_task(UUID(item.uid), task)
|
||||
refresh_required = True
|
||||
except ClientResponseError as e:
|
||||
except (HabiticaException, ClientError) as e:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key=f"update_{self.entity_description.key}_item_failed",
|
||||
@ -172,32 +173,33 @@ class BaseHabiticaListEntity(HabiticaBase, TodoListEntity):
|
||||
current_item.status is TodoItemStatus.NEEDS_ACTION
|
||||
and item.status == TodoItemStatus.COMPLETED
|
||||
):
|
||||
score_result = (
|
||||
await self.coordinator.api.tasks[item.uid].score["up"].post()
|
||||
score_result = await self.coordinator.habitica.update_score(
|
||||
UUID(item.uid), Direction.UP
|
||||
)
|
||||
refresh_required = True
|
||||
elif (
|
||||
current_item.status is TodoItemStatus.COMPLETED
|
||||
and item.status == TodoItemStatus.NEEDS_ACTION
|
||||
):
|
||||
score_result = (
|
||||
await self.coordinator.api.tasks[item.uid].score["down"].post()
|
||||
score_result = await self.coordinator.habitica.update_score(
|
||||
UUID(item.uid), Direction.DOWN
|
||||
)
|
||||
refresh_required = True
|
||||
else:
|
||||
score_result = None
|
||||
|
||||
except ClientResponseError as e:
|
||||
except (HabiticaException, ClientError) as e:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key=f"score_{self.entity_description.key}_item_failed",
|
||||
translation_placeholders={"name": item.summary or ""},
|
||||
) from e
|
||||
|
||||
if score_result and (drop := score_result.get("_tmp", {}).get("drop", False)):
|
||||
if score_result and score_result.data.tmp.drop.key:
|
||||
drop = score_result.data.tmp.drop
|
||||
msg = (
|
||||
f"![{drop["key"]}]({ASSETS_URL}Pet_{drop["type"]}_{drop["key"]}.png)\n"
|
||||
f"{drop["dialog"]}"
|
||||
f"\n"
|
||||
f"{drop.dialog}"
|
||||
)
|
||||
persistent_notification.async_create(
|
||||
self.hass, message=msg, title="Habitica"
|
||||
@ -229,38 +231,36 @@ class HabiticaTodosListEntity(BaseHabiticaListEntity):
|
||||
return [
|
||||
*(
|
||||
TodoItem(
|
||||
uid=task["id"],
|
||||
summary=task["text"],
|
||||
description=task["notes"],
|
||||
due=(
|
||||
dt_util.as_local(
|
||||
datetime.datetime.fromisoformat(task["date"])
|
||||
).date()
|
||||
if task.get("date")
|
||||
else None
|
||||
),
|
||||
uid=str(task.id),
|
||||
summary=task.text,
|
||||
description=task.notes,
|
||||
due=dt_util.as_local(task.date).date() if task.date else None,
|
||||
status=(
|
||||
TodoItemStatus.NEEDS_ACTION
|
||||
if not task["completed"]
|
||||
if not task.completed
|
||||
else TodoItemStatus.COMPLETED
|
||||
),
|
||||
)
|
||||
for task in self.coordinator.data.tasks
|
||||
if task["type"] == HabiticaTaskType.TODO
|
||||
if task.Type is TaskType.TODO
|
||||
),
|
||||
]
|
||||
|
||||
async def async_create_todo_item(self, item: TodoItem) -> None:
|
||||
"""Create a Habitica todo."""
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert item.summary
|
||||
assert item.description
|
||||
try:
|
||||
await self.coordinator.api.tasks.user.post(
|
||||
text=item.summary,
|
||||
type=HabiticaTaskType.TODO,
|
||||
notes=item.description,
|
||||
date=item.due.isoformat() if item.due else None,
|
||||
await self.coordinator.habitica.create_task(
|
||||
Task(
|
||||
text=item.summary,
|
||||
type=TaskType.TODO,
|
||||
notes=item.description,
|
||||
date=item.due,
|
||||
)
|
||||
)
|
||||
except ClientResponseError as e:
|
||||
except (HabiticaException, ClientError) as e:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key=f"create_{self.entity_description.key}_item_failed",
|
||||
@ -295,23 +295,23 @@ class HabiticaDailiesListEntity(BaseHabiticaListEntity):
|
||||
that have been completed but forgotten to mark as completed before resetting the dailies.
|
||||
Changes of the date input field in Home Assistant will be ignored.
|
||||
"""
|
||||
|
||||
last_cron = self.coordinator.data.user["lastCron"]
|
||||
if TYPE_CHECKING:
|
||||
assert self.coordinator.data.user.lastCron
|
||||
|
||||
return [
|
||||
*(
|
||||
TodoItem(
|
||||
uid=task["id"],
|
||||
summary=task["text"],
|
||||
description=task["notes"],
|
||||
due=next_due_date(task, last_cron),
|
||||
uid=str(task.id),
|
||||
summary=task.text,
|
||||
description=task.notes,
|
||||
due=next_due_date(task, self.coordinator.data.user.lastCron),
|
||||
status=(
|
||||
TodoItemStatus.COMPLETED
|
||||
if task["completed"]
|
||||
if task.completed
|
||||
else TodoItemStatus.NEEDS_ACTION
|
||||
),
|
||||
)
|
||||
for task in self.coordinator.data.tasks
|
||||
if task["type"] == HabiticaTaskType.DAILY
|
||||
if task.Type is TaskType.DAILY
|
||||
)
|
||||
]
|
||||
|
@ -2,9 +2,10 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import fields
|
||||
import datetime
|
||||
from math import floor
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dateutil.rrule import (
|
||||
DAILY,
|
||||
@ -20,6 +21,7 @@ from dateutil.rrule import (
|
||||
YEARLY,
|
||||
rrule,
|
||||
)
|
||||
from habiticalib import ContentData, Frequency, TaskData, UserData
|
||||
|
||||
from homeassistant.components.automation import automations_with_entity
|
||||
from homeassistant.components.script import scripts_with_entity
|
||||
@ -27,50 +29,32 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
|
||||
def next_due_date(task: dict[str, Any], last_cron: str) -> datetime.date | None:
|
||||
def next_due_date(task: TaskData, today: datetime.datetime) -> datetime.date | None:
|
||||
"""Calculate due date for dailies and yesterdailies."""
|
||||
|
||||
if task["everyX"] == 0 or not task.get("nextDue"): # grey dailies never become due
|
||||
if task.everyX == 0 or not task.nextDue: # grey dailies never become due
|
||||
return None
|
||||
|
||||
today = to_date(last_cron)
|
||||
startdate = to_date(task["startDate"])
|
||||
if TYPE_CHECKING:
|
||||
assert today
|
||||
assert startdate
|
||||
assert task.startDate
|
||||
|
||||
if task["isDue"] and not task["completed"]:
|
||||
return to_date(last_cron)
|
||||
if task.isDue is True and not task.completed:
|
||||
return dt_util.as_local(today).date()
|
||||
|
||||
if startdate > today:
|
||||
if task["frequency"] == "daily" or (
|
||||
task["frequency"] in ("monthly", "yearly") and task["daysOfMonth"]
|
||||
if task.startDate > today:
|
||||
if task.frequency is Frequency.DAILY or (
|
||||
task.frequency in (Frequency.MONTHLY, Frequency.YEARLY) and task.daysOfMonth
|
||||
):
|
||||
return startdate
|
||||
return dt_util.as_local(task.startDate).date()
|
||||
|
||||
if (
|
||||
task["frequency"] in ("weekly", "monthly")
|
||||
and (nextdue := to_date(task["nextDue"][0]))
|
||||
and startdate > nextdue
|
||||
task.frequency in (Frequency.WEEKLY, Frequency.MONTHLY)
|
||||
and (nextdue := task.nextDue[0])
|
||||
and task.startDate > nextdue
|
||||
):
|
||||
return to_date(task["nextDue"][1])
|
||||
return dt_util.as_local(task.nextDue[1]).date()
|
||||
|
||||
return to_date(task["nextDue"][0])
|
||||
|
||||
|
||||
def to_date(date: str) -> datetime.date | None:
|
||||
"""Convert an iso date to a datetime.date object."""
|
||||
try:
|
||||
return dt_util.as_local(datetime.datetime.fromisoformat(date)).date()
|
||||
except ValueError:
|
||||
# sometimes nextDue dates are JavaScript datetime strings instead of iso:
|
||||
# "Mon May 06 2024 00:00:00 GMT+0200"
|
||||
try:
|
||||
return dt_util.as_local(
|
||||
datetime.datetime.strptime(date, "%a %b %d %Y %H:%M:%S %Z%z")
|
||||
).date()
|
||||
except ValueError:
|
||||
return None
|
||||
return dt_util.as_local(task.nextDue[0]).date()
|
||||
|
||||
|
||||
def entity_used_in(hass: HomeAssistant, entity_id: str) -> list[str]:
|
||||
@ -84,30 +68,27 @@ FREQUENCY_MAP = {"daily": DAILY, "weekly": WEEKLY, "monthly": MONTHLY, "yearly":
|
||||
WEEKDAY_MAP = {"m": MO, "t": TU, "w": WE, "th": TH, "f": FR, "s": SA, "su": SU}
|
||||
|
||||
|
||||
def build_rrule(task: dict[str, Any]) -> rrule:
|
||||
def build_rrule(task: TaskData) -> rrule:
|
||||
"""Build rrule string."""
|
||||
|
||||
rrule_frequency = FREQUENCY_MAP.get(task["frequency"], DAILY)
|
||||
weekdays = [
|
||||
WEEKDAY_MAP[day] for day, is_active in task["repeat"].items() if is_active
|
||||
]
|
||||
if TYPE_CHECKING:
|
||||
assert task.frequency
|
||||
assert task.everyX
|
||||
rrule_frequency = FREQUENCY_MAP.get(task.frequency, DAILY)
|
||||
weekdays = [day for key, day in WEEKDAY_MAP.items() if getattr(task.repeat, key)]
|
||||
bymonthday = (
|
||||
task["daysOfMonth"]
|
||||
if rrule_frequency == MONTHLY and task["daysOfMonth"]
|
||||
else None
|
||||
task.daysOfMonth if rrule_frequency == MONTHLY and task.daysOfMonth else None
|
||||
)
|
||||
|
||||
bysetpos = None
|
||||
if rrule_frequency == MONTHLY and task["weeksOfMonth"]:
|
||||
bysetpos = task["weeksOfMonth"]
|
||||
if rrule_frequency == MONTHLY and task.weeksOfMonth:
|
||||
bysetpos = task.weeksOfMonth
|
||||
weekdays = weekdays if weekdays else [MO]
|
||||
|
||||
return rrule(
|
||||
freq=rrule_frequency,
|
||||
interval=task["everyX"],
|
||||
dtstart=dt_util.start_of_local_day(
|
||||
datetime.datetime.fromisoformat(task["startDate"])
|
||||
),
|
||||
interval=task.everyX,
|
||||
dtstart=dt_util.start_of_local_day(task.startDate),
|
||||
byweekday=weekdays if rrule_frequency in [WEEKLY, MONTHLY] else None,
|
||||
bymonthday=bymonthday,
|
||||
bysetpos=bysetpos,
|
||||
@ -143,48 +124,37 @@ def get_recurrence_rule(recurrence: rrule) -> str:
|
||||
|
||||
|
||||
def get_attribute_points(
|
||||
user: dict[str, Any], content: dict[str, Any], attribute: str
|
||||
user: UserData, content: ContentData, attribute: str
|
||||
) -> dict[str, float]:
|
||||
"""Get modifiers contributing to strength attribute."""
|
||||
|
||||
gear_set = {
|
||||
"weapon",
|
||||
"armor",
|
||||
"head",
|
||||
"shield",
|
||||
"back",
|
||||
"headAccessory",
|
||||
"eyewear",
|
||||
"body",
|
||||
}
|
||||
"""Get modifiers contributing to STR/INT/CON/PER attributes."""
|
||||
|
||||
equipment = sum(
|
||||
stats[attribute]
|
||||
for gear in gear_set
|
||||
if (equipped := user["items"]["gear"]["equipped"].get(gear))
|
||||
and (stats := content["gear"]["flat"].get(equipped))
|
||||
getattr(stats, attribute)
|
||||
for gear in fields(user.items.gear.equipped)
|
||||
if (equipped := getattr(user.items.gear.equipped, gear.name))
|
||||
and (stats := content.gear.flat[equipped])
|
||||
)
|
||||
|
||||
class_bonus = sum(
|
||||
stats[attribute] / 2
|
||||
for gear in gear_set
|
||||
if (equipped := user["items"]["gear"]["equipped"].get(gear))
|
||||
and (stats := content["gear"]["flat"].get(equipped))
|
||||
and stats["klass"] == user["stats"]["class"]
|
||||
getattr(stats, attribute) / 2
|
||||
for gear in fields(user.items.gear.equipped)
|
||||
if (equipped := getattr(user.items.gear.equipped, gear.name))
|
||||
and (stats := content.gear.flat[equipped])
|
||||
and stats.klass == user.stats.Class
|
||||
)
|
||||
if TYPE_CHECKING:
|
||||
assert user.stats.lvl
|
||||
|
||||
return {
|
||||
"level": min(floor(user["stats"]["lvl"] / 2), 50),
|
||||
"level": min(floor(user.stats.lvl / 2), 50),
|
||||
"equipment": equipment,
|
||||
"class": class_bonus,
|
||||
"allocated": user["stats"][attribute],
|
||||
"buffs": user["stats"]["buffs"][attribute],
|
||||
"allocated": getattr(user.stats, attribute),
|
||||
"buffs": getattr(user.stats.buffs, attribute),
|
||||
}
|
||||
|
||||
|
||||
def get_attributes_total(
|
||||
user: dict[str, Any], content: dict[str, Any], attribute: str
|
||||
) -> int:
|
||||
def get_attributes_total(user: UserData, content: ContentData, attribute: str) -> int:
|
||||
"""Get total attribute points."""
|
||||
return floor(
|
||||
sum(value for value in get_attribute_points(user, content, attribute).values())
|
||||
|
@ -1088,7 +1088,7 @@ ha-iotawattpy==0.1.2
|
||||
ha-philipsjs==3.2.2
|
||||
|
||||
# homeassistant.components.habitica
|
||||
habitipy==0.3.3
|
||||
habiticalib==0.3.1
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
habluetooth==3.6.0
|
||||
|
@ -929,7 +929,7 @@ ha-iotawattpy==0.1.2
|
||||
ha-philipsjs==3.2.2
|
||||
|
||||
# homeassistant.components.habitica
|
||||
habitipy==0.3.3
|
||||
habiticalib==0.3.1
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
habluetooth==3.6.0
|
||||
|
@ -1,77 +1,41 @@
|
||||
"""Tests for the habitica component."""
|
||||
|
||||
from unittest.mock import patch
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from habiticalib import (
|
||||
BadRequestError,
|
||||
HabiticaContentResponse,
|
||||
HabiticaErrorResponse,
|
||||
HabiticaGroupMembersResponse,
|
||||
HabiticaLoginResponse,
|
||||
HabiticaQuestResponse,
|
||||
HabiticaResponse,
|
||||
HabiticaScoreResponse,
|
||||
HabiticaSleepResponse,
|
||||
HabiticaTaskOrderResponse,
|
||||
HabiticaTaskResponse,
|
||||
HabiticaTasksResponse,
|
||||
HabiticaUserAnonymizedrResponse,
|
||||
HabiticaUserResponse,
|
||||
NotAuthorizedError,
|
||||
NotFoundError,
|
||||
TaskFilter,
|
||||
TooManyRequestsError,
|
||||
)
|
||||
import pytest
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.components.habitica.const import CONF_API_USER, DEFAULT_URL, DOMAIN
|
||||
from homeassistant.const import CONF_API_KEY, CONF_URL
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry, load_json_object_fixture
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
from tests.common import MockConfigEntry, load_fixture
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def disable_plumbum():
|
||||
"""Disable plumbum in tests as it can cause the test suite to fail.
|
||||
|
||||
plumbum can leave behind PlumbumTimeoutThreads
|
||||
"""
|
||||
with patch("plumbum.local"), patch("plumbum.colors"):
|
||||
yield
|
||||
|
||||
|
||||
def mock_called_with(
|
||||
mock_client: AiohttpClientMocker,
|
||||
method: str,
|
||||
url: str,
|
||||
) -> tuple | None:
|
||||
"""Assert request mock was called with json data."""
|
||||
|
||||
return next(
|
||||
(
|
||||
call
|
||||
for call in mock_client.mock_calls
|
||||
if call[0].upper() == method.upper() and call[1] == URL(url)
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_habitica(aioclient_mock: AiohttpClientMocker) -> AiohttpClientMocker:
|
||||
"""Mock aiohttp requests."""
|
||||
|
||||
aioclient_mock.get(
|
||||
f"{DEFAULT_URL}/api/v3/user", json=load_json_object_fixture("user.json", DOMAIN)
|
||||
)
|
||||
aioclient_mock.get(
|
||||
f"{DEFAULT_URL}/api/v3/tasks/user",
|
||||
params={"type": "completedTodos"},
|
||||
json=load_json_object_fixture("completed_todos.json", DOMAIN),
|
||||
)
|
||||
aioclient_mock.get(
|
||||
f"{DEFAULT_URL}/api/v3/tasks/user",
|
||||
json=load_json_object_fixture("tasks.json", DOMAIN),
|
||||
)
|
||||
aioclient_mock.get(
|
||||
f"{DEFAULT_URL}/api/v3/content",
|
||||
params={"language": "en"},
|
||||
json=load_json_object_fixture("content.json", DOMAIN),
|
||||
)
|
||||
aioclient_mock.get(
|
||||
f"{DEFAULT_URL}/api/v3/user/anonymized",
|
||||
json={
|
||||
"data": {
|
||||
"user": load_json_object_fixture("user.json", DOMAIN)["data"],
|
||||
"tasks": load_json_object_fixture("tasks.json", DOMAIN)["data"],
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return aioclient_mock
|
||||
ERROR_RESPONSE = HabiticaErrorResponse(success=False, error="error", message="message")
|
||||
ERROR_NOT_AUTHORIZED = NotAuthorizedError(error=ERROR_RESPONSE, headers={})
|
||||
ERROR_NOT_FOUND = NotFoundError(error=ERROR_RESPONSE, headers={})
|
||||
ERROR_BAD_REQUEST = BadRequestError(error=ERROR_RESPONSE, headers={})
|
||||
ERROR_TOO_MANY_REQUESTS = TooManyRequestsError(error=ERROR_RESPONSE, headers={})
|
||||
|
||||
|
||||
@pytest.fixture(name="config_entry")
|
||||
@ -82,10 +46,10 @@ def mock_config_entry() -> MockConfigEntry:
|
||||
title="test-user",
|
||||
data={
|
||||
CONF_URL: DEFAULT_URL,
|
||||
CONF_API_USER: "test-api-user",
|
||||
CONF_API_KEY: "test-api-key",
|
||||
CONF_API_USER: "a380546a-94be-4b8e-8a0b-23e0d5c03303",
|
||||
CONF_API_KEY: "cd0e5985-17de-4b4f-849e-5d506c5e4382",
|
||||
},
|
||||
unique_id="00000000-0000-0000-0000-000000000000",
|
||||
unique_id="a380546a-94be-4b8e-8a0b-23e0d5c03303",
|
||||
)
|
||||
|
||||
|
||||
@ -93,3 +57,109 @@ def mock_config_entry() -> MockConfigEntry:
|
||||
async def set_tz(hass: HomeAssistant) -> None:
|
||||
"""Fixture to set timezone."""
|
||||
await hass.config.async_set_time_zone("Europe/Berlin")
|
||||
|
||||
|
||||
def mock_get_tasks(task_type: TaskFilter | None = None) -> HabiticaTasksResponse:
|
||||
"""Load tasks fixtures."""
|
||||
|
||||
if task_type is TaskFilter.COMPLETED_TODOS:
|
||||
return HabiticaTasksResponse.from_json(
|
||||
load_fixture("completed_todos.json", DOMAIN)
|
||||
)
|
||||
return HabiticaTasksResponse.from_json(load_fixture("tasks.json", DOMAIN))
|
||||
|
||||
|
||||
@pytest.fixture(name="habitica")
|
||||
async def mock_habiticalib() -> Generator[AsyncMock]:
|
||||
"""Mock habiticalib."""
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.habitica.Habitica", autospec=True
|
||||
) as mock_client,
|
||||
patch(
|
||||
"homeassistant.components.habitica.config_flow.Habitica", new=mock_client
|
||||
),
|
||||
):
|
||||
client = mock_client.return_value
|
||||
|
||||
client.login.return_value = HabiticaLoginResponse.from_json(
|
||||
load_fixture("login.json", DOMAIN)
|
||||
)
|
||||
|
||||
client.get_user.return_value = HabiticaUserResponse.from_json(
|
||||
load_fixture("user.json", DOMAIN)
|
||||
)
|
||||
|
||||
client.cast_skill.return_value = HabiticaUserResponse.from_json(
|
||||
load_fixture("user.json", DOMAIN)
|
||||
)
|
||||
client.toggle_sleep.return_value = HabiticaSleepResponse(
|
||||
success=True, data=True
|
||||
)
|
||||
client.update_score.return_value = HabiticaUserResponse.from_json(
|
||||
load_fixture("score_with_drop.json", DOMAIN)
|
||||
)
|
||||
client.get_group_members.return_value = HabiticaGroupMembersResponse.from_json(
|
||||
load_fixture("party_members.json", DOMAIN)
|
||||
)
|
||||
for func in (
|
||||
"leave_quest",
|
||||
"reject_quest",
|
||||
"cancel_quest",
|
||||
"abort_quest",
|
||||
"start_quest",
|
||||
"accept_quest",
|
||||
):
|
||||
getattr(client, func).return_value = HabiticaQuestResponse.from_json(
|
||||
load_fixture("party_quest.json", DOMAIN)
|
||||
)
|
||||
client.get_content.return_value = HabiticaContentResponse.from_json(
|
||||
load_fixture("content.json", DOMAIN)
|
||||
)
|
||||
client.get_tasks.side_effect = mock_get_tasks
|
||||
client.update_score.return_value = HabiticaScoreResponse.from_json(
|
||||
load_fixture("score_with_drop.json", DOMAIN)
|
||||
)
|
||||
client.update_task.return_value = HabiticaTaskResponse.from_json(
|
||||
load_fixture("task.json", DOMAIN)
|
||||
)
|
||||
client.create_task.return_value = HabiticaTaskResponse.from_json(
|
||||
load_fixture("task.json", DOMAIN)
|
||||
)
|
||||
client.delete_task.return_value = HabiticaResponse.from_dict(
|
||||
{"data": {}, "success": True}
|
||||
)
|
||||
client.delete_completed_todos.return_value = HabiticaResponse.from_dict(
|
||||
{"data": {}, "success": True}
|
||||
)
|
||||
client.reorder_task.return_value = HabiticaTaskOrderResponse.from_dict(
|
||||
{"data": [], "success": True}
|
||||
)
|
||||
client.get_user_anonymized.return_value = (
|
||||
HabiticaUserAnonymizedrResponse.from_json(
|
||||
load_fixture("anonymized.json", DOMAIN)
|
||||
)
|
||||
)
|
||||
client.habitipy.return_value = {
|
||||
"tasks": {
|
||||
"user": {
|
||||
"post": AsyncMock(
|
||||
return_value={
|
||||
"text": "Use API from Home Assistant",
|
||||
"type": "todo",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.habitica.async_setup_entry", return_value=True
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
||||
|
661
tests/components/habitica/fixtures/anonymized.json
Normal file
661
tests/components/habitica/fixtures/anonymized.json
Normal file
@ -0,0 +1,661 @@
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"user": {
|
||||
"auth": {
|
||||
"timestamps": {
|
||||
"created": "2024-10-10T15:57:01.106Z",
|
||||
"loggedin": "2024-11-27T19:34:28.887Z",
|
||||
"updated": "2024-11-27T20:05:19.047Z"
|
||||
}
|
||||
},
|
||||
"achievements": {
|
||||
"ultimateGearSets": {
|
||||
"healer": false,
|
||||
"wizard": false,
|
||||
"rogue": false,
|
||||
"warrior": false
|
||||
},
|
||||
"streak": 0,
|
||||
"perfect": 2,
|
||||
"quests": {},
|
||||
"completedTask": true,
|
||||
"partyUp": true,
|
||||
"snowball": 1,
|
||||
"spookySparkles": 1,
|
||||
"seafoam": 1,
|
||||
"shinySeed": 1,
|
||||
"createdTask": true
|
||||
},
|
||||
"backer": {},
|
||||
"purchased": {
|
||||
"ads": false,
|
||||
"txnCount": 0,
|
||||
"skin": {},
|
||||
"hair": {},
|
||||
"shirt": {},
|
||||
"background": {
|
||||
"violet": true,
|
||||
"blue": true,
|
||||
"green": true,
|
||||
"purple": true,
|
||||
"red": true,
|
||||
"yellow": true
|
||||
}
|
||||
},
|
||||
"flags": {
|
||||
"tour": {
|
||||
"intro": -1,
|
||||
"classes": -1,
|
||||
"stats": -1,
|
||||
"tavern": -1,
|
||||
"party": -1,
|
||||
"guilds": -1,
|
||||
"challenges": -1,
|
||||
"market": -1,
|
||||
"pets": -1,
|
||||
"mounts": -1,
|
||||
"hall": -1,
|
||||
"equipment": -1,
|
||||
"groupPlans": -1
|
||||
},
|
||||
"tutorial": {
|
||||
"common": {
|
||||
"habits": true,
|
||||
"dailies": true,
|
||||
"todos": true,
|
||||
"rewards": true,
|
||||
"party": true,
|
||||
"pets": true,
|
||||
"gems": true,
|
||||
"skills": true,
|
||||
"classes": true,
|
||||
"tavern": true,
|
||||
"equipment": true,
|
||||
"items": true,
|
||||
"mounts": true,
|
||||
"inbox": true,
|
||||
"stats": true
|
||||
},
|
||||
"ios": {
|
||||
"addTask": false,
|
||||
"editTask": false,
|
||||
"deleteTask": false,
|
||||
"filterTask": false,
|
||||
"groupPets": false,
|
||||
"inviteParty": false,
|
||||
"reorderTask": false
|
||||
}
|
||||
},
|
||||
"verifiedUsername": true,
|
||||
"customizationsNotification": true,
|
||||
"showTour": true,
|
||||
"dropsEnabled": false,
|
||||
"itemsEnabled": true,
|
||||
"lastNewStuffRead": "",
|
||||
"rewrite": true,
|
||||
"classSelected": false,
|
||||
"rebirthEnabled": false,
|
||||
"levelDrops": {},
|
||||
"recaptureEmailsPhase": 0,
|
||||
"weeklyRecapEmailsPhase": 0,
|
||||
"lastWeeklyRecap": "2024-10-10T15:57:01.106Z",
|
||||
"communityGuidelinesAccepted": true,
|
||||
"cronCount": 6,
|
||||
"welcomed": true,
|
||||
"armoireEnabled": true,
|
||||
"armoireOpened": false,
|
||||
"armoireEmpty": false,
|
||||
"cardReceived": false,
|
||||
"warnedLowHealth": true,
|
||||
"thirdPartyTools": "2024-11-27T19:32:18.826Z",
|
||||
"newStuff": false
|
||||
},
|
||||
"history": {
|
||||
"exp": [
|
||||
{
|
||||
"date": "2024-10-30T19:37:01.970Z",
|
||||
"value": 24
|
||||
},
|
||||
{
|
||||
"date": "2024-10-31T23:33:14.972Z",
|
||||
"value": 48
|
||||
},
|
||||
{
|
||||
"date": "2024-11-05T18:25:04.681Z",
|
||||
"value": 66
|
||||
},
|
||||
{
|
||||
"date": "2024-11-21T15:09:07.501Z",
|
||||
"value": 66
|
||||
},
|
||||
{
|
||||
"date": "2024-11-22T00:41:21.137Z",
|
||||
"value": 66
|
||||
},
|
||||
{
|
||||
"date": "2024-11-27T19:34:28.887Z",
|
||||
"value": 66
|
||||
}
|
||||
],
|
||||
"todos": [
|
||||
{
|
||||
"date": "2024-10-30T19:37:01.970Z",
|
||||
"value": -5
|
||||
},
|
||||
{
|
||||
"date": "2024-10-31T23:33:14.972Z",
|
||||
"value": -10.129783523135325
|
||||
},
|
||||
{
|
||||
"date": "2024-11-05T18:25:04.681Z",
|
||||
"value": -16.396221153338182
|
||||
},
|
||||
{
|
||||
"date": "2024-11-21T15:09:07.501Z",
|
||||
"value": -22.8326979965846
|
||||
},
|
||||
{
|
||||
"date": "2024-11-22T00:41:21.137Z",
|
||||
"value": -29.448636229365235
|
||||
},
|
||||
{
|
||||
"date": "2024-11-27T19:34:28.887Z",
|
||||
"value": -36.25425987861077
|
||||
}
|
||||
]
|
||||
},
|
||||
"items": {
|
||||
"gear": {
|
||||
"equipped": {
|
||||
"armor": "armor_base_0",
|
||||
"head": "head_base_0",
|
||||
"shield": "shield_base_0"
|
||||
},
|
||||
"costume": {
|
||||
"armor": "armor_base_0",
|
||||
"head": "head_base_0",
|
||||
"shield": "shield_base_0"
|
||||
},
|
||||
"owned": {
|
||||
"headAccessory_special_blackHeadband": true,
|
||||
"headAccessory_special_blueHeadband": true,
|
||||
"headAccessory_special_greenHeadband": true,
|
||||
"headAccessory_special_pinkHeadband": true,
|
||||
"headAccessory_special_redHeadband": true,
|
||||
"headAccessory_special_whiteHeadband": true,
|
||||
"headAccessory_special_yellowHeadband": true,
|
||||
"eyewear_special_blackTopFrame": true,
|
||||
"eyewear_special_blueTopFrame": true,
|
||||
"eyewear_special_greenTopFrame": true,
|
||||
"eyewear_special_pinkTopFrame": true,
|
||||
"eyewear_special_redTopFrame": true,
|
||||
"eyewear_special_whiteTopFrame": true,
|
||||
"eyewear_special_yellowTopFrame": true,
|
||||
"eyewear_special_blackHalfMoon": true,
|
||||
"eyewear_special_blueHalfMoon": true,
|
||||
"eyewear_special_greenHalfMoon": true,
|
||||
"eyewear_special_pinkHalfMoon": true,
|
||||
"eyewear_special_redHalfMoon": true,
|
||||
"eyewear_special_whiteHalfMoon": true,
|
||||
"eyewear_special_yellowHalfMoon": true,
|
||||
"armor_special_bardRobes": true,
|
||||
"head_special_bardHat": true
|
||||
}
|
||||
},
|
||||
"special": {
|
||||
"snowball": 0,
|
||||
"spookySparkles": 0,
|
||||
"shinySeed": 0,
|
||||
"seafoam": 0,
|
||||
"valentine": 0,
|
||||
"nye": 0,
|
||||
"greeting": 0,
|
||||
"greetingReceived": [],
|
||||
"thankyou": 0,
|
||||
"thankyouReceived": [],
|
||||
"birthday": 0,
|
||||
"birthdayReceived": [],
|
||||
"congrats": 0,
|
||||
"congratsReceived": [],
|
||||
"getwell": 0,
|
||||
"getwellReceived": [],
|
||||
"goodluck": 0,
|
||||
"goodluckReceived": []
|
||||
},
|
||||
"lastDrop": {
|
||||
"count": 0,
|
||||
"date": "2024-11-05T18:25:04.619Z"
|
||||
},
|
||||
"currentPet": "",
|
||||
"currentMount": "",
|
||||
"pets": {},
|
||||
"eggs": {
|
||||
"BearCub": 1,
|
||||
"Cactus": 1,
|
||||
"Wolf": 1,
|
||||
"Dragon": 1
|
||||
},
|
||||
"hatchingPotions": {
|
||||
"Skeleton": 1,
|
||||
"Zombie": 1,
|
||||
"RoyalPurple": 1
|
||||
},
|
||||
"food": {
|
||||
"Candy_Red": 1,
|
||||
"Chocolate": 1,
|
||||
"Meat": 1,
|
||||
"CottonCandyPink": 1
|
||||
},
|
||||
"mounts": {},
|
||||
"quests": {
|
||||
"dustbunnies": 1
|
||||
}
|
||||
},
|
||||
"party": {
|
||||
"quest": {
|
||||
"progress": {
|
||||
"up": 0,
|
||||
"down": 0,
|
||||
"collectedItems": 0,
|
||||
"collect": {}
|
||||
},
|
||||
"RSVPNeeded": true,
|
||||
"key": "dustbunnies",
|
||||
"completed": null
|
||||
},
|
||||
"order": "level",
|
||||
"orderAscending": "ascending",
|
||||
"_id": "94cd398c-2240-4320-956e-6d345cf2c0de"
|
||||
},
|
||||
"preferences": {
|
||||
"hair": {
|
||||
"color": "red",
|
||||
"base": 3,
|
||||
"bangs": 1,
|
||||
"beard": 0,
|
||||
"mustache": 0,
|
||||
"flower": 1
|
||||
},
|
||||
"emailNotifications": {
|
||||
"unsubscribeFromAll": false,
|
||||
"newPM": true,
|
||||
"kickedGroup": true,
|
||||
"wonChallenge": true,
|
||||
"giftedGems": true,
|
||||
"giftedSubscription": true,
|
||||
"invitedParty": true,
|
||||
"invitedGuild": true,
|
||||
"questStarted": true,
|
||||
"invitedQuest": true,
|
||||
"importantAnnouncements": true,
|
||||
"weeklyRecaps": true,
|
||||
"onboarding": true,
|
||||
"majorUpdates": true,
|
||||
"subscriptionReminders": true,
|
||||
"contentRelease": true
|
||||
},
|
||||
"pushNotifications": {
|
||||
"unsubscribeFromAll": false,
|
||||
"newPM": true,
|
||||
"wonChallenge": true,
|
||||
"giftedGems": true,
|
||||
"giftedSubscription": true,
|
||||
"invitedParty": true,
|
||||
"invitedGuild": true,
|
||||
"questStarted": true,
|
||||
"invitedQuest": true,
|
||||
"majorUpdates": true,
|
||||
"mentionParty": true,
|
||||
"mentionJoinedGuild": true,
|
||||
"mentionUnjoinedGuild": true,
|
||||
"partyActivity": true,
|
||||
"contentRelease": true
|
||||
},
|
||||
"suppressModals": {
|
||||
"levelUp": false,
|
||||
"hatchPet": false,
|
||||
"raisePet": false,
|
||||
"streak": false
|
||||
},
|
||||
"tasks": {
|
||||
"activeFilter": {
|
||||
"habit": "all",
|
||||
"daily": "all",
|
||||
"todo": "remaining",
|
||||
"reward": "all"
|
||||
},
|
||||
"groupByChallenge": false,
|
||||
"confirmScoreNotes": false,
|
||||
"mirrorGroupTasks": []
|
||||
},
|
||||
"language": "de",
|
||||
"dayStart": 0,
|
||||
"size": "slim",
|
||||
"hideHeader": false,
|
||||
"skin": "915533",
|
||||
"shirt": "blue",
|
||||
"timezoneOffset": -60,
|
||||
"sound": "rosstavoTheme",
|
||||
"chair": "none",
|
||||
"allocationMode": "flat",
|
||||
"autoEquip": true,
|
||||
"costume": false,
|
||||
"dateFormat": "MM/dd/yyyy",
|
||||
"sleep": false,
|
||||
"stickyHeader": true,
|
||||
"disableClasses": false,
|
||||
"newTaskEdit": false,
|
||||
"dailyDueDefaultView": false,
|
||||
"advancedCollapsed": false,
|
||||
"toolbarCollapsed": false,
|
||||
"reverseChatOrder": false,
|
||||
"developerMode": false,
|
||||
"displayInviteToPartyWhenPartyIs1": true,
|
||||
"webhooks": {},
|
||||
"improvementCategories": [],
|
||||
"background": "violet",
|
||||
"timezoneOffsetAtLastCron": -60
|
||||
},
|
||||
"stats": {
|
||||
"buffs": {
|
||||
"str": 0,
|
||||
"int": 0,
|
||||
"per": 0,
|
||||
"con": 0,
|
||||
"stealth": 0,
|
||||
"streaks": false,
|
||||
"seafoam": false,
|
||||
"shinySeed": false,
|
||||
"snowball": false,
|
||||
"spookySparkles": false
|
||||
},
|
||||
"training": {
|
||||
"int": 0,
|
||||
"per": 0,
|
||||
"str": 0,
|
||||
"con": 0
|
||||
},
|
||||
"hp": 25.40000000000002,
|
||||
"mp": 32,
|
||||
"exp": 41,
|
||||
"gp": 11.100978952781748,
|
||||
"lvl": 2,
|
||||
"class": "warrior",
|
||||
"points": 2,
|
||||
"str": 0,
|
||||
"con": 0,
|
||||
"int": 0,
|
||||
"per": 0,
|
||||
"toNextLevel": 50,
|
||||
"maxHealth": 50,
|
||||
"maxMP": 32
|
||||
},
|
||||
"inbox": {
|
||||
"newMessages": 0,
|
||||
"optOut": false,
|
||||
"blocks": [],
|
||||
"messages": {}
|
||||
},
|
||||
"tasksOrder": {
|
||||
"habits": ["30923acd-3b4c-486d-9ef3-c8f57cf56049"],
|
||||
"dailys": ["6e53f1f5-a315-4edd-984d-8d762e4a08ef"],
|
||||
"todos": ["e6e06dc6-c887-4b86-b175-b99cc2e20fdf"],
|
||||
"rewards": ["2fbf11a5-ab1e-4fb7-97f0-dfb5c45c96a9"]
|
||||
},
|
||||
"_id": "ffce870c-3ff3-4fa4-bad1-87612e52b8e7",
|
||||
"_v": 131,
|
||||
"balance": 0,
|
||||
"_subSignature": "NOT_RUNNING",
|
||||
"challenges": [],
|
||||
"guilds": [],
|
||||
"loginIncentives": 6,
|
||||
"invitesSent": 0,
|
||||
"pinnedItemsOrder": [],
|
||||
"lastCron": "2024-11-27T19:34:28.887Z",
|
||||
"tags": [
|
||||
{
|
||||
"id": "c1a35186-9895-4ac0-9cd7-49e7bb875695",
|
||||
"name": "tag",
|
||||
"challenge": "challenge"
|
||||
},
|
||||
{
|
||||
"id": "53d1deb8-ed2b-4f94-bbfc-955e9e92aa98",
|
||||
"name": "tag",
|
||||
"challenge": "challenge"
|
||||
},
|
||||
{
|
||||
"id": "29bf6a99-536f-446b-838f-a81d41e1ed4d",
|
||||
"name": "tag",
|
||||
"challenge": "challenge"
|
||||
},
|
||||
{
|
||||
"id": "1b1297e7-4fd8-460a-b148-e92d7bcfa9a5",
|
||||
"name": "tag",
|
||||
"challenge": "challenge"
|
||||
},
|
||||
{
|
||||
"id": "05e6cf40-48ea-415a-9b8b-e2ecad258ef6",
|
||||
"name": "tag",
|
||||
"challenge": "challenge"
|
||||
},
|
||||
{
|
||||
"id": "fe53f179-59d8-4c28-9bf7-b9068ab552a4",
|
||||
"name": "tag",
|
||||
"challenge": "challenge"
|
||||
},
|
||||
{
|
||||
"id": "c44e9e8c-4bff-42df-98d5-1a1a7b69eada",
|
||||
"name": "tag",
|
||||
"challenge": "challenge"
|
||||
}
|
||||
],
|
||||
"extra": {},
|
||||
"pushDevices": [],
|
||||
"pinnedItems": [
|
||||
{
|
||||
"path": "gear.flat.weapon_warrior_0",
|
||||
"type": "marketGear"
|
||||
},
|
||||
{
|
||||
"path": "gear.flat.armor_warrior_1",
|
||||
"type": "marketGear"
|
||||
},
|
||||
{
|
||||
"path": "gear.flat.shield_warrior_1",
|
||||
"type": "marketGear"
|
||||
},
|
||||
{
|
||||
"path": "gear.flat.head_warrior_1",
|
||||
"type": "marketGear"
|
||||
},
|
||||
{
|
||||
"path": "potion",
|
||||
"type": "potion"
|
||||
},
|
||||
{
|
||||
"path": "armoire",
|
||||
"type": "armoire"
|
||||
}
|
||||
],
|
||||
"unpinnedItems": [],
|
||||
"id": "ffce870c-3ff3-4fa4-bad1-87612e52b8e7"
|
||||
},
|
||||
"tasks": [
|
||||
{
|
||||
"challenge": {},
|
||||
"group": {
|
||||
"completedBy": {},
|
||||
"assignedUsers": []
|
||||
},
|
||||
"_id": "30923acd-3b4c-486d-9ef3-c8f57cf56049",
|
||||
"up": true,
|
||||
"down": false,
|
||||
"counterUp": 0,
|
||||
"counterDown": 0,
|
||||
"frequency": "daily",
|
||||
"history": [],
|
||||
"type": "habit",
|
||||
"text": "task text",
|
||||
"notes": "task notes",
|
||||
"tags": [],
|
||||
"value": 0,
|
||||
"priority": 1,
|
||||
"attribute": "str",
|
||||
"byHabitica": false,
|
||||
"reminders": [],
|
||||
"createdAt": "2024-10-10T15:57:14.287Z",
|
||||
"updatedAt": "2024-10-10T15:57:14.287Z",
|
||||
"userId": "ffce870c-3ff3-4fa4-bad1-87612e52b8e7",
|
||||
"id": "30923acd-3b4c-486d-9ef3-c8f57cf56049"
|
||||
},
|
||||
{
|
||||
"challenge": {},
|
||||
"group": {
|
||||
"completedBy": {},
|
||||
"assignedUsers": []
|
||||
},
|
||||
"_id": "e6e06dc6-c887-4b86-b175-b99cc2e20fdf",
|
||||
"completed": false,
|
||||
"collapseChecklist": false,
|
||||
"type": "todo",
|
||||
"text": "task text",
|
||||
"notes": "task notes",
|
||||
"tags": [],
|
||||
"value": -6.418582324043852,
|
||||
"priority": 1,
|
||||
"attribute": "str",
|
||||
"byHabitica": true,
|
||||
"checklist": [],
|
||||
"reminders": [],
|
||||
"createdAt": "2024-10-10T15:57:14.290Z",
|
||||
"updatedAt": "2024-11-27T19:34:29.001Z",
|
||||
"userId": "ffce870c-3ff3-4fa4-bad1-87612e52b8e7",
|
||||
"id": "e6e06dc6-c887-4b86-b175-b99cc2e20fdf"
|
||||
},
|
||||
{
|
||||
"challenge": {},
|
||||
"group": {
|
||||
"completedBy": {},
|
||||
"assignedUsers": []
|
||||
},
|
||||
"_id": "2fbf11a5-ab1e-4fb7-97f0-dfb5c45c96a9",
|
||||
"type": "reward",
|
||||
"text": "task text",
|
||||
"notes": "task notes",
|
||||
"tags": [],
|
||||
"value": 10,
|
||||
"priority": 1,
|
||||
"attribute": "str",
|
||||
"byHabitica": false,
|
||||
"reminders": [],
|
||||
"createdAt": "2024-10-10T15:57:14.290Z",
|
||||
"updatedAt": "2024-10-10T15:57:14.290Z",
|
||||
"userId": "ffce870c-3ff3-4fa4-bad1-87612e52b8e7",
|
||||
"id": "2fbf11a5-ab1e-4fb7-97f0-dfb5c45c96a9"
|
||||
},
|
||||
{
|
||||
"repeat": {
|
||||
"m": true,
|
||||
"t": true,
|
||||
"w": true,
|
||||
"th": true,
|
||||
"f": true,
|
||||
"s": true,
|
||||
"su": true
|
||||
},
|
||||
"challenge": {},
|
||||
"group": {
|
||||
"completedBy": {},
|
||||
"assignedUsers": []
|
||||
},
|
||||
"_id": "6e53f1f5-a315-4edd-984d-8d762e4a08ef",
|
||||
"frequency": "weekly",
|
||||
"everyX": 1,
|
||||
"streak": 0,
|
||||
"nextDue": [
|
||||
"2024-11-27T23:00:00.000Z",
|
||||
"2024-11-28T23:00:00.000Z",
|
||||
"2024-11-29T23:00:00.000Z",
|
||||
"2024-12-01T23:00:00.000Z",
|
||||
"2024-12-02T23:00:00.000Z",
|
||||
"2024-12-03T23:00:00.000Z"
|
||||
],
|
||||
"yesterDaily": true,
|
||||
"history": [
|
||||
{
|
||||
"date": 1730317021817,
|
||||
"value": 1,
|
||||
"isDue": true,
|
||||
"completed": true
|
||||
},
|
||||
{
|
||||
"date": 1730417594890,
|
||||
"value": 1.9747,
|
||||
"isDue": true,
|
||||
"completed": true
|
||||
},
|
||||
{
|
||||
"date": 1730831104730,
|
||||
"value": 1.024043774264157,
|
||||
"isDue": true,
|
||||
"completed": false
|
||||
},
|
||||
{
|
||||
"date": 1732201747573,
|
||||
"value": 0.049944135963563174,
|
||||
"isDue": true,
|
||||
"completed": false
|
||||
},
|
||||
{
|
||||
"date": 1732236081228,
|
||||
"value": -0.9487768368544092,
|
||||
"isDue": true,
|
||||
"completed": false
|
||||
},
|
||||
{
|
||||
"date": 1732736068973,
|
||||
"value": -1.973387732005249,
|
||||
"isDue": true,
|
||||
"completed": false
|
||||
}
|
||||
],
|
||||
"completed": false,
|
||||
"collapseChecklist": false,
|
||||
"type": "daily",
|
||||
"text": "task text",
|
||||
"notes": "task notes",
|
||||
"tags": [],
|
||||
"value": -1.973387732005249,
|
||||
"priority": 1,
|
||||
"attribute": "str",
|
||||
"byHabitica": false,
|
||||
"startDate": "2024-10-09T22:00:00.000Z",
|
||||
"daysOfMonth": [],
|
||||
"weeksOfMonth": [],
|
||||
"checklist": [],
|
||||
"reminders": [],
|
||||
"createdAt": "2024-10-10T15:57:14.304Z",
|
||||
"updatedAt": "2024-11-27T19:34:29.001Z",
|
||||
"userId": "ffce870c-3ff3-4fa4-bad1-87612e52b8e7",
|
||||
"isDue": true,
|
||||
"id": "6e53f1f5-a315-4edd-984d-8d762e4a08ef"
|
||||
}
|
||||
]
|
||||
},
|
||||
"notifications": [
|
||||
{
|
||||
"id": "35708dfc-74c6-4352-97d3-8aeddbb31d20",
|
||||
"type": "NEW_CHAT_MESSAGE",
|
||||
"data": {
|
||||
"group": {
|
||||
"id": "94cd398c-2240-4320-956e-6d345cf2c0de",
|
||||
"name": "tests Party"
|
||||
}
|
||||
},
|
||||
"seen": false
|
||||
}
|
||||
],
|
||||
"userV": 131,
|
||||
"appVersion": "5.29.2"
|
||||
}
|
@ -2,6 +2,88 @@
|
||||
"success": true,
|
||||
"data": {
|
||||
"gear": {
|
||||
"tree": {
|
||||
"weapon": {
|
||||
"base": {},
|
||||
"warrior": {},
|
||||
"wizard": {},
|
||||
"rogue": {},
|
||||
"special": {},
|
||||
"armoire": {},
|
||||
"mystery": {},
|
||||
"healer": {}
|
||||
},
|
||||
"armor": {
|
||||
"base": {},
|
||||
"warrior": {},
|
||||
"wizard": {},
|
||||
"rogue": {},
|
||||
"special": {},
|
||||
"armoire": {},
|
||||
"mystery": {},
|
||||
"healer": {}
|
||||
},
|
||||
"head": {
|
||||
"base": {},
|
||||
"warrior": {},
|
||||
"wizard": {},
|
||||
"rogue": {},
|
||||
"special": {},
|
||||
"armoire": {},
|
||||
"mystery": {},
|
||||
"healer": {}
|
||||
},
|
||||
"shield": {
|
||||
"base": {},
|
||||
"warrior": {},
|
||||
"wizard": {},
|
||||
"rogue": {},
|
||||
"special": {},
|
||||
"armoire": {},
|
||||
"mystery": {},
|
||||
"healer": {}
|
||||
},
|
||||
"back": {
|
||||
"base": {},
|
||||
"warrior": {},
|
||||
"wizard": {},
|
||||
"rogue": {},
|
||||
"special": {},
|
||||
"armoire": {},
|
||||
"mystery": {},
|
||||
"healer": {}
|
||||
},
|
||||
"body": {
|
||||
"base": {},
|
||||
"warrior": {},
|
||||
"wizard": {},
|
||||
"rogue": {},
|
||||
"special": {},
|
||||
"armoire": {},
|
||||
"mystery": {},
|
||||
"healer": {}
|
||||
},
|
||||
"headAccessory": {
|
||||
"base": {},
|
||||
"warrior": {},
|
||||
"wizard": {},
|
||||
"rogue": {},
|
||||
"special": {},
|
||||
"armoire": {},
|
||||
"mystery": {},
|
||||
"healer": {}
|
||||
},
|
||||
"eyewear": {
|
||||
"base": {},
|
||||
"warrior": {},
|
||||
"wizard": {},
|
||||
"rogue": {},
|
||||
"special": {},
|
||||
"armoire": {},
|
||||
"mystery": {},
|
||||
"healer": {}
|
||||
}
|
||||
},
|
||||
"flat": {
|
||||
"weapon_warrior_5": {
|
||||
"text": "Ruby Sword",
|
||||
@ -281,7 +363,110 @@
|
||||
"per": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"achievements": {},
|
||||
"questSeriesAchievements": {},
|
||||
"animalColorAchievements": [],
|
||||
"animalSetAchievements": {},
|
||||
"stableAchievements": {},
|
||||
"petSetCompleteAchievs": [],
|
||||
"quests": {},
|
||||
"questsByLevel": {},
|
||||
"userCanOwnQuestCategories": [],
|
||||
"itemList": {
|
||||
"weapon": {
|
||||
"localeKey": "weapon",
|
||||
"isEquipment": true
|
||||
},
|
||||
"armor": {
|
||||
"localeKey": "armor",
|
||||
"isEquipment": true
|
||||
},
|
||||
"head": {
|
||||
"localeKey": "headgear",
|
||||
"isEquipment": true
|
||||
},
|
||||
"shield": {
|
||||
"localeKey": "offhand",
|
||||
"isEquipment": true
|
||||
},
|
||||
"back": {
|
||||
"localeKey": "back",
|
||||
"isEquipment": true
|
||||
},
|
||||
"body": {
|
||||
"localeKey": "body",
|
||||
"isEquipment": true
|
||||
},
|
||||
"headAccessory": {
|
||||
"localeKey": "headAccessory",
|
||||
"isEquipment": true
|
||||
},
|
||||
"eyewear": {
|
||||
"localeKey": "eyewear",
|
||||
"isEquipment": true
|
||||
},
|
||||
"hatchingPotions": {
|
||||
"localeKey": "hatchingPotion",
|
||||
"isEquipment": false
|
||||
},
|
||||
"premiumHatchingPotions": {
|
||||
"localeKey": "hatchingPotion",
|
||||
"isEquipment": false
|
||||
},
|
||||
"eggs": {
|
||||
"localeKey": "eggSingular",
|
||||
"isEquipment": false
|
||||
},
|
||||
"quests": {
|
||||
"localeKey": "quest",
|
||||
"isEquipment": false
|
||||
},
|
||||
"food": {
|
||||
"localeKey": "foodTextThe",
|
||||
"isEquipment": false
|
||||
},
|
||||
"Saddle": {
|
||||
"localeKey": "foodSaddleText",
|
||||
"isEquipment": false
|
||||
},
|
||||
"bundles": {
|
||||
"localeKey": "discountBundle",
|
||||
"isEquipment": false
|
||||
}
|
||||
},
|
||||
"spells": {
|
||||
"wizard": {},
|
||||
"warrior": {},
|
||||
"rogue": {},
|
||||
"healer": {},
|
||||
"special": {}
|
||||
},
|
||||
"officialPinnedItems": [],
|
||||
"audioThemes": [],
|
||||
"classes": [],
|
||||
"gearTypes": [],
|
||||
"cardTypes": {},
|
||||
"special": {},
|
||||
"dropEggs": {},
|
||||
"questEggs": {},
|
||||
"eggs": {},
|
||||
"dropHatchingPotions": {},
|
||||
"premiumHatchingPotions": {},
|
||||
"wackyHatchingPotions": {},
|
||||
"hatchingPotions": {},
|
||||
"pets": {},
|
||||
"premiumPets": {},
|
||||
"questPets": {},
|
||||
"specialPets": {},
|
||||
"wackyPets": {},
|
||||
"petInfo": {},
|
||||
"mounts": {},
|
||||
"premiumMounts": {},
|
||||
"questMounts": {},
|
||||
"specialMounts": {},
|
||||
"mountInfo": {},
|
||||
"food": {}
|
||||
},
|
||||
"appVersion": "5.29.2"
|
||||
}
|
||||
|
10
tests/components/habitica/fixtures/login.json
Normal file
10
tests/components/habitica/fixtures/login.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "a380546a-94be-4b8e-8a0b-23e0d5c03303",
|
||||
"apiToken": "cd0e5985-17de-4b4f-849e-5d506c5e4382",
|
||||
"newUser": false,
|
||||
"username": "test-username"
|
||||
},
|
||||
"appVersion": "5.30.0"
|
||||
}
|
31
tests/components/habitica/fixtures/party_quest.json
Normal file
31
tests/components/habitica/fixtures/party_quest.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"progress": {
|
||||
"collect": {},
|
||||
"hp": 100
|
||||
},
|
||||
"key": "dustbunnies",
|
||||
"active": true,
|
||||
"leader": "a380546a-94be-4b8e-8a0b-23e0d5c03303",
|
||||
"members": {
|
||||
"a380546a-94be-4b8e-8a0b-23e0d5c03303": true
|
||||
},
|
||||
"extra": {}
|
||||
},
|
||||
"notifications": [
|
||||
{
|
||||
"id": "3f59313f-6d7c-4fff-a8c4-3b153d828c6f",
|
||||
"type": "NEW_CHAT_MESSAGE",
|
||||
"data": {
|
||||
"group": {
|
||||
"id": "94cd398c-2240-4320-956e-6d345cf2c0de",
|
||||
"name": "tests Party"
|
||||
}
|
||||
},
|
||||
"seen": false
|
||||
}
|
||||
],
|
||||
"userV": 287,
|
||||
"appVersion": "5.29.2"
|
||||
}
|
51
tests/components/habitica/fixtures/task.json
Normal file
51
tests/components/habitica/fixtures/task.json
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"_id": "88de7cd9-af2b-49ce-9afd-bf941d87336b",
|
||||
"date": "2024-09-27T22:17:00.000Z",
|
||||
"completed": false,
|
||||
"collapseChecklist": false,
|
||||
"checklist": [],
|
||||
"type": "todo",
|
||||
"text": "Buch zu Ende lesen",
|
||||
"notes": "Das Buch, das du angefangen hast, bis zum Wochenende fertig lesen.",
|
||||
"tags": [],
|
||||
"value": 0,
|
||||
"priority": 1,
|
||||
"attribute": "str",
|
||||
"challenge": {},
|
||||
"group": {
|
||||
"completedBy": {},
|
||||
"assignedUsers": []
|
||||
},
|
||||
"reminders": [],
|
||||
"byHabitica": false,
|
||||
"createdAt": "2024-09-21T22:17:57.816Z",
|
||||
"updatedAt": "2024-09-21T22:17:57.816Z",
|
||||
"userId": "5f359083-ef78-4af0-985a-0b2c6d05797c",
|
||||
"id": "88de7cd9-af2b-49ce-9afd-bf941d87336b"
|
||||
},
|
||||
"notifications": [
|
||||
{
|
||||
"type": "ITEM_RECEIVED",
|
||||
"data": {
|
||||
"icon": "notif_orca_mount",
|
||||
"title": "Orcas for Summer Splash!",
|
||||
"text": "To celebrate Summer Splash, we've given you an Orca Mount!",
|
||||
"destination": "stable"
|
||||
},
|
||||
"seen": true,
|
||||
"id": "b7a85df1-06ed-4ab1-b56d-43418fc6a5e5"
|
||||
},
|
||||
{
|
||||
"type": "UNALLOCATED_STATS_POINTS",
|
||||
"data": {
|
||||
"points": 2
|
||||
},
|
||||
"seen": true,
|
||||
"id": "bc3f8a69-231f-4eb1-ba48-a00b6c0e0f37"
|
||||
}
|
||||
],
|
||||
"userV": 589,
|
||||
"appVersion": "5.28.6"
|
||||
}
|
@ -83,6 +83,7 @@
|
||||
"body": "body_special_aetherAmulet"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"balance": 10
|
||||
}
|
||||
}
|
||||
|
86
tests/components/habitica/fixtures/user_no_party.json
Normal file
86
tests/components/habitica/fixtures/user_no_party.json
Normal file
@ -0,0 +1,86 @@
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"api_user": "test-api-user",
|
||||
"profile": { "name": "test-user" },
|
||||
"auth": { "local": { "username": "test-username" } },
|
||||
"stats": {
|
||||
"buffs": {
|
||||
"str": 26,
|
||||
"int": 26,
|
||||
"per": 26,
|
||||
"con": 26,
|
||||
"stealth": 0,
|
||||
"streaks": false,
|
||||
"seafoam": false,
|
||||
"shinySeed": false,
|
||||
"snowball": false,
|
||||
"spookySparkles": false
|
||||
},
|
||||
"hp": 0,
|
||||
"mp": 50.89999999999998,
|
||||
"exp": 737,
|
||||
"gp": 137.62587214609795,
|
||||
"lvl": 38,
|
||||
"class": "wizard",
|
||||
"maxHealth": 50,
|
||||
"maxMP": 166,
|
||||
"toNextLevel": 880,
|
||||
"points": 5,
|
||||
"str": 15,
|
||||
"con": 15,
|
||||
"int": 15,
|
||||
"per": 15
|
||||
},
|
||||
"preferences": {
|
||||
"sleep": false,
|
||||
"automaticAllocation": true,
|
||||
"disableClasses": false,
|
||||
"language": "en"
|
||||
},
|
||||
"flags": {
|
||||
"classSelected": true
|
||||
},
|
||||
"tasksOrder": {
|
||||
"rewards": ["5e2ea1df-f6e6-4ba3-bccb-97c5ec63e99b"],
|
||||
"todos": [
|
||||
"88de7cd9-af2b-49ce-9afd-bf941d87336b",
|
||||
"2f6fcabc-f670-4ec3-ba65-817e8deea490",
|
||||
"1aa3137e-ef72-4d1f-91ee-41933602f438",
|
||||
"86ea2475-d1b5-4020-bdcc-c188c7996afa"
|
||||
],
|
||||
"dailys": [
|
||||
"f21fa608-cfc6-4413-9fc7-0eb1b48ca43a",
|
||||
"bc1d1855-b2b8-4663-98ff-62e7b763dfc4",
|
||||
"e97659e0-2c42-4599-a7bb-00282adc410d",
|
||||
"564b9ac9-c53d-4638-9e7f-1cd96fe19baa",
|
||||
"f2c85972-1a19-4426-bc6d-ce3337b9d99f",
|
||||
"2c6d136c-a1c3-4bef-b7c4-fa980784b1e1"
|
||||
],
|
||||
"habits": ["1d147de6-5c02-4740-8e2f-71d3015a37f4"]
|
||||
},
|
||||
"party": {
|
||||
"quest": {
|
||||
"RSVPNeeded": true,
|
||||
"key": "dustbunnies"
|
||||
}
|
||||
},
|
||||
"needsCron": true,
|
||||
"lastCron": "2024-09-21T22:01:55.586Z",
|
||||
"id": "a380546a-94be-4b8e-8a0b-23e0d5c03303",
|
||||
"items": {
|
||||
"gear": {
|
||||
"equipped": {
|
||||
"weapon": "weapon_warrior_5",
|
||||
"armor": "armor_warrior_5",
|
||||
"head": "head_warrior_5",
|
||||
"shield": "shield_warrior_5",
|
||||
"back": "back_special_heroicAureole",
|
||||
"headAccessory": "headAccessory_armoire_gogglesOfBookbinding",
|
||||
"eyewear": "eyewear_armoire_plagueDoctorMask",
|
||||
"body": "body_special_aetherAmulet"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -28,7 +28,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabiticaBinarySensor.PENDING_QUEST: 'pending_quest'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_pending_quest',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_pending_quest',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
|
@ -28,7 +28,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipyButtonEntity.ALLOCATE_ALL_STAT_POINTS: 'allocate_all_stat_points'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_allocate_all_stat_points',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_allocate_all_stat_points',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
@ -74,7 +74,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipyButtonEntity.HEAL_ALL: 'heal_all'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_heal_all',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_heal_all',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
@ -121,7 +121,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipyButtonEntity.BUY_HEALTH_POTION: 'buy_health_potion'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_buy_health_potion',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_buy_health_potion',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
@ -168,7 +168,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipyButtonEntity.HEAL: 'heal'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_heal',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_heal',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
@ -215,7 +215,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipyButtonEntity.PROTECT_AURA: 'protect_aura'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_protect_aura',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_protect_aura',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
@ -262,7 +262,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipyButtonEntity.REVIVE: 'revive'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_revive',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_revive',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
@ -308,7 +308,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipyButtonEntity.BRIGHTNESS: 'brightness'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_brightness',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_brightness',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
@ -355,7 +355,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipyButtonEntity.RUN_CRON: 'run_cron'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_run_cron',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_run_cron',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
@ -401,7 +401,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipyButtonEntity.ALLOCATE_ALL_STAT_POINTS: 'allocate_all_stat_points'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_allocate_all_stat_points',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_allocate_all_stat_points',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
@ -447,7 +447,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipyButtonEntity.BUY_HEALTH_POTION: 'buy_health_potion'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_buy_health_potion',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_buy_health_potion',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
@ -494,7 +494,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipyButtonEntity.REVIVE: 'revive'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_revive',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_revive',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
@ -540,7 +540,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipyButtonEntity.RUN_CRON: 'run_cron'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_run_cron',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_run_cron',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
@ -586,7 +586,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipyButtonEntity.STEALTH: 'stealth'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_stealth',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_stealth',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
@ -633,7 +633,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipyButtonEntity.TOOLS_OF_TRADE: 'tools_of_trade'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_tools_of_trade',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_tools_of_trade',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
@ -680,7 +680,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipyButtonEntity.ALLOCATE_ALL_STAT_POINTS: 'allocate_all_stat_points'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_allocate_all_stat_points',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_allocate_all_stat_points',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
@ -726,7 +726,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipyButtonEntity.BUY_HEALTH_POTION: 'buy_health_potion'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_buy_health_potion',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_buy_health_potion',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
@ -773,7 +773,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipyButtonEntity.DEFENSIVE_STANCE: 'defensive_stance'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_defensive_stance',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_defensive_stance',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
@ -820,7 +820,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipyButtonEntity.INTIMIDATE: 'intimidate'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_intimidate',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_intimidate',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
@ -867,7 +867,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipyButtonEntity.REVIVE: 'revive'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_revive',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_revive',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
@ -913,7 +913,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipyButtonEntity.RUN_CRON: 'run_cron'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_run_cron',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_run_cron',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
@ -959,7 +959,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipyButtonEntity.VALOROUS_PRESENCE: 'valorous_presence'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_valorous_presence',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_valorous_presence',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
@ -1006,7 +1006,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipyButtonEntity.ALLOCATE_ALL_STAT_POINTS: 'allocate_all_stat_points'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_allocate_all_stat_points',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_allocate_all_stat_points',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
@ -1052,7 +1052,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipyButtonEntity.BUY_HEALTH_POTION: 'buy_health_potion'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_buy_health_potion',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_buy_health_potion',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
@ -1099,7 +1099,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipyButtonEntity.FROST: 'frost'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_frost',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_frost',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
@ -1146,7 +1146,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipyButtonEntity.EARTH: 'earth'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_earth',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_earth',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
@ -1193,7 +1193,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipyButtonEntity.MPHEAL: 'mpheal'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_mpheal',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_mpheal',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
@ -1240,7 +1240,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipyButtonEntity.REVIVE: 'revive'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_revive',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_revive',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
@ -1286,7 +1286,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipyButtonEntity.RUN_CRON: 'run_cron'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_run_cron',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_run_cron',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
|
@ -928,7 +928,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabiticaCalendar.DAILIES: 'dailys'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_dailys',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_dailys',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
@ -981,7 +981,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabiticaCalendar.DAILY_REMINDERS: 'daily_reminders'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_daily_reminders',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_daily_reminders',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
@ -1033,7 +1033,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabiticaCalendar.TODO_REMINDERS: 'todo_reminders'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_todo_reminders',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_todo_reminders',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
@ -1085,7 +1085,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabiticaCalendar.TODOS: 'todos'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_todos',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_todos',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -7,9 +7,9 @@
|
||||
'capabilities': dict({
|
||||
'options': list([
|
||||
'warrior',
|
||||
'healer',
|
||||
'wizard',
|
||||
'rogue',
|
||||
'wizard',
|
||||
'healer',
|
||||
]),
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
@ -35,7 +35,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipySensorEntity.CLASS: 'class'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_class',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_class',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
@ -46,9 +46,9 @@
|
||||
'friendly_name': 'test-user Class',
|
||||
'options': list([
|
||||
'warrior',
|
||||
'healer',
|
||||
'wizard',
|
||||
'rogue',
|
||||
'wizard',
|
||||
'healer',
|
||||
]),
|
||||
}),
|
||||
'context': <ANY>,
|
||||
@ -91,7 +91,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipySensorEntity.CONSTITUTION: 'constitution'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_constitution',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_constitution',
|
||||
'unit_of_measurement': 'CON',
|
||||
})
|
||||
# ---
|
||||
@ -143,7 +143,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipySensorEntity.DAILIES: 'dailys'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_dailys',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_dailys',
|
||||
'unit_of_measurement': 'tasks',
|
||||
})
|
||||
# ---
|
||||
@ -151,23 +151,39 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'2c6d136c-a1c3-4bef-b7c4-fa980784b1e1': dict({
|
||||
'created_at': '2024-09-22T11:44:43.774Z',
|
||||
'challenge': dict({
|
||||
'broken': None,
|
||||
'id': None,
|
||||
'shortName': None,
|
||||
'taskId': None,
|
||||
'winner': None,
|
||||
}),
|
||||
'created_at': '2024-09-22T11:44:43.774000+00:00',
|
||||
'every_x': 1,
|
||||
'frequency': 'weekly',
|
||||
'group': dict({
|
||||
'assignedDate': None,
|
||||
'assignedUsers': list([
|
||||
]),
|
||||
'completedBy': dict({
|
||||
'assignedUsersDetail': dict({
|
||||
}),
|
||||
'assigningUsername': None,
|
||||
'completedBy': dict({
|
||||
'date': None,
|
||||
'userId': None,
|
||||
}),
|
||||
'id': None,
|
||||
'managerNotes': None,
|
||||
'taskId': None,
|
||||
}),
|
||||
'is_due': True,
|
||||
'next_due': list([
|
||||
'2024-09-24T22:00:00.000Z',
|
||||
'2024-09-27T22:00:00.000Z',
|
||||
'2024-09-28T22:00:00.000Z',
|
||||
'2024-10-01T22:00:00.000Z',
|
||||
'2024-10-04T22:00:00.000Z',
|
||||
'2024-10-08T22:00:00.000Z',
|
||||
'2024-09-24T22:00:00+00:00',
|
||||
'2024-09-27T22:00:00+00:00',
|
||||
'2024-09-28T22:00:00+00:00',
|
||||
'2024-10-01T22:00:00+00:00',
|
||||
'2024-10-04T22:00:00+00:00',
|
||||
'2024-10-08T22:00:00+00:00',
|
||||
]),
|
||||
'notes': 'Ein einstündiges Workout im Fitnessstudio absolvieren.',
|
||||
'priority': 2,
|
||||
@ -180,7 +196,7 @@
|
||||
'th': False,
|
||||
'w': True,
|
||||
}),
|
||||
'start_date': '2024-09-21T22:00:00.000Z',
|
||||
'start_date': '2024-09-21T22:00:00+00:00',
|
||||
'tags': list([
|
||||
'51076966-2970-4b40-b6ba-d58c6a756dd7',
|
||||
]),
|
||||
@ -189,24 +205,40 @@
|
||||
'yester_daily': True,
|
||||
}),
|
||||
'564b9ac9-c53d-4638-9e7f-1cd96fe19baa': dict({
|
||||
'challenge': dict({
|
||||
'broken': None,
|
||||
'id': None,
|
||||
'shortName': None,
|
||||
'taskId': None,
|
||||
'winner': None,
|
||||
}),
|
||||
'completed': True,
|
||||
'created_at': '2024-07-07T17:51:53.268Z',
|
||||
'created_at': '2024-07-07T17:51:53.268000+00:00',
|
||||
'every_x': 1,
|
||||
'frequency': 'weekly',
|
||||
'group': dict({
|
||||
'assignedDate': None,
|
||||
'assignedUsers': list([
|
||||
]),
|
||||
'completedBy': dict({
|
||||
'assignedUsersDetail': dict({
|
||||
}),
|
||||
'assigningUsername': None,
|
||||
'completedBy': dict({
|
||||
'date': None,
|
||||
'userId': None,
|
||||
}),
|
||||
'id': None,
|
||||
'managerNotes': None,
|
||||
'taskId': None,
|
||||
}),
|
||||
'is_due': True,
|
||||
'next_due': list([
|
||||
'Mon Sep 23 2024 00:00:00 GMT+0200',
|
||||
'Tue Sep 24 2024 00:00:00 GMT+0200',
|
||||
'Wed Sep 25 2024 00:00:00 GMT+0200',
|
||||
'Thu Sep 26 2024 00:00:00 GMT+0200',
|
||||
'Fri Sep 27 2024 00:00:00 GMT+0200',
|
||||
'Sat Sep 28 2024 00:00:00 GMT+0200',
|
||||
'2024-09-23T00:00:00+02:00',
|
||||
'2024-09-24T00:00:00+02:00',
|
||||
'2024-09-25T00:00:00+02:00',
|
||||
'2024-09-26T00:00:00+02:00',
|
||||
'2024-09-27T00:00:00+02:00',
|
||||
'2024-09-28T00:00:00+02:00',
|
||||
]),
|
||||
'notes': 'Klicke um Änderungen zu machen!',
|
||||
'priority': 1,
|
||||
@ -219,7 +251,7 @@
|
||||
'th': True,
|
||||
'w': True,
|
||||
}),
|
||||
'start_date': '2024-07-06T22:00:00.000Z',
|
||||
'start_date': '2024-07-06T22:00:00+00:00',
|
||||
'streak': 1,
|
||||
'text': 'Zahnseide benutzen',
|
||||
'type': 'daily',
|
||||
@ -227,22 +259,38 @@
|
||||
'yester_daily': True,
|
||||
}),
|
||||
'6e53f1f5-a315-4edd-984d-8d762e4a08ef': dict({
|
||||
'created_at': '2024-10-10T15:57:14.304Z',
|
||||
'challenge': dict({
|
||||
'broken': None,
|
||||
'id': None,
|
||||
'shortName': None,
|
||||
'taskId': None,
|
||||
'winner': None,
|
||||
}),
|
||||
'created_at': '2024-10-10T15:57:14.304000+00:00',
|
||||
'every_x': 1,
|
||||
'frequency': 'monthly',
|
||||
'group': dict({
|
||||
'assignedDate': None,
|
||||
'assignedUsers': list([
|
||||
]),
|
||||
'completedBy': dict({
|
||||
'assignedUsersDetail': dict({
|
||||
}),
|
||||
'assigningUsername': None,
|
||||
'completedBy': dict({
|
||||
'date': None,
|
||||
'userId': None,
|
||||
}),
|
||||
'id': None,
|
||||
'managerNotes': None,
|
||||
'taskId': None,
|
||||
}),
|
||||
'next_due': list([
|
||||
'2024-12-14T23:00:00.000Z',
|
||||
'2025-01-18T23:00:00.000Z',
|
||||
'2025-02-15T23:00:00.000Z',
|
||||
'2025-03-15T23:00:00.000Z',
|
||||
'2025-04-19T23:00:00.000Z',
|
||||
'2025-05-17T23:00:00.000Z',
|
||||
'2024-12-14T23:00:00+00:00',
|
||||
'2025-01-18T23:00:00+00:00',
|
||||
'2025-02-15T23:00:00+00:00',
|
||||
'2025-03-15T23:00:00+00:00',
|
||||
'2025-04-19T23:00:00+00:00',
|
||||
'2025-05-17T23:00:00+00:00',
|
||||
]),
|
||||
'notes': 'Klicke um den Namen Deines aktuellen Projekts anzugeben & setze einen Terminplan!',
|
||||
'priority': 1,
|
||||
@ -255,7 +303,7 @@
|
||||
'th': False,
|
||||
'w': False,
|
||||
}),
|
||||
'start_date': '2024-09-20T23:00:00.000Z',
|
||||
'start_date': '2024-09-20T23:00:00+00:00',
|
||||
'streak': 1,
|
||||
'text': 'Arbeite an einem kreativen Projekt',
|
||||
'type': 'daily',
|
||||
@ -266,23 +314,39 @@
|
||||
'yester_daily': True,
|
||||
}),
|
||||
'f2c85972-1a19-4426-bc6d-ce3337b9d99f': dict({
|
||||
'created_at': '2024-07-07T17:51:53.266Z',
|
||||
'challenge': dict({
|
||||
'broken': None,
|
||||
'id': None,
|
||||
'shortName': None,
|
||||
'taskId': None,
|
||||
'winner': None,
|
||||
}),
|
||||
'created_at': '2024-07-07T17:51:53.266000+00:00',
|
||||
'every_x': 1,
|
||||
'frequency': 'weekly',
|
||||
'group': dict({
|
||||
'assignedDate': None,
|
||||
'assignedUsers': list([
|
||||
]),
|
||||
'completedBy': dict({
|
||||
'assignedUsersDetail': dict({
|
||||
}),
|
||||
'assigningUsername': None,
|
||||
'completedBy': dict({
|
||||
'date': None,
|
||||
'userId': None,
|
||||
}),
|
||||
'id': None,
|
||||
'managerNotes': None,
|
||||
'taskId': None,
|
||||
}),
|
||||
'is_due': True,
|
||||
'next_due': list([
|
||||
'2024-09-22T22:00:00.000Z',
|
||||
'2024-09-23T22:00:00.000Z',
|
||||
'2024-09-24T22:00:00.000Z',
|
||||
'2024-09-25T22:00:00.000Z',
|
||||
'2024-09-26T22:00:00.000Z',
|
||||
'2024-09-27T22:00:00.000Z',
|
||||
'2024-09-22T22:00:00+00:00',
|
||||
'2024-09-23T22:00:00+00:00',
|
||||
'2024-09-24T22:00:00+00:00',
|
||||
'2024-09-25T22:00:00+00:00',
|
||||
'2024-09-26T22:00:00+00:00',
|
||||
'2024-09-27T22:00:00+00:00',
|
||||
]),
|
||||
'notes': 'Klicke um Deinen Terminplan festzulegen!',
|
||||
'priority': 1,
|
||||
@ -295,7 +359,7 @@
|
||||
'th': True,
|
||||
'w': True,
|
||||
}),
|
||||
'start_date': '2024-07-06T22:00:00.000Z',
|
||||
'start_date': '2024-07-06T22:00:00+00:00',
|
||||
'text': '5 Minuten ruhig durchatmen',
|
||||
'type': 'daily',
|
||||
'value': -1.919611992979862,
|
||||
@ -341,7 +405,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipySensorEntity.DISPLAY_NAME: 'display_name'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_display_name',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_display_name',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
@ -387,7 +451,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipySensorEntity.EXPERIENCE: 'experience'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_experience',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_experience',
|
||||
'unit_of_measurement': 'XP',
|
||||
})
|
||||
# ---
|
||||
@ -437,7 +501,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipySensorEntity.GEMS: 'gems'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_gems',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_gems',
|
||||
'unit_of_measurement': 'gems',
|
||||
})
|
||||
# ---
|
||||
@ -453,7 +517,7 @@
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0',
|
||||
'state': '40',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.test_user_gold-entry]
|
||||
@ -488,7 +552,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipySensorEntity.GOLD: 'gold'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_gold',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_gold',
|
||||
'unit_of_measurement': 'GP',
|
||||
})
|
||||
# ---
|
||||
@ -535,7 +599,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipySensorEntity.HABITS: 'habits'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_habits',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_habits',
|
||||
'unit_of_measurement': 'tasks',
|
||||
})
|
||||
# ---
|
||||
@ -543,60 +607,160 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'1d147de6-5c02-4740-8e2f-71d3015a37f4': dict({
|
||||
'created_at': '2024-07-07T17:51:53.266Z',
|
||||
'challenge': dict({
|
||||
'broken': None,
|
||||
'id': None,
|
||||
'shortName': None,
|
||||
'taskId': None,
|
||||
'winner': None,
|
||||
}),
|
||||
'created_at': '2024-07-07T17:51:53.266000+00:00',
|
||||
'frequency': 'daily',
|
||||
'group': dict({
|
||||
'assignedDate': None,
|
||||
'assignedUsers': list([
|
||||
]),
|
||||
'completedBy': dict({
|
||||
'assignedUsersDetail': dict({
|
||||
}),
|
||||
'assigningUsername': None,
|
||||
'completedBy': dict({
|
||||
'date': None,
|
||||
'userId': None,
|
||||
}),
|
||||
'id': None,
|
||||
'managerNotes': None,
|
||||
'taskId': None,
|
||||
}),
|
||||
'priority': 1,
|
||||
'repeat': dict({
|
||||
'f': False,
|
||||
'm': True,
|
||||
's': False,
|
||||
'su': False,
|
||||
't': True,
|
||||
'th': False,
|
||||
'w': True,
|
||||
}),
|
||||
'text': 'Eine kurze Pause machen',
|
||||
'type': 'habit',
|
||||
'up': True,
|
||||
}),
|
||||
'bc1d1855-b2b8-4663-98ff-62e7b763dfc4': dict({
|
||||
'created_at': '2024-07-07T17:51:53.265Z',
|
||||
'challenge': dict({
|
||||
'broken': None,
|
||||
'id': None,
|
||||
'shortName': None,
|
||||
'taskId': None,
|
||||
'winner': None,
|
||||
}),
|
||||
'created_at': '2024-07-07T17:51:53.265000+00:00',
|
||||
'down': True,
|
||||
'frequency': 'daily',
|
||||
'group': dict({
|
||||
'assignedDate': None,
|
||||
'assignedUsers': list([
|
||||
]),
|
||||
'completedBy': dict({
|
||||
'assignedUsersDetail': dict({
|
||||
}),
|
||||
'assigningUsername': None,
|
||||
'completedBy': dict({
|
||||
'date': None,
|
||||
'userId': None,
|
||||
}),
|
||||
'id': None,
|
||||
'managerNotes': None,
|
||||
'taskId': None,
|
||||
}),
|
||||
'notes': 'Oder lösche es über die Bearbeitungs-Ansicht',
|
||||
'priority': 1,
|
||||
'repeat': dict({
|
||||
'f': False,
|
||||
'm': True,
|
||||
's': False,
|
||||
'su': False,
|
||||
't': True,
|
||||
'th': False,
|
||||
'w': True,
|
||||
}),
|
||||
'text': 'Klicke hier um dies als schlechte Gewohnheit zu markieren, die Du gerne loswerden möchtest',
|
||||
'type': 'habit',
|
||||
}),
|
||||
'e97659e0-2c42-4599-a7bb-00282adc410d': dict({
|
||||
'created_at': '2024-07-07T17:51:53.264Z',
|
||||
'challenge': dict({
|
||||
'broken': None,
|
||||
'id': None,
|
||||
'shortName': None,
|
||||
'taskId': None,
|
||||
'winner': None,
|
||||
}),
|
||||
'created_at': '2024-07-07T17:51:53.264000+00:00',
|
||||
'frequency': 'daily',
|
||||
'group': dict({
|
||||
'assignedDate': None,
|
||||
'assignedUsers': list([
|
||||
]),
|
||||
'completedBy': dict({
|
||||
'assignedUsersDetail': dict({
|
||||
}),
|
||||
'assigningUsername': None,
|
||||
'completedBy': dict({
|
||||
'date': None,
|
||||
'userId': None,
|
||||
}),
|
||||
'id': None,
|
||||
'managerNotes': None,
|
||||
'taskId': None,
|
||||
}),
|
||||
'notes': 'Eine Gewohnheit, eine Tagesaufgabe oder ein To-Do',
|
||||
'priority': 1,
|
||||
'repeat': dict({
|
||||
'f': False,
|
||||
'm': True,
|
||||
's': False,
|
||||
'su': False,
|
||||
't': True,
|
||||
'th': False,
|
||||
'w': True,
|
||||
}),
|
||||
'text': 'Füge eine Aufgabe zu Habitica hinzu',
|
||||
'type': 'habit',
|
||||
'up': True,
|
||||
}),
|
||||
'f21fa608-cfc6-4413-9fc7-0eb1b48ca43a': dict({
|
||||
'created_at': '2024-07-07T17:51:53.268Z',
|
||||
'challenge': dict({
|
||||
'broken': None,
|
||||
'id': None,
|
||||
'shortName': None,
|
||||
'taskId': None,
|
||||
'winner': None,
|
||||
}),
|
||||
'created_at': '2024-07-07T17:51:53.268000+00:00',
|
||||
'down': True,
|
||||
'frequency': 'daily',
|
||||
'group': dict({
|
||||
'assignedDate': None,
|
||||
'assignedUsers': list([
|
||||
]),
|
||||
'completedBy': dict({
|
||||
'assignedUsersDetail': dict({
|
||||
}),
|
||||
'assigningUsername': None,
|
||||
'completedBy': dict({
|
||||
'date': None,
|
||||
'userId': None,
|
||||
}),
|
||||
'id': None,
|
||||
'managerNotes': None,
|
||||
'taskId': None,
|
||||
}),
|
||||
'priority': 1,
|
||||
'repeat': dict({
|
||||
'f': False,
|
||||
'm': True,
|
||||
's': False,
|
||||
'su': False,
|
||||
't': True,
|
||||
'th': False,
|
||||
'w': True,
|
||||
}),
|
||||
'text': 'Gesundes Essen/Junkfood',
|
||||
'type': 'habit',
|
||||
'up': True,
|
||||
@ -644,7 +808,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipySensorEntity.HEALTH: 'health'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_health',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_health',
|
||||
'unit_of_measurement': 'HP',
|
||||
})
|
||||
# ---
|
||||
@ -659,7 +823,7 @@
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '0',
|
||||
'state': '0.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_sensors[sensor.test_user_intelligence-entry]
|
||||
@ -694,7 +858,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipySensorEntity.INTELLIGENCE: 'intelligence'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_intelligence',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_intelligence',
|
||||
'unit_of_measurement': 'INT',
|
||||
})
|
||||
# ---
|
||||
@ -746,7 +910,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipySensorEntity.LEVEL: 'level'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_level',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_level',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
@ -795,7 +959,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipySensorEntity.MANA: 'mana'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_mana',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_mana',
|
||||
'unit_of_measurement': 'MP',
|
||||
})
|
||||
# ---
|
||||
@ -842,7 +1006,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipySensorEntity.HEALTH_MAX: 'health_max'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_health_max',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_health_max',
|
||||
'unit_of_measurement': 'HP',
|
||||
})
|
||||
# ---
|
||||
@ -889,7 +1053,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipySensorEntity.MANA_MAX: 'mana_max'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_mana_max',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_mana_max',
|
||||
'unit_of_measurement': 'MP',
|
||||
})
|
||||
# ---
|
||||
@ -939,7 +1103,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipySensorEntity.TRINKETS: 'trinkets'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_trinkets',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_trinkets',
|
||||
'unit_of_measurement': '⧖',
|
||||
})
|
||||
# ---
|
||||
@ -987,7 +1151,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipySensorEntity.EXPERIENCE_MAX: 'experience_max'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_experience_max',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_experience_max',
|
||||
'unit_of_measurement': 'XP',
|
||||
})
|
||||
# ---
|
||||
@ -1037,7 +1201,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipySensorEntity.PERCEPTION: 'perception'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_perception',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_perception',
|
||||
'unit_of_measurement': 'PER',
|
||||
})
|
||||
# ---
|
||||
@ -1089,7 +1253,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipySensorEntity.REWARDS: 'rewards'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_rewards',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_rewards',
|
||||
'unit_of_measurement': 'tasks',
|
||||
})
|
||||
# ---
|
||||
@ -1097,18 +1261,43 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'5e2ea1df-f6e6-4ba3-bccb-97c5ec63e99b': dict({
|
||||
'created_at': '2024-07-07T17:51:53.266Z',
|
||||
'challenge': dict({
|
||||
'broken': None,
|
||||
'id': None,
|
||||
'shortName': None,
|
||||
'taskId': None,
|
||||
'winner': None,
|
||||
}),
|
||||
'created_at': '2024-07-07T17:51:53.266000+00:00',
|
||||
'group': dict({
|
||||
'assignedDate': None,
|
||||
'assignedUsers': list([
|
||||
]),
|
||||
'completedBy': dict({
|
||||
'assignedUsersDetail': dict({
|
||||
}),
|
||||
'assigningUsername': None,
|
||||
'completedBy': dict({
|
||||
'date': None,
|
||||
'userId': None,
|
||||
}),
|
||||
'id': None,
|
||||
'managerNotes': None,
|
||||
'taskId': None,
|
||||
}),
|
||||
'notes': 'Schaue fern, spiele ein Spiel, gönne Dir einen Leckerbissen, es liegt ganz bei Dir!',
|
||||
'priority': 1,
|
||||
'repeat': dict({
|
||||
'f': False,
|
||||
'm': True,
|
||||
's': False,
|
||||
'su': False,
|
||||
't': True,
|
||||
'th': False,
|
||||
'w': True,
|
||||
}),
|
||||
'text': 'Belohne Dich selbst',
|
||||
'type': 'reward',
|
||||
'value': 10,
|
||||
'value': 10.0,
|
||||
}),
|
||||
'friendly_name': 'test-user Rewards',
|
||||
'unit_of_measurement': 'tasks',
|
||||
@ -1153,7 +1342,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipySensorEntity.STRENGTH: 'strength'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_strength',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_strength',
|
||||
'unit_of_measurement': 'STR',
|
||||
})
|
||||
# ---
|
||||
@ -1205,7 +1394,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabitipySensorEntity.TODOS: 'todos'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_todos',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_todos',
|
||||
'unit_of_measurement': 'tasks',
|
||||
})
|
||||
# ---
|
||||
@ -1213,41 +1402,116 @@
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'1aa3137e-ef72-4d1f-91ee-41933602f438': dict({
|
||||
'created_at': '2024-09-21T22:16:38.153Z',
|
||||
'challenge': dict({
|
||||
'broken': None,
|
||||
'id': None,
|
||||
'shortName': None,
|
||||
'taskId': None,
|
||||
'winner': None,
|
||||
}),
|
||||
'created_at': '2024-09-21T22:16:38.153000+00:00',
|
||||
'group': dict({
|
||||
'assignedDate': None,
|
||||
'assignedUsers': list([
|
||||
]),
|
||||
'completedBy': dict({
|
||||
'assignedUsersDetail': dict({
|
||||
}),
|
||||
'assigningUsername': None,
|
||||
'completedBy': dict({
|
||||
'date': None,
|
||||
'userId': None,
|
||||
}),
|
||||
'id': None,
|
||||
'managerNotes': None,
|
||||
'taskId': None,
|
||||
}),
|
||||
'notes': 'Rasen mähen und die Pflanzen gießen.',
|
||||
'priority': 1,
|
||||
'repeat': dict({
|
||||
'f': False,
|
||||
'm': True,
|
||||
's': False,
|
||||
'su': False,
|
||||
't': True,
|
||||
'th': False,
|
||||
'w': True,
|
||||
}),
|
||||
'text': 'Garten pflegen',
|
||||
'type': 'todo',
|
||||
}),
|
||||
'2f6fcabc-f670-4ec3-ba65-817e8deea490': dict({
|
||||
'created_at': '2024-09-21T22:17:19.513Z',
|
||||
'challenge': dict({
|
||||
'broken': None,
|
||||
'id': None,
|
||||
'shortName': None,
|
||||
'taskId': None,
|
||||
'winner': None,
|
||||
}),
|
||||
'created_at': '2024-09-21T22:17:19.513000+00:00',
|
||||
'group': dict({
|
||||
'assignedDate': None,
|
||||
'assignedUsers': list([
|
||||
]),
|
||||
'completedBy': dict({
|
||||
'assignedUsersDetail': dict({
|
||||
}),
|
||||
'assigningUsername': None,
|
||||
'completedBy': dict({
|
||||
'date': None,
|
||||
'userId': None,
|
||||
}),
|
||||
'id': None,
|
||||
'managerNotes': None,
|
||||
'taskId': None,
|
||||
}),
|
||||
'notes': 'Strom- und Internetrechnungen rechtzeitig überweisen.',
|
||||
'priority': 1,
|
||||
'repeat': dict({
|
||||
'f': False,
|
||||
'm': True,
|
||||
's': False,
|
||||
'su': False,
|
||||
't': True,
|
||||
'th': False,
|
||||
'w': True,
|
||||
}),
|
||||
'text': 'Rechnungen bezahlen',
|
||||
'type': 'todo',
|
||||
}),
|
||||
'86ea2475-d1b5-4020-bdcc-c188c7996afa': dict({
|
||||
'created_at': '2024-09-21T22:16:16.756Z',
|
||||
'challenge': dict({
|
||||
'broken': None,
|
||||
'id': None,
|
||||
'shortName': None,
|
||||
'taskId': None,
|
||||
'winner': None,
|
||||
}),
|
||||
'created_at': '2024-09-21T22:16:16.756000+00:00',
|
||||
'group': dict({
|
||||
'assignedDate': None,
|
||||
'assignedUsers': list([
|
||||
]),
|
||||
'completedBy': dict({
|
||||
'assignedUsersDetail': dict({
|
||||
}),
|
||||
'assigningUsername': None,
|
||||
'completedBy': dict({
|
||||
'date': None,
|
||||
'userId': None,
|
||||
}),
|
||||
'id': None,
|
||||
'managerNotes': None,
|
||||
'taskId': None,
|
||||
}),
|
||||
'notes': 'Den Ausflug für das kommende Wochenende organisieren.',
|
||||
'priority': 1,
|
||||
'repeat': dict({
|
||||
'f': False,
|
||||
'm': True,
|
||||
's': False,
|
||||
'su': False,
|
||||
't': True,
|
||||
'th': False,
|
||||
'w': True,
|
||||
}),
|
||||
'tags': list([
|
||||
'51076966-2970-4b40-b6ba-d58c6a756dd7',
|
||||
]),
|
||||
@ -1255,15 +1519,40 @@
|
||||
'type': 'todo',
|
||||
}),
|
||||
'88de7cd9-af2b-49ce-9afd-bf941d87336b': dict({
|
||||
'created_at': '2024-09-21T22:17:57.816Z',
|
||||
'challenge': dict({
|
||||
'broken': None,
|
||||
'id': None,
|
||||
'shortName': None,
|
||||
'taskId': None,
|
||||
'winner': None,
|
||||
}),
|
||||
'created_at': '2024-09-21T22:17:57.816000+00:00',
|
||||
'group': dict({
|
||||
'assignedDate': None,
|
||||
'assignedUsers': list([
|
||||
]),
|
||||
'completedBy': dict({
|
||||
'assignedUsersDetail': dict({
|
||||
}),
|
||||
'assigningUsername': None,
|
||||
'completedBy': dict({
|
||||
'date': None,
|
||||
'userId': None,
|
||||
}),
|
||||
'id': None,
|
||||
'managerNotes': None,
|
||||
'taskId': None,
|
||||
}),
|
||||
'notes': 'Das Buch, das du angefangen hast, bis zum Wochenende fertig lesen.',
|
||||
'priority': 1,
|
||||
'repeat': dict({
|
||||
'f': False,
|
||||
'm': True,
|
||||
's': False,
|
||||
'su': False,
|
||||
't': True,
|
||||
'th': False,
|
||||
'w': True,
|
||||
}),
|
||||
'text': 'Buch zu Ende lesen',
|
||||
'type': 'todo',
|
||||
}),
|
||||
|
@ -28,7 +28,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': <HabiticaSwitchEntity.SLEEP: 'sleep'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_sleep',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_sleep',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
|
@ -129,7 +129,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': <TodoListEntityFeature: 92>,
|
||||
'translation_key': <HabiticaTodoList.DAILIES: 'dailys'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_dailys',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_dailys',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
@ -176,7 +176,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': <TodoListEntityFeature: 95>,
|
||||
'translation_key': <HabiticaTodoList.TODOS: 'todos'>,
|
||||
'unique_id': '00000000-0000-0000-0000-000000000000_todos',
|
||||
'unique_id': 'a380546a-94be-4b8e-8a0b-23e0d5c03303_todos',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
|
@ -1,19 +1,19 @@
|
||||
"""Tests for the Habitica binary sensor platform."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from habiticalib import HabiticaUserResponse
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.habitica.const import ASSETS_URL, DEFAULT_URL, DOMAIN
|
||||
from homeassistant.components.habitica.const import ASSETS_URL, DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import STATE_OFF, STATE_ON, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from tests.common import MockConfigEntry, load_json_object_fixture, snapshot_platform
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
from tests.common import MockConfigEntry, load_fixture, snapshot_platform
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@ -26,7 +26,7 @@ def binary_sensor_only() -> Generator[None]:
|
||||
yield
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_habitica")
|
||||
@pytest.mark.usefixtures("habitica")
|
||||
async def test_binary_sensors(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
@ -54,23 +54,17 @@ async def test_binary_sensors(
|
||||
async def test_pending_quest_states(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
habitica: AsyncMock,
|
||||
fixture: str,
|
||||
entity_state: str,
|
||||
entity_picture: str | None,
|
||||
) -> None:
|
||||
"""Test states of pending quest sensor."""
|
||||
|
||||
aioclient_mock.get(
|
||||
f"{DEFAULT_URL}/api/v3/user",
|
||||
json=load_json_object_fixture(f"{fixture}.json", DOMAIN),
|
||||
)
|
||||
aioclient_mock.get(f"{DEFAULT_URL}/api/v3/tasks/user", json={"data": []})
|
||||
aioclient_mock.get(
|
||||
f"{DEFAULT_URL}/api/v3/content",
|
||||
params={"language": "en"},
|
||||
json=load_json_object_fixture("content.json", DOMAIN),
|
||||
habitica.get_user.return_value = HabiticaUserResponse.from_json(
|
||||
load_fixture(f"{fixture}.json", DOMAIN)
|
||||
)
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
@ -2,31 +2,29 @@
|
||||
|
||||
from collections.abc import Generator
|
||||
from datetime import timedelta
|
||||
from http import HTTPStatus
|
||||
import re
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from habiticalib import HabiticaUserResponse, Skill
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
|
||||
from homeassistant.components.habitica.const import DEFAULT_URL, DOMAIN
|
||||
from homeassistant.components.habitica.const import DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .conftest import mock_called_with
|
||||
from .conftest import ERROR_BAD_REQUEST, ERROR_NOT_AUTHORIZED, ERROR_TOO_MANY_REQUESTS
|
||||
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
async_fire_time_changed,
|
||||
load_json_object_fixture,
|
||||
load_fixture,
|
||||
snapshot_platform,
|
||||
)
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@ -51,29 +49,15 @@ def button_only() -> Generator[None]:
|
||||
async def test_buttons(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
habitica: AsyncMock,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
fixture: str,
|
||||
) -> None:
|
||||
"""Test button entities."""
|
||||
aioclient_mock.get(
|
||||
f"{DEFAULT_URL}/api/v3/user",
|
||||
json=load_json_object_fixture(f"{fixture}.json", DOMAIN),
|
||||
)
|
||||
aioclient_mock.get(
|
||||
f"{DEFAULT_URL}/api/v3/tasks/user",
|
||||
params={"type": "completedTodos"},
|
||||
json=load_json_object_fixture("completed_todos.json", DOMAIN),
|
||||
)
|
||||
aioclient_mock.get(
|
||||
f"{DEFAULT_URL}/api/v3/tasks/user",
|
||||
json=load_json_object_fixture("tasks.json", DOMAIN),
|
||||
)
|
||||
aioclient_mock.get(
|
||||
f"{DEFAULT_URL}/api/v3/content",
|
||||
params={"language": "en"},
|
||||
json=load_json_object_fixture("content.json", DOMAIN),
|
||||
|
||||
habitica.get_user.return_value = HabiticaUserResponse.from_json(
|
||||
load_fixture(f"{fixture}.json", DOMAIN)
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
@ -85,70 +69,87 @@ async def test_buttons(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("entity_id", "api_url", "fixture"),
|
||||
("entity_id", "call_func", "call_args", "fixture"),
|
||||
[
|
||||
("button.test_user_allocate_all_stat_points", "user/allocate-now", "user"),
|
||||
("button.test_user_buy_a_health_potion", "user/buy-health-potion", "user"),
|
||||
("button.test_user_revive_from_death", "user/revive", "user"),
|
||||
("button.test_user_start_my_day", "cron", "user"),
|
||||
(
|
||||
"button.test_user_allocate_all_stat_points",
|
||||
"allocate_stat_points",
|
||||
None,
|
||||
"user",
|
||||
),
|
||||
("button.test_user_buy_a_health_potion", "buy_health_potion", None, "user"),
|
||||
("button.test_user_revive_from_death", "revive", None, "user"),
|
||||
("button.test_user_start_my_day", "run_cron", None, "user"),
|
||||
(
|
||||
"button.test_user_chilling_frost",
|
||||
"user/class/cast/frost",
|
||||
"cast_skill",
|
||||
Skill.CHILLING_FROST,
|
||||
"wizard_fixture",
|
||||
),
|
||||
(
|
||||
"button.test_user_earthquake",
|
||||
"user/class/cast/earth",
|
||||
"cast_skill",
|
||||
Skill.EARTHQUAKE,
|
||||
"wizard_fixture",
|
||||
),
|
||||
(
|
||||
"button.test_user_ethereal_surge",
|
||||
"user/class/cast/mpheal",
|
||||
"cast_skill",
|
||||
Skill.ETHEREAL_SURGE,
|
||||
"wizard_fixture",
|
||||
),
|
||||
(
|
||||
"button.test_user_stealth",
|
||||
"user/class/cast/stealth",
|
||||
"cast_skill",
|
||||
Skill.STEALTH,
|
||||
"rogue_fixture",
|
||||
),
|
||||
(
|
||||
"button.test_user_tools_of_the_trade",
|
||||
"user/class/cast/toolsOfTrade",
|
||||
"cast_skill",
|
||||
Skill.TOOLS_OF_THE_TRADE,
|
||||
"rogue_fixture",
|
||||
),
|
||||
(
|
||||
"button.test_user_defensive_stance",
|
||||
"user/class/cast/defensiveStance",
|
||||
"cast_skill",
|
||||
Skill.DEFENSIVE_STANCE,
|
||||
"warrior_fixture",
|
||||
),
|
||||
(
|
||||
"button.test_user_intimidating_gaze",
|
||||
"user/class/cast/intimidate",
|
||||
"cast_skill",
|
||||
Skill.INTIMIDATING_GAZE,
|
||||
"warrior_fixture",
|
||||
),
|
||||
(
|
||||
"button.test_user_valorous_presence",
|
||||
"user/class/cast/valorousPresence",
|
||||
"cast_skill",
|
||||
Skill.VALOROUS_PRESENCE,
|
||||
"warrior_fixture",
|
||||
),
|
||||
(
|
||||
"button.test_user_healing_light",
|
||||
"user/class/cast/heal",
|
||||
"cast_skill",
|
||||
Skill.HEALING_LIGHT,
|
||||
"healer_fixture",
|
||||
),
|
||||
(
|
||||
"button.test_user_protective_aura",
|
||||
"user/class/cast/protectAura",
|
||||
"cast_skill",
|
||||
Skill.PROTECTIVE_AURA,
|
||||
"healer_fixture",
|
||||
),
|
||||
(
|
||||
"button.test_user_searing_brightness",
|
||||
"user/class/cast/brightness",
|
||||
"cast_skill",
|
||||
Skill.SEARING_BRIGHTNESS,
|
||||
"healer_fixture",
|
||||
),
|
||||
(
|
||||
"button.test_user_blessing",
|
||||
"user/class/cast/healAll",
|
||||
"cast_skill",
|
||||
Skill.BLESSING,
|
||||
"healer_fixture",
|
||||
),
|
||||
],
|
||||
@ -156,58 +157,48 @@ async def test_buttons(
|
||||
async def test_button_press(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
habitica: AsyncMock,
|
||||
entity_id: str,
|
||||
api_url: str,
|
||||
call_func: str,
|
||||
call_args: Skill | None,
|
||||
fixture: str,
|
||||
) -> None:
|
||||
"""Test button press method."""
|
||||
aioclient_mock.get(
|
||||
f"{DEFAULT_URL}/api/v3/user",
|
||||
json=load_json_object_fixture(f"{fixture}.json", DOMAIN),
|
||||
)
|
||||
aioclient_mock.get(
|
||||
f"{DEFAULT_URL}/api/v3/tasks/user",
|
||||
params={"type": "completedTodos"},
|
||||
json=load_json_object_fixture("completed_todos.json", DOMAIN),
|
||||
)
|
||||
aioclient_mock.get(
|
||||
f"{DEFAULT_URL}/api/v3/tasks/user",
|
||||
json=load_json_object_fixture("tasks.json", DOMAIN),
|
||||
)
|
||||
aioclient_mock.get(
|
||||
f"{DEFAULT_URL}/api/v3/content",
|
||||
params={"language": "en"},
|
||||
json=load_json_object_fixture("content.json", DOMAIN),
|
||||
|
||||
habitica.get_user.return_value = HabiticaUserResponse.from_json(
|
||||
load_fixture(f"{fixture}.json", DOMAIN)
|
||||
)
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
aioclient_mock.post(f"{DEFAULT_URL}/api/v3/{api_url}", json={"data": None})
|
||||
|
||||
mocked = getattr(habitica, call_func)
|
||||
mocked.reset_mock()
|
||||
await hass.services.async_call(
|
||||
BUTTON_DOMAIN,
|
||||
SERVICE_PRESS,
|
||||
{ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_called_with(aioclient_mock, "post", f"{DEFAULT_URL}/api/v3/{api_url}")
|
||||
if call_args:
|
||||
mocked.assert_awaited_once_with(call_args)
|
||||
else:
|
||||
mocked.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("entity_id", "api_url"),
|
||||
("entity_id", "call_func"),
|
||||
[
|
||||
("button.test_user_allocate_all_stat_points", "user/allocate-now"),
|
||||
("button.test_user_buy_a_health_potion", "user/buy-health-potion"),
|
||||
("button.test_user_revive_from_death", "user/revive"),
|
||||
("button.test_user_start_my_day", "cron"),
|
||||
("button.test_user_chilling_frost", "user/class/cast/frost"),
|
||||
("button.test_user_earthquake", "user/class/cast/earth"),
|
||||
("button.test_user_ethereal_surge", "user/class/cast/mpheal"),
|
||||
("button.test_user_allocate_all_stat_points", "allocate_stat_points"),
|
||||
("button.test_user_buy_a_health_potion", "buy_health_potion"),
|
||||
("button.test_user_revive_from_death", "revive"),
|
||||
("button.test_user_start_my_day", "run_cron"),
|
||||
("button.test_user_chilling_frost", "cast_skill"),
|
||||
("button.test_user_earthquake", "cast_skill"),
|
||||
("button.test_user_ethereal_surge", "cast_skill"),
|
||||
],
|
||||
ids=[
|
||||
"allocate-points",
|
||||
@ -220,20 +211,20 @@ async def test_button_press(
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("status_code", "msg", "exception"),
|
||||
("raise_exception", "msg", "expected_exception"),
|
||||
[
|
||||
(
|
||||
HTTPStatus.TOO_MANY_REQUESTS,
|
||||
ERROR_TOO_MANY_REQUESTS,
|
||||
"Rate limit exceeded, try again later",
|
||||
ServiceValidationError,
|
||||
HomeAssistantError,
|
||||
),
|
||||
(
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
ERROR_BAD_REQUEST,
|
||||
"Unable to connect to Habitica, try again later",
|
||||
HomeAssistantError,
|
||||
),
|
||||
(
|
||||
HTTPStatus.UNAUTHORIZED,
|
||||
ERROR_NOT_AUTHORIZED,
|
||||
"Unable to complete action, the required conditions are not met",
|
||||
ServiceValidationError,
|
||||
),
|
||||
@ -242,12 +233,12 @@ async def test_button_press(
|
||||
async def test_button_press_exceptions(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_habitica: AiohttpClientMocker,
|
||||
habitica: AsyncMock,
|
||||
entity_id: str,
|
||||
api_url: str,
|
||||
status_code: HTTPStatus,
|
||||
call_func: str,
|
||||
raise_exception: Exception,
|
||||
msg: str,
|
||||
exception: Exception,
|
||||
expected_exception: Exception,
|
||||
) -> None:
|
||||
"""Test button press exceptions."""
|
||||
|
||||
@ -257,13 +248,10 @@ async def test_button_press_exceptions(
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
mock_habitica.post(
|
||||
f"{DEFAULT_URL}/api/v3/{api_url}",
|
||||
status=status_code,
|
||||
json={"data": None},
|
||||
)
|
||||
func = getattr(habitica, call_func)
|
||||
func.side_effect = raise_exception
|
||||
|
||||
with pytest.raises(exception, match=msg):
|
||||
with pytest.raises(expected_exception, match=msg):
|
||||
await hass.services.async_call(
|
||||
BUTTON_DOMAIN,
|
||||
SERVICE_PRESS,
|
||||
@ -271,8 +259,6 @@ async def test_button_press_exceptions(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_called_with(mock_habitica, "post", f"{DEFAULT_URL}/api/v3/{api_url}")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("fixture", "entity_ids"),
|
||||
@ -322,21 +308,15 @@ async def test_button_press_exceptions(
|
||||
async def test_button_unavailable(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
habitica: AsyncMock,
|
||||
fixture: str,
|
||||
entity_ids: list[str],
|
||||
) -> None:
|
||||
"""Test buttons are unavailable if conditions are not met."""
|
||||
|
||||
aioclient_mock.get(
|
||||
f"{DEFAULT_URL}/api/v3/user",
|
||||
json=load_json_object_fixture(f"{fixture}.json", DOMAIN),
|
||||
habitica.get_user.return_value = HabiticaUserResponse.from_json(
|
||||
load_fixture(f"{fixture}.json", DOMAIN)
|
||||
)
|
||||
aioclient_mock.get(
|
||||
f"{DEFAULT_URL}/api/v3/tasks/user",
|
||||
json=load_json_object_fixture("tasks.json", DOMAIN),
|
||||
)
|
||||
aioclient_mock.get(re.compile(r".*"), json={"data": []})
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
@ -352,9 +332,8 @@ async def test_button_unavailable(
|
||||
async def test_class_change(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
habitica: AsyncMock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test removing and adding skills after class change."""
|
||||
mage_skills = [
|
||||
@ -368,23 +347,9 @@ async def test_class_change(
|
||||
"button.test_user_searing_brightness",
|
||||
"button.test_user_blessing",
|
||||
]
|
||||
aioclient_mock.get(
|
||||
f"{DEFAULT_URL}/api/v3/user",
|
||||
json=load_json_object_fixture("wizard_fixture.json", DOMAIN),
|
||||
)
|
||||
aioclient_mock.get(
|
||||
f"{DEFAULT_URL}/api/v3/tasks/user",
|
||||
params={"type": "completedTodos"},
|
||||
json=load_json_object_fixture("completed_todos.json", DOMAIN),
|
||||
)
|
||||
aioclient_mock.get(
|
||||
f"{DEFAULT_URL}/api/v3/tasks/user",
|
||||
json=load_json_object_fixture("tasks.json", DOMAIN),
|
||||
)
|
||||
aioclient_mock.get(
|
||||
f"{DEFAULT_URL}/api/v3/content",
|
||||
params={"language": "en"},
|
||||
json=load_json_object_fixture("content.json", DOMAIN),
|
||||
|
||||
habitica.get_user.return_value = HabiticaUserResponse.from_json(
|
||||
load_fixture("wizard_fixture.json", DOMAIN)
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
@ -395,13 +360,11 @@ async def test_class_change(
|
||||
for skill in mage_skills:
|
||||
assert hass.states.get(skill)
|
||||
|
||||
aioclient_mock._mocks.pop(0)
|
||||
aioclient_mock.get(
|
||||
f"{DEFAULT_URL}/api/v3/user",
|
||||
json=load_json_object_fixture("healer_fixture.json", DOMAIN),
|
||||
habitica.get_user.return_value = HabiticaUserResponse.from_json(
|
||||
load_fixture("healer_fixture.json", DOMAIN)
|
||||
)
|
||||
|
||||
async_fire_time_changed(hass, dt_util.now() + timedelta(seconds=60))
|
||||
freezer.tick(timedelta(seconds=60))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
for skill in mage_skills:
|
||||
|
@ -3,6 +3,7 @@
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import patch
|
||||
|
||||
from freezegun.api import freeze_time
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
@ -31,8 +32,8 @@ async def set_tz(hass: HomeAssistant) -> None:
|
||||
await hass.config.async_set_time_zone("Europe/Berlin")
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_habitica")
|
||||
@pytest.mark.freeze_time("2024-09-20T22:00:00.000Z")
|
||||
@pytest.mark.usefixtures("habitica")
|
||||
@freeze_time("2024-09-20T22:00:00.000Z")
|
||||
async def test_calendar_platform(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
@ -70,8 +71,7 @@ async def test_calendar_platform(
|
||||
"date range in the past",
|
||||
],
|
||||
)
|
||||
@pytest.mark.freeze_time("2024-09-20T22:00:00.000Z")
|
||||
@pytest.mark.usefixtures("mock_habitica")
|
||||
@pytest.mark.usefixtures("habitica")
|
||||
async def test_api_events(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
|
@ -1,14 +1,14 @@
|
||||
"""Test the habitica config flow."""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.habitica.const import CONF_API_USER, DEFAULT_URL, DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_NAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_URL,
|
||||
CONF_USERNAME,
|
||||
@ -17,23 +17,32 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from .conftest import ERROR_BAD_REQUEST, ERROR_NOT_AUTHORIZED
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
TEST_API_USER = "a380546a-94be-4b8e-8a0b-23e0d5c03303"
|
||||
TEST_API_KEY = "cd0e5985-17de-4b4f-849e-5d506c5e4382"
|
||||
|
||||
|
||||
MOCK_DATA_LOGIN_STEP = {
|
||||
CONF_USERNAME: "test-email@example.com",
|
||||
CONF_PASSWORD: "test-password",
|
||||
}
|
||||
MOCK_DATA_ADVANCED_STEP = {
|
||||
CONF_API_USER: "test-api-user",
|
||||
CONF_API_KEY: "test-api-key",
|
||||
CONF_API_USER: TEST_API_USER,
|
||||
CONF_API_KEY: TEST_API_KEY,
|
||||
CONF_URL: DEFAULT_URL,
|
||||
CONF_VERIFY_SSL: True,
|
||||
}
|
||||
|
||||
|
||||
async def test_form_login(hass: HomeAssistant) -> None:
|
||||
@pytest.mark.usefixtures("habitica")
|
||||
async def test_form_login(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None:
|
||||
"""Test we get the login form."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
@ -47,55 +56,41 @@ async def test_form_login(hass: HomeAssistant) -> None:
|
||||
assert result["errors"] == {}
|
||||
assert result["step_id"] == "login"
|
||||
|
||||
mock_obj = MagicMock()
|
||||
mock_obj.user.auth.local.login.post = AsyncMock()
|
||||
mock_obj.user.auth.local.login.post.return_value = {
|
||||
"id": "test-api-user",
|
||||
"apiToken": "test-api-key",
|
||||
"username": "test-username",
|
||||
}
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.habitica.config_flow.HabitipyAsync",
|
||||
return_value=mock_obj,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.habitica.async_setup", return_value=True
|
||||
) as mock_setup,
|
||||
patch(
|
||||
"homeassistant.components.habitica.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=MOCK_DATA_LOGIN_STEP,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=MOCK_DATA_LOGIN_STEP,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "test-username"
|
||||
assert result["title"] == "test-user"
|
||||
assert result["data"] == {
|
||||
**MOCK_DATA_ADVANCED_STEP,
|
||||
CONF_USERNAME: "test-username",
|
||||
CONF_API_USER: TEST_API_USER,
|
||||
CONF_API_KEY: TEST_API_KEY,
|
||||
CONF_URL: DEFAULT_URL,
|
||||
CONF_NAME: "test-user",
|
||||
CONF_VERIFY_SSL: True,
|
||||
}
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert result["result"].unique_id == TEST_API_USER
|
||||
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("raise_error", "text_error"),
|
||||
[
|
||||
(ClientResponseError(MagicMock(), (), status=400), "cannot_connect"),
|
||||
(ClientResponseError(MagicMock(), (), status=401), "invalid_auth"),
|
||||
(ERROR_BAD_REQUEST, "cannot_connect"),
|
||||
(ERROR_NOT_AUTHORIZED, "invalid_auth"),
|
||||
(IndexError(), "unknown"),
|
||||
],
|
||||
)
|
||||
async def test_form_login_errors(hass: HomeAssistant, raise_error, text_error) -> None:
|
||||
async def test_form_login_errors(
|
||||
hass: HomeAssistant, habitica: AsyncMock, raise_error, text_error
|
||||
) -> None:
|
||||
"""Test we handle invalid credentials error."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
@ -105,26 +100,40 @@ async def test_form_login_errors(hass: HomeAssistant, raise_error, text_error) -
|
||||
DOMAIN, context={"source": "login"}
|
||||
)
|
||||
|
||||
mock_obj = MagicMock()
|
||||
mock_obj.user.auth.local.login.post = AsyncMock(side_effect=raise_error)
|
||||
with patch(
|
||||
"homeassistant.components.habitica.config_flow.HabitipyAsync",
|
||||
return_value=mock_obj,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=MOCK_DATA_LOGIN_STEP,
|
||||
)
|
||||
habitica.login.side_effect = raise_error
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=MOCK_DATA_LOGIN_STEP,
|
||||
)
|
||||
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": text_error}
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": text_error}
|
||||
|
||||
# recover from errors
|
||||
habitica.login.side_effect = None
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=MOCK_DATA_LOGIN_STEP,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "test-user"
|
||||
assert result["data"] == {
|
||||
CONF_API_USER: TEST_API_USER,
|
||||
CONF_API_KEY: TEST_API_KEY,
|
||||
CONF_URL: DEFAULT_URL,
|
||||
CONF_NAME: "test-user",
|
||||
CONF_VERIFY_SSL: True,
|
||||
}
|
||||
assert result["result"].unique_id == TEST_API_USER
|
||||
|
||||
|
||||
async def test_form_advanced(hass: HomeAssistant) -> None:
|
||||
@pytest.mark.usefixtures("habitica")
|
||||
async def test_form_advanced(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None:
|
||||
"""Test we get the form."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
@ -144,54 +153,41 @@ async def test_form_advanced(hass: HomeAssistant) -> None:
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
mock_obj = MagicMock()
|
||||
mock_obj.user.get = AsyncMock()
|
||||
mock_obj.user.get.return_value = {"auth": {"local": {"username": "test-username"}}}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=MOCK_DATA_ADVANCED_STEP,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.habitica.config_flow.HabitipyAsync",
|
||||
return_value=mock_obj,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.habitica.async_setup", return_value=True
|
||||
) as mock_setup,
|
||||
patch(
|
||||
"homeassistant.components.habitica.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=MOCK_DATA_ADVANCED_STEP,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result2["title"] == "test-username"
|
||||
assert result2["data"] == {
|
||||
**MOCK_DATA_ADVANCED_STEP,
|
||||
CONF_USERNAME: "test-username",
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "test-user"
|
||||
assert result["data"] == {
|
||||
CONF_API_USER: TEST_API_USER,
|
||||
CONF_API_KEY: TEST_API_KEY,
|
||||
CONF_URL: DEFAULT_URL,
|
||||
CONF_NAME: "test-user",
|
||||
CONF_VERIFY_SSL: True,
|
||||
}
|
||||
assert len(mock_setup.mock_calls) == 1
|
||||
assert result["result"].unique_id == TEST_API_USER
|
||||
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("raise_error", "text_error"),
|
||||
[
|
||||
(ClientResponseError(MagicMock(), (), status=400), "cannot_connect"),
|
||||
(ClientResponseError(MagicMock(), (), status=401), "invalid_auth"),
|
||||
(ERROR_BAD_REQUEST, "cannot_connect"),
|
||||
(ERROR_NOT_AUTHORIZED, "invalid_auth"),
|
||||
(IndexError(), "unknown"),
|
||||
],
|
||||
)
|
||||
async def test_form_advanced_errors(
|
||||
hass: HomeAssistant, raise_error, text_error
|
||||
hass: HomeAssistant, habitica: AsyncMock, raise_error, text_error
|
||||
) -> None:
|
||||
"""Test we handle invalid credentials error."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
@ -201,17 +197,59 @@ async def test_form_advanced_errors(
|
||||
DOMAIN, context={"source": "advanced"}
|
||||
)
|
||||
|
||||
mock_obj = MagicMock()
|
||||
mock_obj.user.get = AsyncMock(side_effect=raise_error)
|
||||
habitica.get_user.side_effect = raise_error
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.habitica.config_flow.HabitipyAsync",
|
||||
return_value=mock_obj,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=MOCK_DATA_ADVANCED_STEP,
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=MOCK_DATA_ADVANCED_STEP,
|
||||
)
|
||||
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": text_error}
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": text_error}
|
||||
|
||||
# recover from errors
|
||||
habitica.get_user.side_effect = None
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=MOCK_DATA_ADVANCED_STEP,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "test-user"
|
||||
assert result["data"] == {
|
||||
CONF_API_USER: TEST_API_USER,
|
||||
CONF_API_KEY: TEST_API_KEY,
|
||||
CONF_URL: DEFAULT_URL,
|
||||
CONF_NAME: "test-user",
|
||||
CONF_VERIFY_SSL: True,
|
||||
}
|
||||
assert result["result"].unique_id == TEST_API_USER
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("habitica")
|
||||
async def test_form_advanced_already_configured(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test we abort user data set when entry is already configured."""
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.MENU
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": "advanced"}
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=MOCK_DATA_ADVANCED_STEP,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
@ -10,7 +10,7 @@ from tests.components.diagnostics import get_diagnostics_for_config_entry
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_habitica")
|
||||
@pytest.mark.usefixtures("habitica")
|
||||
async def test_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
|
@ -1,8 +1,8 @@
|
||||
"""Test the habitica module."""
|
||||
|
||||
import datetime
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
@ -11,7 +11,6 @@ from homeassistant.components.habitica.const import (
|
||||
ATTR_ARGS,
|
||||
ATTR_DATA,
|
||||
ATTR_PATH,
|
||||
DEFAULT_URL,
|
||||
DOMAIN,
|
||||
EVENT_API_CALL_SUCCESS,
|
||||
SERVICE_API_CALL,
|
||||
@ -20,16 +19,14 @@ from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import ATTR_NAME
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
async_capture_events,
|
||||
async_fire_time_changed,
|
||||
load_json_object_fixture,
|
||||
from .conftest import (
|
||||
ERROR_BAD_REQUEST,
|
||||
ERROR_NOT_AUTHORIZED,
|
||||
ERROR_NOT_FOUND,
|
||||
ERROR_TOO_MANY_REQUESTS,
|
||||
)
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
TEST_API_CALL_ARGS = {"text": "Use API from Home Assistant", "type": "todo"}
|
||||
TEST_USER_NAME = "test_user"
|
||||
from tests.common import MockConfigEntry, async_capture_events, async_fire_time_changed
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -38,7 +35,7 @@ def capture_api_call_success(hass: HomeAssistant) -> list[Event]:
|
||||
return async_capture_events(hass, EVENT_API_CALL_SUCCESS)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_habitica")
|
||||
@pytest.mark.usefixtures("habitica")
|
||||
async def test_entry_setup_unload(
|
||||
hass: HomeAssistant, config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
@ -55,12 +52,11 @@ async def test_entry_setup_unload(
|
||||
assert config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_habitica")
|
||||
@pytest.mark.usefixtures("habitica")
|
||||
async def test_service_call(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
capture_api_call_success: list[Event],
|
||||
mock_habitica: AiohttpClientMocker,
|
||||
) -> None:
|
||||
"""Test integration setup, service call and unload."""
|
||||
config_entry.add_to_hass(hass)
|
||||
@ -71,16 +67,10 @@ async def test_service_call(
|
||||
|
||||
assert len(capture_api_call_success) == 0
|
||||
|
||||
mock_habitica.post(
|
||||
"https://habitica.com/api/v3/tasks/user",
|
||||
status=HTTPStatus.CREATED,
|
||||
json={"data": TEST_API_CALL_ARGS},
|
||||
)
|
||||
|
||||
TEST_SERVICE_DATA = {
|
||||
ATTR_NAME: "test-user",
|
||||
ATTR_PATH: ["tasks", "user", "post"],
|
||||
ATTR_ARGS: TEST_API_CALL_ARGS,
|
||||
ATTR_ARGS: {"text": "Use API from Home Assistant", "type": "todo"},
|
||||
}
|
||||
await hass.services.async_call(
|
||||
DOMAIN, SERVICE_API_CALL, TEST_SERVICE_DATA, blocking=True
|
||||
@ -94,21 +84,23 @@ async def test_service_call(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("status"), [HTTPStatus.NOT_FOUND, HTTPStatus.TOO_MANY_REQUESTS]
|
||||
("exception"),
|
||||
[
|
||||
ERROR_BAD_REQUEST,
|
||||
ERROR_TOO_MANY_REQUESTS,
|
||||
ERROR_NOT_AUTHORIZED,
|
||||
],
|
||||
ids=["BadRequestError", "TooManyRequestsError", "NotAuthorizedError"],
|
||||
)
|
||||
async def test_config_entry_not_ready(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
status: HTTPStatus,
|
||||
habitica: AsyncMock,
|
||||
exception: Exception,
|
||||
) -> None:
|
||||
"""Test config entry not ready."""
|
||||
|
||||
aioclient_mock.get(
|
||||
f"{DEFAULT_URL}/api/v3/user",
|
||||
status=status,
|
||||
)
|
||||
|
||||
habitica.get_user.side_effect = exception
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
@ -119,19 +111,11 @@ async def test_config_entry_not_ready(
|
||||
async def test_coordinator_update_failed(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
habitica: AsyncMock,
|
||||
) -> None:
|
||||
"""Test coordinator update failed."""
|
||||
|
||||
aioclient_mock.get(
|
||||
f"{DEFAULT_URL}/api/v3/user",
|
||||
json=load_json_object_fixture("user.json", DOMAIN),
|
||||
)
|
||||
aioclient_mock.get(
|
||||
f"{DEFAULT_URL}/api/v3/tasks/user",
|
||||
status=HTTPStatus.NOT_FOUND,
|
||||
)
|
||||
|
||||
habitica.get_tasks.side_effect = ERROR_NOT_FOUND
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
@ -142,7 +126,7 @@ async def test_coordinator_update_failed(
|
||||
async def test_coordinator_rate_limited(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_habitica: AiohttpClientMocker,
|
||||
habitica: AsyncMock,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
@ -154,11 +138,7 @@ async def test_coordinator_rate_limited(
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
mock_habitica.clear_requests()
|
||||
mock_habitica.get(
|
||||
f"{DEFAULT_URL}/api/v3/user",
|
||||
status=HTTPStatus.TOO_MANY_REQUESTS,
|
||||
)
|
||||
habitica.get_user.side_effect = ERROR_TOO_MANY_REQUESTS
|
||||
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
freezer.tick(datetime.timedelta(seconds=60))
|
||||
|
@ -26,7 +26,7 @@ def sensor_only() -> Generator[None]:
|
||||
yield
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_habitica", "entity_registry_enabled_by_default")
|
||||
@pytest.mark.usefixtures("habitica", "entity_registry_enabled_by_default")
|
||||
async def test_sensors(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
@ -44,7 +44,7 @@ async def test_sensors(
|
||||
await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_habitica", "entity_registry_enabled_by_default")
|
||||
@pytest.mark.usefixtures("habitica", "entity_registry_enabled_by_default")
|
||||
async def test_sensor_deprecation_issue(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
|
@ -1,10 +1,11 @@
|
||||
"""Test Habitica actions."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from http import HTTPStatus
|
||||
from typing import Any
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from uuid import UUID
|
||||
|
||||
from habiticalib import Direction, Skill
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.habitica.const import (
|
||||
@ -14,7 +15,6 @@ from homeassistant.components.habitica.const import (
|
||||
ATTR_SKILL,
|
||||
ATTR_TARGET,
|
||||
ATTR_TASK,
|
||||
DEFAULT_URL,
|
||||
DOMAIN,
|
||||
SERVICE_ABORT_QUEST,
|
||||
SERVICE_ACCEPT_QUEST,
|
||||
@ -31,10 +31,14 @@ from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
|
||||
from .conftest import load_json_object_fixture, mock_called_with
|
||||
from .conftest import (
|
||||
ERROR_BAD_REQUEST,
|
||||
ERROR_NOT_AUTHORIZED,
|
||||
ERROR_NOT_FOUND,
|
||||
ERROR_TOO_MANY_REQUESTS,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
REQUEST_EXCEPTION_MSG = "Unable to connect to Habitica, try again later"
|
||||
RATE_LIMIT_EXCEPTION_MSG = "Rate limit exceeded, try again later"
|
||||
@ -54,7 +58,7 @@ def services_only() -> Generator[None]:
|
||||
async def load_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_habitica: AiohttpClientMocker,
|
||||
habitica: AsyncMock,
|
||||
services_only: Generator,
|
||||
) -> None:
|
||||
"""Load config entry."""
|
||||
@ -75,55 +79,70 @@ def uuid_mock() -> Generator[None]:
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service_data", "item", "target_id"),
|
||||
(
|
||||
"service_data",
|
||||
"call_args",
|
||||
),
|
||||
[
|
||||
(
|
||||
{
|
||||
ATTR_TASK: "2f6fcabc-f670-4ec3-ba65-817e8deea490",
|
||||
ATTR_SKILL: "pickpocket",
|
||||
},
|
||||
"pickPocket",
|
||||
"2f6fcabc-f670-4ec3-ba65-817e8deea490",
|
||||
{
|
||||
"skill": Skill.PICKPOCKET,
|
||||
"target_id": UUID("2f6fcabc-f670-4ec3-ba65-817e8deea490"),
|
||||
},
|
||||
),
|
||||
(
|
||||
{
|
||||
ATTR_TASK: "2f6fcabc-f670-4ec3-ba65-817e8deea490",
|
||||
ATTR_SKILL: "backstab",
|
||||
},
|
||||
"backStab",
|
||||
"2f6fcabc-f670-4ec3-ba65-817e8deea490",
|
||||
{
|
||||
"skill": Skill.BACKSTAB,
|
||||
"target_id": UUID("2f6fcabc-f670-4ec3-ba65-817e8deea490"),
|
||||
},
|
||||
),
|
||||
(
|
||||
{
|
||||
ATTR_TASK: "2f6fcabc-f670-4ec3-ba65-817e8deea490",
|
||||
ATTR_SKILL: "fireball",
|
||||
},
|
||||
"fireball",
|
||||
"2f6fcabc-f670-4ec3-ba65-817e8deea490",
|
||||
{
|
||||
"skill": Skill.BURST_OF_FLAMES,
|
||||
"target_id": UUID("2f6fcabc-f670-4ec3-ba65-817e8deea490"),
|
||||
},
|
||||
),
|
||||
(
|
||||
{
|
||||
ATTR_TASK: "2f6fcabc-f670-4ec3-ba65-817e8deea490",
|
||||
ATTR_SKILL: "smash",
|
||||
},
|
||||
"smash",
|
||||
"2f6fcabc-f670-4ec3-ba65-817e8deea490",
|
||||
{
|
||||
"skill": Skill.BRUTAL_SMASH,
|
||||
"target_id": UUID("2f6fcabc-f670-4ec3-ba65-817e8deea490"),
|
||||
},
|
||||
),
|
||||
(
|
||||
{
|
||||
ATTR_TASK: "Rechnungen bezahlen",
|
||||
ATTR_SKILL: "smash",
|
||||
},
|
||||
"smash",
|
||||
"2f6fcabc-f670-4ec3-ba65-817e8deea490",
|
||||
{
|
||||
"skill": Skill.BRUTAL_SMASH,
|
||||
"target_id": UUID("2f6fcabc-f670-4ec3-ba65-817e8deea490"),
|
||||
},
|
||||
),
|
||||
(
|
||||
{
|
||||
ATTR_TASK: "pay_bills",
|
||||
ATTR_SKILL: "smash",
|
||||
},
|
||||
"smash",
|
||||
"2f6fcabc-f670-4ec3-ba65-817e8deea490",
|
||||
{
|
||||
"skill": Skill.BRUTAL_SMASH,
|
||||
"target_id": UUID("2f6fcabc-f670-4ec3-ba65-817e8deea490"),
|
||||
},
|
||||
),
|
||||
],
|
||||
ids=[
|
||||
@ -138,18 +157,12 @@ def uuid_mock() -> Generator[None]:
|
||||
async def test_cast_skill(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_habitica: AiohttpClientMocker,
|
||||
habitica: AsyncMock,
|
||||
service_data: dict[str, Any],
|
||||
item: str,
|
||||
target_id: str,
|
||||
call_args: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test Habitica cast skill action."""
|
||||
|
||||
mock_habitica.post(
|
||||
f"{DEFAULT_URL}/api/v3/user/class/cast/{item}?targetId={target_id}",
|
||||
json={"success": True, "data": {}},
|
||||
)
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_CAST_SKILL,
|
||||
@ -160,18 +173,13 @@ async def test_cast_skill(
|
||||
return_response=True,
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_called_with(
|
||||
mock_habitica,
|
||||
"post",
|
||||
f"{DEFAULT_URL}/api/v3/user/class/cast/{item}?targetId={target_id}",
|
||||
)
|
||||
habitica.cast_skill.assert_awaited_once_with(**call_args)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"service_data",
|
||||
"http_status",
|
||||
"raise_exception",
|
||||
"expected_exception",
|
||||
"expected_exception_msg",
|
||||
),
|
||||
@ -181,7 +189,7 @@ async def test_cast_skill(
|
||||
ATTR_TASK: "task-not-found",
|
||||
ATTR_SKILL: "smash",
|
||||
},
|
||||
HTTPStatus.OK,
|
||||
None,
|
||||
ServiceValidationError,
|
||||
"Unable to complete action, could not find the task 'task-not-found'",
|
||||
),
|
||||
@ -190,8 +198,8 @@ async def test_cast_skill(
|
||||
ATTR_TASK: "Rechnungen bezahlen",
|
||||
ATTR_SKILL: "smash",
|
||||
},
|
||||
HTTPStatus.TOO_MANY_REQUESTS,
|
||||
ServiceValidationError,
|
||||
ERROR_TOO_MANY_REQUESTS,
|
||||
HomeAssistantError,
|
||||
RATE_LIMIT_EXCEPTION_MSG,
|
||||
),
|
||||
(
|
||||
@ -199,7 +207,7 @@ async def test_cast_skill(
|
||||
ATTR_TASK: "Rechnungen bezahlen",
|
||||
ATTR_SKILL: "smash",
|
||||
},
|
||||
HTTPStatus.NOT_FOUND,
|
||||
ERROR_NOT_FOUND,
|
||||
ServiceValidationError,
|
||||
"Unable to cast skill, your character does not have the skill or spell smash",
|
||||
),
|
||||
@ -208,7 +216,7 @@ async def test_cast_skill(
|
||||
ATTR_TASK: "Rechnungen bezahlen",
|
||||
ATTR_SKILL: "smash",
|
||||
},
|
||||
HTTPStatus.UNAUTHORIZED,
|
||||
ERROR_NOT_AUTHORIZED,
|
||||
ServiceValidationError,
|
||||
"Unable to cast skill, not enough mana. Your character has 50 MP, but the skill costs 10 MP",
|
||||
),
|
||||
@ -217,30 +225,24 @@ async def test_cast_skill(
|
||||
ATTR_TASK: "Rechnungen bezahlen",
|
||||
ATTR_SKILL: "smash",
|
||||
},
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
ERROR_BAD_REQUEST,
|
||||
HomeAssistantError,
|
||||
REQUEST_EXCEPTION_MSG,
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_habitica")
|
||||
async def test_cast_skill_exceptions(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_habitica: AiohttpClientMocker,
|
||||
habitica: AsyncMock,
|
||||
service_data: dict[str, Any],
|
||||
http_status: HTTPStatus,
|
||||
raise_exception: Exception,
|
||||
expected_exception: Exception,
|
||||
expected_exception_msg: str,
|
||||
) -> None:
|
||||
"""Test Habitica cast skill action exceptions."""
|
||||
|
||||
mock_habitica.post(
|
||||
f"{DEFAULT_URL}/api/v3/user/class/cast/smash?targetId=2f6fcabc-f670-4ec3-ba65-817e8deea490",
|
||||
json={"success": True, "data": {}},
|
||||
status=http_status,
|
||||
)
|
||||
|
||||
habitica.cast_skill.side_effect = raise_exception
|
||||
with pytest.raises(expected_exception, match=expected_exception_msg):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
@ -254,11 +256,9 @@ async def test_cast_skill_exceptions(
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_habitica")
|
||||
async def test_get_config_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_habitica: AiohttpClientMocker,
|
||||
) -> None:
|
||||
"""Test Habitica config entry exceptions."""
|
||||
|
||||
@ -298,31 +298,24 @@ async def test_get_config_entry(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service", "command"),
|
||||
"service",
|
||||
[
|
||||
(SERVICE_ABORT_QUEST, "abort"),
|
||||
(SERVICE_ACCEPT_QUEST, "accept"),
|
||||
(SERVICE_CANCEL_QUEST, "cancel"),
|
||||
(SERVICE_LEAVE_QUEST, "leave"),
|
||||
(SERVICE_REJECT_QUEST, "reject"),
|
||||
(SERVICE_START_QUEST, "force-start"),
|
||||
SERVICE_ABORT_QUEST,
|
||||
SERVICE_ACCEPT_QUEST,
|
||||
SERVICE_CANCEL_QUEST,
|
||||
SERVICE_LEAVE_QUEST,
|
||||
SERVICE_REJECT_QUEST,
|
||||
SERVICE_START_QUEST,
|
||||
],
|
||||
ids=[],
|
||||
)
|
||||
async def test_handle_quests(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_habitica: AiohttpClientMocker,
|
||||
habitica: AsyncMock,
|
||||
service: str,
|
||||
command: str,
|
||||
) -> None:
|
||||
"""Test Habitica actions for quest handling."""
|
||||
|
||||
mock_habitica.post(
|
||||
f"{DEFAULT_URL}/api/v3/groups/party/quests/{command}",
|
||||
json={"success": True, "data": {}},
|
||||
)
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
service,
|
||||
@ -331,63 +324,65 @@ async def test_handle_quests(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_called_with(
|
||||
mock_habitica,
|
||||
"post",
|
||||
f"{DEFAULT_URL}/api/v3/groups/party/quests/{command}",
|
||||
)
|
||||
getattr(habitica, service).assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"http_status",
|
||||
"raise_exception",
|
||||
"expected_exception",
|
||||
"expected_exception_msg",
|
||||
),
|
||||
[
|
||||
(
|
||||
HTTPStatus.TOO_MANY_REQUESTS,
|
||||
ServiceValidationError,
|
||||
ERROR_TOO_MANY_REQUESTS,
|
||||
HomeAssistantError,
|
||||
RATE_LIMIT_EXCEPTION_MSG,
|
||||
),
|
||||
(
|
||||
HTTPStatus.NOT_FOUND,
|
||||
ERROR_NOT_FOUND,
|
||||
ServiceValidationError,
|
||||
"Unable to complete action, quest or group not found",
|
||||
),
|
||||
(
|
||||
HTTPStatus.UNAUTHORIZED,
|
||||
ERROR_NOT_AUTHORIZED,
|
||||
ServiceValidationError,
|
||||
"Action not allowed, only quest leader or group leader can perform this action",
|
||||
),
|
||||
(
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
ERROR_BAD_REQUEST,
|
||||
HomeAssistantError,
|
||||
REQUEST_EXCEPTION_MSG,
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_habitica")
|
||||
@pytest.mark.parametrize(
|
||||
"service",
|
||||
[
|
||||
SERVICE_ACCEPT_QUEST,
|
||||
SERVICE_ABORT_QUEST,
|
||||
SERVICE_CANCEL_QUEST,
|
||||
SERVICE_LEAVE_QUEST,
|
||||
SERVICE_REJECT_QUEST,
|
||||
SERVICE_START_QUEST,
|
||||
],
|
||||
)
|
||||
async def test_handle_quests_exceptions(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_habitica: AiohttpClientMocker,
|
||||
http_status: HTTPStatus,
|
||||
habitica: AsyncMock,
|
||||
raise_exception: Exception,
|
||||
service: str,
|
||||
expected_exception: Exception,
|
||||
expected_exception_msg: str,
|
||||
) -> None:
|
||||
"""Test Habitica handle quests action exceptions."""
|
||||
|
||||
mock_habitica.post(
|
||||
f"{DEFAULT_URL}/api/v3/groups/party/quests/accept",
|
||||
json={"success": True, "data": {}},
|
||||
status=http_status,
|
||||
)
|
||||
|
||||
getattr(habitica, service).side_effect = raise_exception
|
||||
with pytest.raises(expected_exception, match=expected_exception_msg):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_ACCEPT_QUEST,
|
||||
service,
|
||||
service_data={ATTR_CONFIG_ENTRY: config_entry.entry_id},
|
||||
return_response=True,
|
||||
blocking=True,
|
||||
@ -395,7 +390,7 @@ async def test_handle_quests_exceptions(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service", "service_data", "task_id"),
|
||||
("service", "service_data", "call_args"),
|
||||
[
|
||||
(
|
||||
SERVICE_SCORE_HABIT,
|
||||
@ -403,7 +398,10 @@ async def test_handle_quests_exceptions(
|
||||
ATTR_TASK: "e97659e0-2c42-4599-a7bb-00282adc410d",
|
||||
ATTR_DIRECTION: "up",
|
||||
},
|
||||
"e97659e0-2c42-4599-a7bb-00282adc410d",
|
||||
{
|
||||
"task_id": UUID("e97659e0-2c42-4599-a7bb-00282adc410d"),
|
||||
"direction": Direction.UP,
|
||||
},
|
||||
),
|
||||
(
|
||||
SERVICE_SCORE_HABIT,
|
||||
@ -411,14 +409,20 @@ async def test_handle_quests_exceptions(
|
||||
ATTR_TASK: "e97659e0-2c42-4599-a7bb-00282adc410d",
|
||||
ATTR_DIRECTION: "down",
|
||||
},
|
||||
"e97659e0-2c42-4599-a7bb-00282adc410d",
|
||||
{
|
||||
"task_id": UUID("e97659e0-2c42-4599-a7bb-00282adc410d"),
|
||||
"direction": Direction.DOWN,
|
||||
},
|
||||
),
|
||||
(
|
||||
SERVICE_SCORE_REWARD,
|
||||
{
|
||||
ATTR_TASK: "5e2ea1df-f6e6-4ba3-bccb-97c5ec63e99b",
|
||||
},
|
||||
"5e2ea1df-f6e6-4ba3-bccb-97c5ec63e99b",
|
||||
{
|
||||
"task_id": UUID("5e2ea1df-f6e6-4ba3-bccb-97c5ec63e99b"),
|
||||
"direction": Direction.UP,
|
||||
},
|
||||
),
|
||||
(
|
||||
SERVICE_SCORE_HABIT,
|
||||
@ -426,7 +430,10 @@ async def test_handle_quests_exceptions(
|
||||
ATTR_TASK: "Füge eine Aufgabe zu Habitica hinzu",
|
||||
ATTR_DIRECTION: "up",
|
||||
},
|
||||
"e97659e0-2c42-4599-a7bb-00282adc410d",
|
||||
{
|
||||
"task_id": UUID("e97659e0-2c42-4599-a7bb-00282adc410d"),
|
||||
"direction": Direction.UP,
|
||||
},
|
||||
),
|
||||
(
|
||||
SERVICE_SCORE_HABIT,
|
||||
@ -434,7 +441,10 @@ async def test_handle_quests_exceptions(
|
||||
ATTR_TASK: "create_a_task",
|
||||
ATTR_DIRECTION: "up",
|
||||
},
|
||||
"e97659e0-2c42-4599-a7bb-00282adc410d",
|
||||
{
|
||||
"task_id": UUID("e97659e0-2c42-4599-a7bb-00282adc410d"),
|
||||
"direction": Direction.UP,
|
||||
},
|
||||
),
|
||||
],
|
||||
ids=[
|
||||
@ -448,18 +458,13 @@ async def test_handle_quests_exceptions(
|
||||
async def test_score_task(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_habitica: AiohttpClientMocker,
|
||||
habitica: AsyncMock,
|
||||
service: str,
|
||||
service_data: dict[str, Any],
|
||||
task_id: str,
|
||||
call_args: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test Habitica score task action."""
|
||||
|
||||
mock_habitica.post(
|
||||
f"{DEFAULT_URL}/api/v3/tasks/{task_id}/score/{service_data.get(ATTR_DIRECTION, "up")}",
|
||||
json={"success": True, "data": {}},
|
||||
)
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
service,
|
||||
@ -471,17 +476,13 @@ async def test_score_task(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_called_with(
|
||||
mock_habitica,
|
||||
"post",
|
||||
f"{DEFAULT_URL}/api/v3/tasks/{task_id}/score/{service_data.get(ATTR_DIRECTION, "up")}",
|
||||
)
|
||||
habitica.update_score.assert_awaited_once_with(**call_args)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"service_data",
|
||||
"http_status",
|
||||
"raise_exception",
|
||||
"expected_exception",
|
||||
"expected_exception_msg",
|
||||
),
|
||||
@ -491,7 +492,7 @@ async def test_score_task(
|
||||
ATTR_TASK: "task does not exist",
|
||||
ATTR_DIRECTION: "up",
|
||||
},
|
||||
HTTPStatus.OK,
|
||||
None,
|
||||
ServiceValidationError,
|
||||
"Unable to complete action, could not find the task 'task does not exist'",
|
||||
),
|
||||
@ -500,8 +501,8 @@ async def test_score_task(
|
||||
ATTR_TASK: "e97659e0-2c42-4599-a7bb-00282adc410d",
|
||||
ATTR_DIRECTION: "up",
|
||||
},
|
||||
HTTPStatus.TOO_MANY_REQUESTS,
|
||||
ServiceValidationError,
|
||||
ERROR_TOO_MANY_REQUESTS,
|
||||
HomeAssistantError,
|
||||
RATE_LIMIT_EXCEPTION_MSG,
|
||||
),
|
||||
(
|
||||
@ -509,7 +510,7 @@ async def test_score_task(
|
||||
ATTR_TASK: "e97659e0-2c42-4599-a7bb-00282adc410d",
|
||||
ATTR_DIRECTION: "up",
|
||||
},
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
ERROR_BAD_REQUEST,
|
||||
HomeAssistantError,
|
||||
REQUEST_EXCEPTION_MSG,
|
||||
),
|
||||
@ -518,35 +519,24 @@ async def test_score_task(
|
||||
ATTR_TASK: "5e2ea1df-f6e6-4ba3-bccb-97c5ec63e99b",
|
||||
ATTR_DIRECTION: "up",
|
||||
},
|
||||
HTTPStatus.UNAUTHORIZED,
|
||||
ERROR_NOT_AUTHORIZED,
|
||||
HomeAssistantError,
|
||||
"Unable to buy reward, not enough gold. Your character has 137.63 GP, but the reward costs 10 GP",
|
||||
"Unable to buy reward, not enough gold. Your character has 137.63 GP, but the reward costs 10.00 GP",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_habitica")
|
||||
async def test_score_task_exceptions(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_habitica: AiohttpClientMocker,
|
||||
habitica: AsyncMock,
|
||||
service_data: dict[str, Any],
|
||||
http_status: HTTPStatus,
|
||||
raise_exception: Exception,
|
||||
expected_exception: Exception,
|
||||
expected_exception_msg: str,
|
||||
) -> None:
|
||||
"""Test Habitica score task action exceptions."""
|
||||
|
||||
mock_habitica.post(
|
||||
f"{DEFAULT_URL}/api/v3/tasks/e97659e0-2c42-4599-a7bb-00282adc410d/score/up",
|
||||
json={"success": True, "data": {}},
|
||||
status=http_status,
|
||||
)
|
||||
mock_habitica.post(
|
||||
f"{DEFAULT_URL}/api/v3/tasks/5e2ea1df-f6e6-4ba3-bccb-97c5ec63e99b/score/up",
|
||||
json={"success": True, "data": {}},
|
||||
status=http_status,
|
||||
)
|
||||
|
||||
habitica.update_score.side_effect = raise_exception
|
||||
with pytest.raises(expected_exception, match=expected_exception_msg):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
@ -561,100 +551,119 @@ async def test_score_task_exceptions(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("service_data", "item", "target_id"),
|
||||
("service_data", "call_args"),
|
||||
[
|
||||
(
|
||||
{
|
||||
ATTR_TARGET: "a380546a-94be-4b8e-8a0b-23e0d5c03303",
|
||||
ATTR_ITEM: "spooky_sparkles",
|
||||
},
|
||||
"spookySparkles",
|
||||
"a380546a-94be-4b8e-8a0b-23e0d5c03303",
|
||||
{
|
||||
"skill": Skill.SPOOKY_SPARKLES,
|
||||
"target_id": UUID("a380546a-94be-4b8e-8a0b-23e0d5c03303"),
|
||||
},
|
||||
),
|
||||
(
|
||||
{
|
||||
ATTR_TARGET: "a380546a-94be-4b8e-8a0b-23e0d5c03303",
|
||||
ATTR_ITEM: "shiny_seed",
|
||||
},
|
||||
"shinySeed",
|
||||
"a380546a-94be-4b8e-8a0b-23e0d5c03303",
|
||||
{
|
||||
"skill": Skill.SHINY_SEED,
|
||||
"target_id": UUID("a380546a-94be-4b8e-8a0b-23e0d5c03303"),
|
||||
},
|
||||
),
|
||||
(
|
||||
{
|
||||
ATTR_TARGET: "a380546a-94be-4b8e-8a0b-23e0d5c03303",
|
||||
ATTR_ITEM: "seafoam",
|
||||
},
|
||||
"seafoam",
|
||||
"a380546a-94be-4b8e-8a0b-23e0d5c03303",
|
||||
{
|
||||
"skill": Skill.SEAFOAM,
|
||||
"target_id": UUID("a380546a-94be-4b8e-8a0b-23e0d5c03303"),
|
||||
},
|
||||
),
|
||||
(
|
||||
{
|
||||
ATTR_TARGET: "a380546a-94be-4b8e-8a0b-23e0d5c03303",
|
||||
ATTR_ITEM: "snowball",
|
||||
},
|
||||
"snowball",
|
||||
"a380546a-94be-4b8e-8a0b-23e0d5c03303",
|
||||
{
|
||||
"skill": Skill.SNOWBALL,
|
||||
"target_id": UUID("a380546a-94be-4b8e-8a0b-23e0d5c03303"),
|
||||
},
|
||||
),
|
||||
(
|
||||
{
|
||||
ATTR_TARGET: "test-user",
|
||||
ATTR_ITEM: "spooky_sparkles",
|
||||
},
|
||||
"spookySparkles",
|
||||
"a380546a-94be-4b8e-8a0b-23e0d5c03303",
|
||||
{
|
||||
"skill": Skill.SPOOKY_SPARKLES,
|
||||
"target_id": UUID("a380546a-94be-4b8e-8a0b-23e0d5c03303"),
|
||||
},
|
||||
),
|
||||
(
|
||||
{
|
||||
ATTR_TARGET: "test-username",
|
||||
ATTR_ITEM: "spooky_sparkles",
|
||||
},
|
||||
"spookySparkles",
|
||||
"a380546a-94be-4b8e-8a0b-23e0d5c03303",
|
||||
{
|
||||
"skill": Skill.SPOOKY_SPARKLES,
|
||||
"target_id": UUID("a380546a-94be-4b8e-8a0b-23e0d5c03303"),
|
||||
},
|
||||
),
|
||||
(
|
||||
{
|
||||
ATTR_TARGET: "ffce870c-3ff3-4fa4-bad1-87612e52b8e7",
|
||||
ATTR_ITEM: "spooky_sparkles",
|
||||
},
|
||||
"spookySparkles",
|
||||
"ffce870c-3ff3-4fa4-bad1-87612e52b8e7",
|
||||
{
|
||||
"skill": Skill.SPOOKY_SPARKLES,
|
||||
"target_id": UUID("ffce870c-3ff3-4fa4-bad1-87612e52b8e7"),
|
||||
},
|
||||
),
|
||||
(
|
||||
{
|
||||
ATTR_TARGET: "test-partymember-username",
|
||||
ATTR_ITEM: "spooky_sparkles",
|
||||
},
|
||||
"spookySparkles",
|
||||
"ffce870c-3ff3-4fa4-bad1-87612e52b8e7",
|
||||
{
|
||||
"skill": Skill.SPOOKY_SPARKLES,
|
||||
"target_id": UUID("ffce870c-3ff3-4fa4-bad1-87612e52b8e7"),
|
||||
},
|
||||
),
|
||||
(
|
||||
{
|
||||
ATTR_TARGET: "test-partymember-displayname",
|
||||
ATTR_ITEM: "spooky_sparkles",
|
||||
},
|
||||
"spookySparkles",
|
||||
"ffce870c-3ff3-4fa4-bad1-87612e52b8e7",
|
||||
{
|
||||
"skill": Skill.SPOOKY_SPARKLES,
|
||||
"target_id": UUID("ffce870c-3ff3-4fa4-bad1-87612e52b8e7"),
|
||||
},
|
||||
),
|
||||
],
|
||||
ids=[],
|
||||
ids=[
|
||||
"use spooky sparkles/select self by id",
|
||||
"use shiny seed",
|
||||
"use seafoam",
|
||||
"use snowball",
|
||||
"select self by displayname",
|
||||
"select self by username",
|
||||
"select partymember by id",
|
||||
"select partymember by username",
|
||||
"select partymember by displayname",
|
||||
],
|
||||
)
|
||||
async def test_transformation(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_habitica: AiohttpClientMocker,
|
||||
habitica: AsyncMock,
|
||||
service_data: dict[str, Any],
|
||||
item: str,
|
||||
target_id: str,
|
||||
call_args: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test Habitica user transformation item action."""
|
||||
mock_habitica.get(
|
||||
f"{DEFAULT_URL}/api/v3/groups/party/members",
|
||||
json=load_json_object_fixture("party_members.json", DOMAIN),
|
||||
)
|
||||
mock_habitica.post(
|
||||
f"{DEFAULT_URL}/api/v3/user/class/cast/{item}?targetId={target_id}",
|
||||
json={"success": True, "data": {}},
|
||||
)
|
||||
"""Test Habitica use transformation item action."""
|
||||
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
@ -667,18 +676,14 @@ async def test_transformation(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_called_with(
|
||||
mock_habitica,
|
||||
"post",
|
||||
f"{DEFAULT_URL}/api/v3/user/class/cast/{item}?targetId={target_id}",
|
||||
)
|
||||
habitica.cast_skill.assert_awaited_once_with(**call_args)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
(
|
||||
"service_data",
|
||||
"http_status_members",
|
||||
"http_status_cast",
|
||||
"raise_exception_members",
|
||||
"raise_exception_cast",
|
||||
"expected_exception",
|
||||
"expected_exception_msg",
|
||||
),
|
||||
@ -688,8 +693,8 @@ async def test_transformation(
|
||||
ATTR_TARGET: "user-not-found",
|
||||
ATTR_ITEM: "spooky_sparkles",
|
||||
},
|
||||
HTTPStatus.OK,
|
||||
HTTPStatus.OK,
|
||||
None,
|
||||
None,
|
||||
ServiceValidationError,
|
||||
"Unable to find target 'user-not-found' in your party",
|
||||
),
|
||||
@ -698,18 +703,8 @@ async def test_transformation(
|
||||
ATTR_TARGET: "test-partymember-username",
|
||||
ATTR_ITEM: "spooky_sparkles",
|
||||
},
|
||||
HTTPStatus.TOO_MANY_REQUESTS,
|
||||
HTTPStatus.OK,
|
||||
ServiceValidationError,
|
||||
RATE_LIMIT_EXCEPTION_MSG,
|
||||
),
|
||||
(
|
||||
{
|
||||
ATTR_TARGET: "test-partymember-username",
|
||||
ATTR_ITEM: "spooky_sparkles",
|
||||
},
|
||||
HTTPStatus.NOT_FOUND,
|
||||
HTTPStatus.OK,
|
||||
ERROR_NOT_FOUND,
|
||||
None,
|
||||
ServiceValidationError,
|
||||
"Unable to find target, you are currently not in a party. You can only target yourself",
|
||||
),
|
||||
@ -718,8 +713,8 @@ async def test_transformation(
|
||||
ATTR_TARGET: "test-partymember-username",
|
||||
ATTR_ITEM: "spooky_sparkles",
|
||||
},
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
HTTPStatus.OK,
|
||||
ERROR_BAD_REQUEST,
|
||||
None,
|
||||
HomeAssistantError,
|
||||
"Unable to connect to Habitica, try again later",
|
||||
),
|
||||
@ -728,9 +723,9 @@ async def test_transformation(
|
||||
ATTR_TARGET: "test-partymember-username",
|
||||
ATTR_ITEM: "spooky_sparkles",
|
||||
},
|
||||
HTTPStatus.OK,
|
||||
HTTPStatus.TOO_MANY_REQUESTS,
|
||||
ServiceValidationError,
|
||||
None,
|
||||
ERROR_TOO_MANY_REQUESTS,
|
||||
HomeAssistantError,
|
||||
RATE_LIMIT_EXCEPTION_MSG,
|
||||
),
|
||||
(
|
||||
@ -738,8 +733,8 @@ async def test_transformation(
|
||||
ATTR_TARGET: "test-partymember-username",
|
||||
ATTR_ITEM: "spooky_sparkles",
|
||||
},
|
||||
HTTPStatus.OK,
|
||||
HTTPStatus.UNAUTHORIZED,
|
||||
None,
|
||||
ERROR_NOT_AUTHORIZED,
|
||||
ServiceValidationError,
|
||||
"Unable to use spooky_sparkles, you don't own this item",
|
||||
),
|
||||
@ -748,36 +743,27 @@ async def test_transformation(
|
||||
ATTR_TARGET: "test-partymember-username",
|
||||
ATTR_ITEM: "spooky_sparkles",
|
||||
},
|
||||
HTTPStatus.OK,
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
None,
|
||||
ERROR_BAD_REQUEST,
|
||||
HomeAssistantError,
|
||||
"Unable to connect to Habitica, try again later",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_habitica")
|
||||
async def test_transformation_exceptions(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_habitica: AiohttpClientMocker,
|
||||
habitica: AsyncMock,
|
||||
service_data: dict[str, Any],
|
||||
http_status_members: HTTPStatus,
|
||||
http_status_cast: HTTPStatus,
|
||||
raise_exception_members: Exception,
|
||||
raise_exception_cast: Exception,
|
||||
expected_exception: Exception,
|
||||
expected_exception_msg: str,
|
||||
) -> None:
|
||||
"""Test Habitica transformation action exceptions."""
|
||||
mock_habitica.get(
|
||||
f"{DEFAULT_URL}/api/v3/groups/party/members",
|
||||
json=load_json_object_fixture("party_members.json", DOMAIN),
|
||||
status=http_status_members,
|
||||
)
|
||||
mock_habitica.post(
|
||||
f"{DEFAULT_URL}/api/v3/user/class/cast/spookySparkles?targetId=ffce870c-3ff3-4fa4-bad1-87612e52b8e7",
|
||||
json={"success": True, "data": {}},
|
||||
status=http_status_cast,
|
||||
)
|
||||
|
||||
habitica.cast_skill.side_effect = raise_exception_cast
|
||||
habitica.get_group_members.side_effect = raise_exception_members
|
||||
with pytest.raises(expected_exception, match=expected_exception_msg):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
|
@ -1,13 +1,11 @@
|
||||
"""Tests for the Habitica switch platform."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from http import HTTPStatus
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.habitica.const import DEFAULT_URL
|
||||
from homeassistant.components.switch import (
|
||||
DOMAIN as SWITCH_DOMAIN,
|
||||
SERVICE_TOGGLE,
|
||||
@ -17,13 +15,12 @@ from homeassistant.components.switch import (
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import ATTR_ENTITY_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .conftest import mock_called_with
|
||||
from .conftest import ERROR_BAD_REQUEST, ERROR_TOO_MANY_REQUESTS
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@ -36,7 +33,7 @@ def switch_only() -> Generator[None]:
|
||||
yield
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_habitica")
|
||||
@pytest.mark.usefixtures("habitica")
|
||||
async def test_switch(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
@ -66,14 +63,10 @@ async def test_turn_on_off_toggle(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
service_call: str,
|
||||
mock_habitica: AiohttpClientMocker,
|
||||
habitica: AsyncMock,
|
||||
) -> None:
|
||||
"""Test switch turn on/off, toggle method."""
|
||||
|
||||
mock_habitica.post(
|
||||
f"{DEFAULT_URL}/api/v3/user/sleep",
|
||||
json={"success": True, "data": False},
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
@ -87,7 +80,7 @@ async def test_turn_on_off_toggle(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_called_with(mock_habitica, "post", f"{DEFAULT_URL}/api/v3/user/sleep")
|
||||
habitica.toggle_sleep.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@ -99,19 +92,19 @@ async def test_turn_on_off_toggle(
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
("status_code", "exception"),
|
||||
("raise_exception", "expected_exception"),
|
||||
[
|
||||
(HTTPStatus.TOO_MANY_REQUESTS, ServiceValidationError),
|
||||
(HTTPStatus.BAD_REQUEST, HomeAssistantError),
|
||||
(ERROR_TOO_MANY_REQUESTS, HomeAssistantError),
|
||||
(ERROR_BAD_REQUEST, HomeAssistantError),
|
||||
],
|
||||
)
|
||||
async def test_turn_on_off_toggle_exceptions(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
service_call: str,
|
||||
mock_habitica: AiohttpClientMocker,
|
||||
status_code: HTTPStatus,
|
||||
exception: Exception,
|
||||
habitica: AsyncMock,
|
||||
raise_exception: Exception,
|
||||
expected_exception: Exception,
|
||||
) -> None:
|
||||
"""Test switch turn on/off, toggle method."""
|
||||
|
||||
@ -121,18 +114,12 @@ async def test_turn_on_off_toggle_exceptions(
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
mock_habitica.post(
|
||||
f"{DEFAULT_URL}/api/v3/user/sleep",
|
||||
status=status_code,
|
||||
json={"success": True, "data": False},
|
||||
)
|
||||
habitica.toggle_sleep.side_effect = raise_exception
|
||||
|
||||
with pytest.raises(expected_exception=exception):
|
||||
with pytest.raises(expected_exception=expected_exception):
|
||||
await hass.services.async_call(
|
||||
SWITCH_DOMAIN,
|
||||
service_call,
|
||||
{ATTR_ENTITY_ID: "switch.test_user_rest_in_the_inn"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_called_with(mock_habitica, "post", f"{DEFAULT_URL}/api/v3/user/sleep")
|
||||
|
@ -1,15 +1,16 @@
|
||||
"""Tests for Habitica todo platform."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from http import HTTPStatus
|
||||
import json
|
||||
import re
|
||||
from unittest.mock import patch
|
||||
from datetime import date
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from uuid import UUID
|
||||
|
||||
from habiticalib import Direction, HabiticaTasksResponse, Task, TaskType
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.habitica.const import DEFAULT_URL, DOMAIN
|
||||
from homeassistant.components.habitica.const import DOMAIN
|
||||
from homeassistant.components.todo import (
|
||||
ATTR_DESCRIPTION,
|
||||
ATTR_DUE_DATE,
|
||||
@ -25,15 +26,14 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from .conftest import mock_called_with
|
||||
from .conftest import ERROR_NOT_FOUND
|
||||
|
||||
from tests.common import (
|
||||
MockConfigEntry,
|
||||
async_get_persistent_notifications,
|
||||
load_json_object_fixture,
|
||||
load_fixture,
|
||||
snapshot_platform,
|
||||
)
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
from tests.typing import WebSocketGenerator
|
||||
|
||||
|
||||
@ -47,7 +47,7 @@ def todo_only() -> Generator[None]:
|
||||
yield
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("mock_habitica")
|
||||
@pytest.mark.usefixtures("habitica")
|
||||
async def test_todos(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
@ -72,7 +72,7 @@ async def test_todos(
|
||||
"todo.test_user_dailies",
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_habitica")
|
||||
@pytest.mark.usefixtures("habitica")
|
||||
async def test_todo_items(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
@ -111,7 +111,7 @@ async def test_todo_items(
|
||||
async def test_complete_todo_item(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_habitica: AiohttpClientMocker,
|
||||
habitica: AsyncMock,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_id: str,
|
||||
uid: str,
|
||||
@ -124,10 +124,6 @@ async def test_complete_todo_item(
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
mock_habitica.post(
|
||||
f"{DEFAULT_URL}/api/v3/tasks/{uid}/score/up",
|
||||
json=load_json_object_fixture("score_with_drop.json", DOMAIN),
|
||||
)
|
||||
await hass.services.async_call(
|
||||
TODO_DOMAIN,
|
||||
TodoServices.UPDATE_ITEM,
|
||||
@ -136,9 +132,7 @@ async def test_complete_todo_item(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_called_with(
|
||||
mock_habitica, "post", f"{DEFAULT_URL}/api/v3/tasks/{uid}/score/up"
|
||||
)
|
||||
habitica.update_score.assert_awaited_once_with(UUID(uid), Direction.UP)
|
||||
|
||||
# Test notification for item drop
|
||||
notifications = async_get_persistent_notifications(hass)
|
||||
@ -158,7 +152,7 @@ async def test_complete_todo_item(
|
||||
async def test_uncomplete_todo_item(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_habitica: AiohttpClientMocker,
|
||||
habitica: AsyncMock,
|
||||
entity_id: str,
|
||||
uid: str,
|
||||
) -> None:
|
||||
@ -170,10 +164,6 @@ async def test_uncomplete_todo_item(
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
mock_habitica.post(
|
||||
f"{DEFAULT_URL}/api/v3/tasks/{uid}/score/down",
|
||||
json={"data": {}, "success": True},
|
||||
)
|
||||
await hass.services.async_call(
|
||||
TODO_DOMAIN,
|
||||
TodoServices.UPDATE_ITEM,
|
||||
@ -182,9 +172,7 @@ async def test_uncomplete_todo_item(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_called_with(
|
||||
mock_habitica, "post", f"{DEFAULT_URL}/api/v3/tasks/{uid}/score/down"
|
||||
)
|
||||
habitica.update_score.assert_called_once_with(UUID(uid), Direction.DOWN)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@ -198,7 +186,7 @@ async def test_uncomplete_todo_item(
|
||||
async def test_complete_todo_item_exception(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_habitica: AiohttpClientMocker,
|
||||
habitica: AsyncMock,
|
||||
uid: str,
|
||||
status: str,
|
||||
) -> None:
|
||||
@ -210,10 +198,7 @@ async def test_complete_todo_item_exception(
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
mock_habitica.post(
|
||||
re.compile(f"{DEFAULT_URL}/api/v3/tasks/{uid}/score/.+"),
|
||||
status=HTTPStatus.NOT_FOUND,
|
||||
)
|
||||
habitica.update_score.side_effect = ERROR_NOT_FOUND
|
||||
with pytest.raises(
|
||||
expected_exception=ServiceValidationError,
|
||||
match=r"Unable to update the score for your Habitica to-do `.+`, please try again",
|
||||
@ -228,30 +213,86 @@ async def test_complete_todo_item_exception(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("entity_id", "uid", "date"),
|
||||
("entity_id", "service_data", "call_args"),
|
||||
[
|
||||
(
|
||||
"todo.test_user_to_do_s",
|
||||
"88de7cd9-af2b-49ce-9afd-bf941d87336b",
|
||||
"2024-07-30",
|
||||
{
|
||||
ATTR_ITEM: "88de7cd9-af2b-49ce-9afd-bf941d87336b",
|
||||
ATTR_RENAME: "test-summary",
|
||||
ATTR_DESCRIPTION: "test-description",
|
||||
ATTR_DUE_DATE: date(2024, 7, 30),
|
||||
},
|
||||
(
|
||||
UUID("88de7cd9-af2b-49ce-9afd-bf941d87336b"),
|
||||
Task(
|
||||
notes="test-description",
|
||||
text="test-summary",
|
||||
date=date(2024, 7, 30),
|
||||
),
|
||||
),
|
||||
),
|
||||
(
|
||||
"todo.test_user_to_do_s",
|
||||
{
|
||||
ATTR_ITEM: "88de7cd9-af2b-49ce-9afd-bf941d87336b",
|
||||
ATTR_RENAME: "test-summary",
|
||||
ATTR_DESCRIPTION: "test-description",
|
||||
ATTR_DUE_DATE: None,
|
||||
},
|
||||
(
|
||||
UUID("88de7cd9-af2b-49ce-9afd-bf941d87336b"),
|
||||
Task(
|
||||
notes="test-description",
|
||||
text="test-summary",
|
||||
date=None,
|
||||
),
|
||||
),
|
||||
),
|
||||
(
|
||||
"todo.test_user_to_do_s",
|
||||
{
|
||||
ATTR_ITEM: "88de7cd9-af2b-49ce-9afd-bf941d87336b",
|
||||
ATTR_RENAME: "test-summary",
|
||||
ATTR_DESCRIPTION: None,
|
||||
ATTR_DUE_DATE: date(2024, 7, 30),
|
||||
},
|
||||
(
|
||||
UUID("88de7cd9-af2b-49ce-9afd-bf941d87336b"),
|
||||
Task(
|
||||
notes="",
|
||||
text="test-summary",
|
||||
date=date(2024, 7, 30),
|
||||
),
|
||||
),
|
||||
),
|
||||
(
|
||||
"todo.test_user_dailies",
|
||||
"f2c85972-1a19-4426-bc6d-ce3337b9d99f",
|
||||
None,
|
||||
{
|
||||
ATTR_ITEM: "f2c85972-1a19-4426-bc6d-ce3337b9d99f",
|
||||
ATTR_RENAME: "test-summary",
|
||||
ATTR_DESCRIPTION: "test-description",
|
||||
},
|
||||
(
|
||||
UUID("f2c85972-1a19-4426-bc6d-ce3337b9d99f"),
|
||||
Task(
|
||||
notes="test-description",
|
||||
text="test-summary",
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
ids=["todo", "daily"],
|
||||
ids=["todo", "todo remove date", "todo remove notes", "daily"],
|
||||
)
|
||||
async def test_update_todo_item(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_habitica: AiohttpClientMocker,
|
||||
habitica: AsyncMock,
|
||||
entity_id: str,
|
||||
uid: str,
|
||||
date: str,
|
||||
service_data: dict[str, Any],
|
||||
call_args: tuple[UUID, Task],
|
||||
) -> None:
|
||||
"""Test update details of a item on the todo list."""
|
||||
"""Test update details of an item on the todo list."""
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
@ -259,38 +300,21 @@ async def test_update_todo_item(
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
mock_habitica.put(
|
||||
f"{DEFAULT_URL}/api/v3/tasks/{uid}",
|
||||
json={"data": {}, "success": True},
|
||||
)
|
||||
await hass.services.async_call(
|
||||
TODO_DOMAIN,
|
||||
TodoServices.UPDATE_ITEM,
|
||||
{
|
||||
ATTR_ITEM: uid,
|
||||
ATTR_RENAME: "test-summary",
|
||||
ATTR_DESCRIPTION: "test-description",
|
||||
ATTR_DUE_DATE: date,
|
||||
},
|
||||
service_data,
|
||||
target={ATTR_ENTITY_ID: entity_id},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_call = mock_called_with(
|
||||
mock_habitica, "PUT", f"{DEFAULT_URL}/api/v3/tasks/{uid}"
|
||||
)
|
||||
assert mock_call
|
||||
assert json.loads(mock_call[2]) == {
|
||||
"date": date,
|
||||
"notes": "test-description",
|
||||
"text": "test-summary",
|
||||
}
|
||||
habitica.update_task.assert_awaited_once_with(*call_args)
|
||||
|
||||
|
||||
async def test_update_todo_item_exception(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_habitica: AiohttpClientMocker,
|
||||
habitica: AsyncMock,
|
||||
) -> None:
|
||||
"""Test exception when update item on the todo list."""
|
||||
uid = "88de7cd9-af2b-49ce-9afd-bf941d87336b"
|
||||
@ -300,10 +324,7 @@ async def test_update_todo_item_exception(
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
mock_habitica.put(
|
||||
f"{DEFAULT_URL}/api/v3/tasks/{uid}",
|
||||
status=HTTPStatus.NOT_FOUND,
|
||||
)
|
||||
habitica.update_task.side_effect = ERROR_NOT_FOUND
|
||||
with pytest.raises(
|
||||
expected_exception=ServiceValidationError,
|
||||
match="Unable to update the Habitica to-do `test-summary`, please try again",
|
||||
@ -325,7 +346,7 @@ async def test_update_todo_item_exception(
|
||||
async def test_add_todo_item(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_habitica: AiohttpClientMocker,
|
||||
habitica: AsyncMock,
|
||||
) -> None:
|
||||
"""Test add a todo item to the todo list."""
|
||||
|
||||
@ -335,11 +356,6 @@ async def test_add_todo_item(
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
mock_habitica.post(
|
||||
f"{DEFAULT_URL}/api/v3/tasks/user",
|
||||
json={"data": {}, "success": True},
|
||||
status=HTTPStatus.CREATED,
|
||||
)
|
||||
await hass.services.async_call(
|
||||
TODO_DOMAIN,
|
||||
TodoServices.ADD_ITEM,
|
||||
@ -352,24 +368,20 @@ async def test_add_todo_item(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mock_call = mock_called_with(
|
||||
mock_habitica,
|
||||
"post",
|
||||
f"{DEFAULT_URL}/api/v3/tasks/user",
|
||||
habitica.create_task.assert_awaited_once_with(
|
||||
Task(
|
||||
date=date(2024, 7, 30),
|
||||
notes="test-description",
|
||||
text="test-summary",
|
||||
type=TaskType.TODO,
|
||||
)
|
||||
)
|
||||
assert mock_call
|
||||
assert json.loads(mock_call[2]) == {
|
||||
"date": "2024-07-30",
|
||||
"notes": "test-description",
|
||||
"text": "test-summary",
|
||||
"type": "todo",
|
||||
}
|
||||
|
||||
|
||||
async def test_add_todo_item_exception(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_habitica: AiohttpClientMocker,
|
||||
habitica: AsyncMock,
|
||||
) -> None:
|
||||
"""Test exception when adding a todo item to the todo list."""
|
||||
|
||||
@ -379,10 +391,7 @@ async def test_add_todo_item_exception(
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
mock_habitica.post(
|
||||
f"{DEFAULT_URL}/api/v3/tasks/user",
|
||||
status=HTTPStatus.NOT_FOUND,
|
||||
)
|
||||
habitica.create_task.side_effect = ERROR_NOT_FOUND
|
||||
with pytest.raises(
|
||||
expected_exception=ServiceValidationError,
|
||||
match="Unable to create new to-do `test-summary` for Habitica, please try again",
|
||||
@ -403,7 +412,7 @@ async def test_add_todo_item_exception(
|
||||
async def test_delete_todo_item(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_habitica: AiohttpClientMocker,
|
||||
habitica: AsyncMock,
|
||||
) -> None:
|
||||
"""Test deleting a todo item from the todo list."""
|
||||
|
||||
@ -414,10 +423,6 @@ async def test_delete_todo_item(
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
mock_habitica.delete(
|
||||
f"{DEFAULT_URL}/api/v3/tasks/{uid}",
|
||||
json={"data": {}, "success": True},
|
||||
)
|
||||
await hass.services.async_call(
|
||||
TODO_DOMAIN,
|
||||
TodoServices.REMOVE_ITEM,
|
||||
@ -426,15 +431,13 @@ async def test_delete_todo_item(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_called_with(
|
||||
mock_habitica, "delete", f"{DEFAULT_URL}/api/v3/tasks/{uid}"
|
||||
)
|
||||
habitica.delete_task.assert_awaited_once_with(UUID(uid))
|
||||
|
||||
|
||||
async def test_delete_todo_item_exception(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_habitica: AiohttpClientMocker,
|
||||
habitica: AsyncMock,
|
||||
) -> None:
|
||||
"""Test exception when deleting a todo item from the todo list."""
|
||||
|
||||
@ -445,10 +448,8 @@ async def test_delete_todo_item_exception(
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
mock_habitica.delete(
|
||||
f"{DEFAULT_URL}/api/v3/tasks/{uid}",
|
||||
status=HTTPStatus.NOT_FOUND,
|
||||
)
|
||||
habitica.delete_task.side_effect = ERROR_NOT_FOUND
|
||||
|
||||
with pytest.raises(
|
||||
expected_exception=ServiceValidationError,
|
||||
match="Unable to delete item from Habitica to-do list, please try again",
|
||||
@ -465,7 +466,7 @@ async def test_delete_todo_item_exception(
|
||||
async def test_delete_completed_todo_items(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_habitica: AiohttpClientMocker,
|
||||
habitica: AsyncMock,
|
||||
) -> None:
|
||||
"""Test deleting completed todo items from the todo list."""
|
||||
config_entry.add_to_hass(hass)
|
||||
@ -474,10 +475,6 @@ async def test_delete_completed_todo_items(
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
mock_habitica.post(
|
||||
f"{DEFAULT_URL}/api/v3/tasks/clearCompletedTodos",
|
||||
json={"data": {}, "success": True},
|
||||
)
|
||||
await hass.services.async_call(
|
||||
TODO_DOMAIN,
|
||||
TodoServices.REMOVE_COMPLETED_ITEMS,
|
||||
@ -486,15 +483,13 @@ async def test_delete_completed_todo_items(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
assert mock_called_with(
|
||||
mock_habitica, "post", f"{DEFAULT_URL}/api/v3/tasks/clearCompletedTodos"
|
||||
)
|
||||
habitica.delete_completed_todos.assert_awaited_once()
|
||||
|
||||
|
||||
async def test_delete_completed_todo_items_exception(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_habitica: AiohttpClientMocker,
|
||||
habitica: AsyncMock,
|
||||
) -> None:
|
||||
"""Test exception when deleting completed todo items from the todo list."""
|
||||
config_entry.add_to_hass(hass)
|
||||
@ -503,10 +498,7 @@ async def test_delete_completed_todo_items_exception(
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
mock_habitica.post(
|
||||
f"{DEFAULT_URL}/api/v3/tasks/clearCompletedTodos",
|
||||
status=HTTPStatus.NOT_FOUND,
|
||||
)
|
||||
habitica.delete_completed_todos.side_effect = ERROR_NOT_FOUND
|
||||
with pytest.raises(
|
||||
expected_exception=ServiceValidationError,
|
||||
match="Unable to delete completed to-do items from Habitica to-do list, please try again",
|
||||
@ -539,7 +531,7 @@ async def test_delete_completed_todo_items_exception(
|
||||
async def test_move_todo_item(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_habitica: AiohttpClientMocker,
|
||||
habitica: AsyncMock,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
entity_id: str,
|
||||
uid: str,
|
||||
@ -553,12 +545,6 @@ async def test_move_todo_item(
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
for pos in (0, 1):
|
||||
mock_habitica.post(
|
||||
f"{DEFAULT_URL}/api/v3/tasks/{uid}/move/to/{pos}",
|
||||
json={"data": {}, "success": True},
|
||||
)
|
||||
|
||||
client = await hass_ws_client()
|
||||
# move to second position
|
||||
data = {
|
||||
@ -572,6 +558,9 @@ async def test_move_todo_item(
|
||||
resp = await client.receive_json()
|
||||
assert resp.get("success")
|
||||
|
||||
habitica.reorder_task.assert_awaited_once_with(UUID(uid), 1)
|
||||
habitica.reorder_task.reset_mock()
|
||||
|
||||
# move to top position
|
||||
data = {
|
||||
"id": id,
|
||||
@ -583,18 +572,13 @@ async def test_move_todo_item(
|
||||
resp = await client.receive_json()
|
||||
assert resp.get("success")
|
||||
|
||||
for pos in (0, 1):
|
||||
assert mock_called_with(
|
||||
mock_habitica,
|
||||
"post",
|
||||
f"{DEFAULT_URL}/api/v3/tasks/{uid}/move/to/{pos}",
|
||||
)
|
||||
habitica.reorder_task.assert_awaited_once_with(UUID(uid), 0)
|
||||
|
||||
|
||||
async def test_move_todo_item_exception(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_habitica: AiohttpClientMocker,
|
||||
habitica: AsyncMock,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
) -> None:
|
||||
"""Test exception when moving todo item."""
|
||||
@ -606,11 +590,7 @@ async def test_move_todo_item_exception(
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
mock_habitica.post(
|
||||
f"{DEFAULT_URL}/api/v3/tasks/{uid}/move/to/0",
|
||||
status=HTTPStatus.NOT_FOUND,
|
||||
)
|
||||
|
||||
habitica.reorder_task.side_effect = ERROR_NOT_FOUND
|
||||
client = await hass_ws_client()
|
||||
|
||||
data = {
|
||||
@ -620,8 +600,15 @@ async def test_move_todo_item_exception(
|
||||
"uid": uid,
|
||||
}
|
||||
await client.send_json_auto_id(data)
|
||||
|
||||
resp = await client.receive_json()
|
||||
assert resp.get("success") is False
|
||||
habitica.reorder_task.assert_awaited_once_with(UUID(uid), 0)
|
||||
|
||||
assert resp["success"] is False
|
||||
assert (
|
||||
resp["error"]["message"]
|
||||
== "Unable to move the Habitica to-do to position 0, please try again"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@ -651,31 +638,18 @@ async def test_move_todo_item_exception(
|
||||
async def test_next_due_date(
|
||||
hass: HomeAssistant,
|
||||
fixture: str,
|
||||
calculated_due_date: tuple | None,
|
||||
calculated_due_date: str | None,
|
||||
config_entry: MockConfigEntry,
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
habitica: AsyncMock,
|
||||
) -> None:
|
||||
"""Test next_due_date calculation."""
|
||||
|
||||
dailies_entity = "todo.test_user_dailies"
|
||||
|
||||
aioclient_mock.get(
|
||||
f"{DEFAULT_URL}/api/v3/user", json=load_json_object_fixture("user.json", DOMAIN)
|
||||
)
|
||||
aioclient_mock.get(
|
||||
f"{DEFAULT_URL}/api/v3/tasks/user",
|
||||
params={"type": "completedTodos"},
|
||||
json={"data": []},
|
||||
)
|
||||
aioclient_mock.get(
|
||||
f"{DEFAULT_URL}/api/v3/tasks/user",
|
||||
json=load_json_object_fixture(fixture, DOMAIN),
|
||||
)
|
||||
aioclient_mock.get(
|
||||
f"{DEFAULT_URL}/api/v3/content",
|
||||
params={"language": "en"},
|
||||
json=load_json_object_fixture("content.json", DOMAIN),
|
||||
)
|
||||
habitica.get_tasks.side_effect = [
|
||||
HabiticaTasksResponse.from_json(load_fixture(fixture, DOMAIN)),
|
||||
HabiticaTasksResponse.from_dict({"success": True, "data": []}),
|
||||
]
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
|
Loading…
x
Reference in New Issue
Block a user