Add sensors for attribute points (str, int, per, con) to Habitica (#130186)

pull/129616/head^2
Manu 2024-11-09 16:04:10 +01:00 committed by GitHub
parent e6d16f06fc
commit c10f078f2a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 1047 additions and 96 deletions

View File

@ -51,12 +51,17 @@ class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]):
),
)
self.api = habitipy
self.content: dict[str, Any] = {}
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")

View File

@ -126,6 +126,18 @@
},
"rewards": {
"default": "mdi:treasure-chest"
},
"strength": {
"default": "mdi:arm-flex-outline"
},
"intelligence": {
"default": "mdi:head-snowflake-outline"
},
"perception": {
"default": "mdi:eye-outline"
},
"constitution": {
"default": "mdi:run-fast"
}
},
"switch": {

View File

@ -27,7 +27,7 @@ from homeassistant.helpers.typing import StateType
from .const import DOMAIN, UNIT_TASKS
from .entity import HabiticaBase
from .types import HabiticaConfigEntry
from .util import entity_used_in
from .util import entity_used_in, get_attribute_points, get_attributes_total
_LOGGER = logging.getLogger(__name__)
@ -36,7 +36,10 @@ _LOGGER = logging.getLogger(__name__)
class HabitipySensorEntityDescription(SensorEntityDescription):
"""Habitipy Sensor Description."""
value_fn: Callable[[dict[str, Any]], StateType]
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
@dataclass(kw_only=True, frozen=True)
@ -65,76 +68,80 @@ class HabitipySensorEntity(StrEnum):
REWARDS = "rewards"
GEMS = "gems"
TRINKETS = "trinkets"
STRENGTH = "strength"
INTELLIGENCE = "intelligence"
CONSTITUTION = "constitution"
PERCEPTION = "perception"
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.get("profile", {}).get("name"),
),
HabitipySensorEntityDescription(
key=HabitipySensorEntity.HEALTH,
translation_key=HabitipySensorEntity.HEALTH,
native_unit_of_measurement="HP",
suggested_display_precision=0,
value_fn=lambda user: user.get("stats", {}).get("hp"),
value_fn=lambda user, _: user.get("stats", {}).get("hp"),
),
HabitipySensorEntityDescription(
key=HabitipySensorEntity.HEALTH_MAX,
translation_key=HabitipySensorEntity.HEALTH_MAX,
native_unit_of_measurement="HP",
entity_registry_enabled_default=False,
value_fn=lambda user: user.get("stats", {}).get("maxHealth"),
value_fn=lambda user, _: user.get("stats", {}).get("maxHealth"),
),
HabitipySensorEntityDescription(
key=HabitipySensorEntity.MANA,
translation_key=HabitipySensorEntity.MANA,
native_unit_of_measurement="MP",
suggested_display_precision=0,
value_fn=lambda user: user.get("stats", {}).get("mp"),
value_fn=lambda user, _: user.get("stats", {}).get("mp"),
),
HabitipySensorEntityDescription(
key=HabitipySensorEntity.MANA_MAX,
translation_key=HabitipySensorEntity.MANA_MAX,
native_unit_of_measurement="MP",
value_fn=lambda user: user.get("stats", {}).get("maxMP"),
value_fn=lambda user, _: user.get("stats", {}).get("maxMP"),
),
HabitipySensorEntityDescription(
key=HabitipySensorEntity.EXPERIENCE,
translation_key=HabitipySensorEntity.EXPERIENCE,
native_unit_of_measurement="XP",
value_fn=lambda user: user.get("stats", {}).get("exp"),
value_fn=lambda user, _: user.get("stats", {}).get("exp"),
),
HabitipySensorEntityDescription(
key=HabitipySensorEntity.EXPERIENCE_MAX,
translation_key=HabitipySensorEntity.EXPERIENCE_MAX,
native_unit_of_measurement="XP",
value_fn=lambda user: user.get("stats", {}).get("toNextLevel"),
value_fn=lambda user, _: user.get("stats", {}).get("toNextLevel"),
),
HabitipySensorEntityDescription(
key=HabitipySensorEntity.LEVEL,
translation_key=HabitipySensorEntity.LEVEL,
value_fn=lambda user: user.get("stats", {}).get("lvl"),
value_fn=lambda user, _: user.get("stats", {}).get("lvl"),
),
HabitipySensorEntityDescription(
key=HabitipySensorEntity.GOLD,
translation_key=HabitipySensorEntity.GOLD,
native_unit_of_measurement="GP",
suggested_display_precision=2,
value_fn=lambda user: user.get("stats", {}).get("gp"),
value_fn=lambda user, _: user.get("stats", {}).get("gp"),
),
HabitipySensorEntityDescription(
key=HabitipySensorEntity.CLASS,
translation_key=HabitipySensorEntity.CLASS,
value_fn=lambda user: user.get("stats", {}).get("class"),
value_fn=lambda user, _: user.get("stats", {}).get("class"),
device_class=SensorDeviceClass.ENUM,
options=["warrior", "healer", "wizard", "rogue"],
),
HabitipySensorEntityDescription(
key=HabitipySensorEntity.GEMS,
translation_key=HabitipySensorEntity.GEMS,
value_fn=lambda user: user.get("balance", 0) * 4,
value_fn=lambda user, _: user.get("balance", 0) * 4,
suggested_display_precision=0,
native_unit_of_measurement="gems",
),
@ -142,7 +149,7 @@ SENSOR_DESCRIPTIONS: tuple[HabitipySensorEntityDescription, ...] = (
key=HabitipySensorEntity.TRINKETS,
translation_key=HabitipySensorEntity.TRINKETS,
value_fn=(
lambda user: user.get("purchased", {})
lambda user, _: user.get("purchased", {})
.get("plan", {})
.get("consecutive", {})
.get("trinkets", 0)
@ -150,6 +157,38 @@ SENSOR_DESCRIPTIONS: tuple[HabitipySensorEntityDescription, ...] = (
suggested_display_precision=0,
native_unit_of_measurement="",
),
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"),
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"),
suggested_display_precision=0,
native_unit_of_measurement="INT",
),
HabitipySensorEntityDescription(
key=HabitipySensorEntity.PERCEPTION,
translation_key=HabitipySensorEntity.PERCEPTION,
value_fn=lambda user, content: get_attributes_total(user, content, "per"),
attributes_fn=lambda user, content: get_attribute_points(user, content, "per"),
suggested_display_precision=0,
native_unit_of_measurement="PER",
),
HabitipySensorEntityDescription(
key=HabitipySensorEntity.CONSTITUTION,
translation_key=HabitipySensorEntity.CONSTITUTION,
value_fn=lambda user, content: get_attributes_total(user, content, "con"),
attributes_fn=lambda user, content: get_attribute_points(user, content, "con"),
suggested_display_precision=0,
native_unit_of_measurement="CON",
),
)
@ -243,7 +282,16 @@ class HabitipySensor(HabiticaBase, SensorEntity):
def native_value(self) -> StateType:
"""Return the state of the device."""
return self.entity_description.value_fn(self.coordinator.data.user)
return self.entity_description.value_fn(
self.coordinator.data.user, self.coordinator.content
)
@property
def extra_state_attributes(self) -> dict[str, float | None] | None:
"""Return entity specific state attributes."""
if func := self.entity_description.attributes_fn:
return func(self.coordinator.data.user, self.coordinator.content)
return None
class HabitipyTaskSensor(HabiticaBase, SensorEntity):

View File

@ -164,6 +164,86 @@
},
"rewards": {
"name": "Rewards"
},
"strength": {
"name": "Strength",
"state_attributes": {
"level": {
"name": "[%key:component::habitica::entity::sensor::level::name%]"
},
"equipment": {
"name": "Battle gear"
},
"class": {
"name": "Class equip bonus"
},
"allocated": {
"name": "Allocated attribute points"
},
"buffs": {
"name": "Buffs"
}
}
},
"intelligence": {
"name": "Intelligence",
"state_attributes": {
"level": {
"name": "[%key:component::habitica::entity::sensor::level::name%]"
},
"equipment": {
"name": "[%key:component::habitica::entity::sensor::strength::state_attributes::equipment::name%]"
},
"class": {
"name": "[%key:component::habitica::entity::sensor::strength::state_attributes::class::name%]"
},
"allocated": {
"name": "[%key:component::habitica::entity::sensor::strength::state_attributes::allocated::name%]"
},
"buffs": {
"name": "[%key:component::habitica::entity::sensor::strength::state_attributes::buffs::name%]"
}
}
},
"perception": {
"name": "Perception",
"state_attributes": {
"level": {
"name": "[%key:component::habitica::entity::sensor::level::name%]"
},
"equipment": {
"name": "[%key:component::habitica::entity::sensor::strength::state_attributes::equipment::name%]"
},
"class": {
"name": "[%key:component::habitica::entity::sensor::strength::state_attributes::class::name%]"
},
"allocated": {
"name": "[%key:component::habitica::entity::sensor::strength::state_attributes::allocated::name%]"
},
"buffs": {
"name": "[%key:component::habitica::entity::sensor::strength::state_attributes::buffs::name%]"
}
}
},
"constitution": {
"name": "Constitution",
"state_attributes": {
"level": {
"name": "[%key:component::habitica::entity::sensor::level::name%]"
},
"equipment": {
"name": "[%key:component::habitica::entity::sensor::strength::state_attributes::equipment::name%]"
},
"class": {
"name": "[%key:component::habitica::entity::sensor::strength::state_attributes::class::name%]"
},
"allocated": {
"name": "[%key:component::habitica::entity::sensor::strength::state_attributes::allocated::name%]"
},
"buffs": {
"name": "[%key:component::habitica::entity::sensor::strength::state_attributes::buffs::name%]"
}
}
}
},
"switch": {

View File

@ -3,6 +3,7 @@
from __future__ import annotations
import datetime
from math import floor
from typing import TYPE_CHECKING, Any
from dateutil.rrule import (
@ -139,3 +140,52 @@ def get_recurrence_rule(recurrence: rrule) -> str:
"""
return str(recurrence).split("RRULE:")[1]
def get_attribute_points(
user: dict[str, Any], content: dict[str, Any], attribute: str
) -> dict[str, float]:
"""Get modifiers contributing to strength attribute."""
gear_set = {
"weapon",
"armor",
"head",
"shield",
"back",
"headAccessory",
"eyewear",
"body",
}
equipment = sum(
stats[attribute]
for gear in gear_set
if (equipped := user["items"]["gear"]["equipped"].get(gear))
and (stats := content["gear"]["flat"].get(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"]
)
return {
"level": min(round(user["stats"]["lvl"] / 2), 50),
"equipment": equipment,
"class": class_bonus,
"allocated": user["stats"][attribute],
"buffs": user["stats"]["buffs"][attribute],
}
def get_attributes_total(
user: dict[str, Any], content: dict[str, Any], attribute: str
) -> int:
"""Get total attribute points."""
return floor(
sum(value for value in get_attribute_points(user, content, attribute).values())
)

View File

@ -56,6 +56,11 @@ def mock_habitica(aioclient_mock: AiohttpClientMocker) -> AiohttpClientMocker:
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),
)
return aioclient_mock

View File

@ -29,11 +29,26 @@
"preferences": {
"sleep": false,
"automaticAllocation": false,
"disableClasses": false
"disableClasses": false,
"language": "en"
},
"flags": {
"classSelected": true
},
"needsCron": false
"needsCron": false,
"items": {
"gear": {
"equipped": {
"weapon": "weapon_warrior_5",
"armor": "armor_warrior_5",
"head": "head_warrior_5",
"shield": "shield_warrior_5",
"back": "heroicAureole",
"headAccessory": "headAccessory_armoire_gogglesOfBookbinding",
"eyewear": "plagueDoctorMask",
"body": "aetherAmulet"
}
}
}
}
}

View File

@ -0,0 +1,287 @@
{
"success": true,
"data": {
"gear": {
"flat": {
"weapon_warrior_5": {
"text": "Ruby Sword",
"notes": "Weapon whose forge-glow never fades. Increases Strength by 15. ",
"str": 15,
"value": 90,
"type": "weapon",
"key": "weapon_warrior_5",
"set": "warrior-5",
"klass": "warrior",
"index": "5",
"int": 0,
"per": 0,
"con": 0
},
"armor_warrior_5": {
"text": "Golden Armor",
"notes": "Looks ceremonial, but no known blade can pierce it. Increases Constitution by 11.",
"con": 11,
"value": 120,
"last": true,
"type": "armor",
"key": "armor_warrior_5",
"set": "warrior-5",
"klass": "warrior",
"index": "5",
"str": 0,
"int": 0,
"per": 0
},
"head_warrior_5": {
"text": "Golden Helm",
"notes": "Regal crown bound to shining armor. Increases Strength by 12.",
"str": 12,
"value": 80,
"last": true,
"type": "head",
"key": "head_warrior_5",
"set": "warrior-5",
"klass": "warrior",
"index": "5",
"int": 0,
"per": 0,
"con": 0
},
"shield_warrior_5": {
"text": "Golden Shield",
"notes": "Shining badge of the vanguard. Increases Constitution by 9.",
"con": 9,
"value": 90,
"last": true,
"type": "shield",
"key": "shield_warrior_5",
"set": "warrior-5",
"klass": "warrior",
"index": "5",
"str": 0,
"int": 0,
"per": 0
},
"weapon_wizard_5": {
"twoHanded": true,
"text": "Archmage Staff",
"notes": "Assists in weaving the most complex of spells. Increases Intelligence by 15 and Perception by 7. Two-handed item.",
"int": 15,
"per": 7,
"value": 160,
"type": "weapon",
"key": "weapon_wizard_5",
"set": "wizard-5",
"klass": "wizard",
"index": "5",
"str": 0,
"con": 0
},
"armor_wizard_5": {
"text": "Royal Magus Robe",
"notes": "Symbol of the power behind the throne. Increases Intelligence by 12.",
"int": 12,
"value": 120,
"last": true,
"type": "armor",
"key": "armor_wizard_5",
"set": "wizard-5",
"klass": "wizard",
"index": "5",
"str": 0,
"per": 0,
"con": 0
},
"head_wizard_5": {
"text": "Royal Magus Hat",
"notes": "Shows authority over fortune, weather, and lesser mages. Increases Perception by 10.",
"per": 10,
"value": 80,
"last": true,
"type": "head",
"key": "head_wizard_5",
"set": "wizard-5",
"klass": "wizard",
"index": "5",
"str": 0,
"int": 0,
"con": 0
},
"weapon_healer_5": {
"text": "Royal Scepter",
"notes": "Fit to grace the hand of a monarch, or of one who stands at a monarch's right hand. Increases Intelligence by 9. ",
"int": 9,
"value": 90,
"type": "weapon",
"key": "weapon_healer_5",
"set": "healer-5",
"klass": "healer",
"index": "5",
"str": 0,
"per": 0,
"con": 0
},
"armor_healer_5": {
"text": "Royal Mantle",
"notes": "Attire of those who have saved the lives of kings. Increases Constitution by 18.",
"con": 18,
"value": 120,
"last": true,
"type": "armor",
"key": "armor_healer_5",
"set": "healer-5",
"klass": "healer",
"index": "5",
"str": 0,
"int": 0,
"per": 0
},
"head_healer_5": {
"text": "Royal Diadem",
"notes": "For king, queen, or miracle-worker. Increases Intelligence by 9.",
"int": 9,
"value": 80,
"last": true,
"type": "head",
"key": "head_healer_5",
"set": "healer-5",
"klass": "healer",
"index": "5",
"str": 0,
"per": 0,
"con": 0
},
"shield_healer_5": {
"text": "Royal Shield",
"notes": "Bestowed upon those most dedicated to the kingdom's defense. Increases Constitution by 12.",
"con": 12,
"value": 90,
"last": true,
"type": "shield",
"key": "shield_healer_5",
"set": "healer-5",
"klass": "healer",
"index": "5",
"str": 0,
"int": 0,
"per": 0
},
"weapon_rogue_5": {
"text": "Ninja-to",
"notes": "Sleek and deadly as the ninja themselves. Increases Strength by 8. ",
"str": 8,
"value": 90,
"type": "weapon",
"key": "weapon_rogue_5",
"set": "rogue-5",
"klass": "rogue",
"index": "5",
"int": 0,
"per": 0,
"con": 0
},
"armor_rogue_5": {
"text": "Umbral Armor",
"notes": "Allows stealth in the open in broad daylight. Increases Perception by 18.",
"per": 18,
"value": 120,
"last": true,
"type": "armor",
"key": "armor_rogue_5",
"set": "rogue-5",
"klass": "rogue",
"index": "5",
"str": 0,
"int": 0,
"con": 0
},
"head_rogue_5": {
"text": "Umbral Hood",
"notes": "Conceals even thoughts from those who would probe them. Increases Perception by 12.",
"per": 12,
"value": 80,
"last": true,
"type": "head",
"key": "head_rogue_5",
"set": "rogue-5",
"klass": "rogue",
"index": "5",
"str": 0,
"int": 0,
"con": 0
},
"shield_rogue_5": {
"text": "Ninja-to",
"notes": "Sleek and deadly as the ninja themselves. Increases Strength by 8. ",
"str": 8,
"value": 90,
"type": "shield",
"key": "shield_rogue_5",
"set": "rogue-5",
"klass": "rogue",
"index": "5",
"int": 0,
"per": 0,
"con": 0
},
"back_special_heroicAureole": {
"text": "Heroic Aureole",
"notes": "The gems on this aureole glimmer when you tell your tales of glory. Increases all stats by 7.",
"con": 7,
"str": 7,
"per": 7,
"int": 7,
"value": 175,
"type": "back",
"key": "back_special_heroicAureole",
"set": "special-heroicAureole",
"klass": "special",
"index": "heroicAureole"
},
"headAccessory_armoire_gogglesOfBookbinding": {
"per": 8,
"set": "bookbinder",
"notes": "These goggles will help you zero in on any task, large or small! Increases Perception by 8. Enchanted Armoire: Bookbinder Set (Item 1 of 4).",
"text": "Goggles of Bookbinding",
"value": 100,
"type": "headAccessory",
"key": "headAccessory_armoire_gogglesOfBookbinding",
"klass": "armoire",
"index": "gogglesOfBookbinding",
"str": 0,
"int": 0,
"con": 0
},
"eyewear_armoire_plagueDoctorMask": {
"con": 5,
"int": 5,
"set": "plagueDoctor",
"notes": "An authentic mask worn by the doctors who battle the Plague of Procrastination. Increases Constitution and Intelligence by 5 each. Enchanted Armoire: Plague Doctor Set (Item 2 of 3).",
"text": "Plague Doctor Mask",
"value": 100,
"type": "eyewear",
"key": "eyewear_armoire_plagueDoctorMask",
"klass": "armoire",
"index": "plagueDoctorMask",
"str": 0,
"per": 0
},
"body_special_aetherAmulet": {
"text": "Aether Amulet",
"notes": "This amulet has a mysterious history. Increases Constitution and Strength by 10 each.",
"value": 175,
"str": 10,
"con": 10,
"type": "body",
"key": "body_special_aetherAmulet",
"set": "special-aetherAmulet",
"klass": "special",
"index": "aetherAmulet",
"int": 0,
"per": 0
}
}
}
},
"appVersion": "5.29.2"
}

View File

@ -4,10 +4,10 @@
"profile": { "name": "test-user" },
"stats": {
"buffs": {
"str": 0,
"int": 0,
"per": 0,
"con": 0,
"str": 26,
"int": 26,
"per": 26,
"con": 26,
"stealth": 0,
"streaks": false,
"seafoam": false,
@ -24,17 +24,36 @@
"maxHealth": 50,
"maxMP": 166,
"toNextLevel": 880,
"points": 5
"points": 5,
"str": 15,
"con": 15,
"int": 15,
"per": 15
},
"preferences": {
"sleep": false,
"automaticAllocation": true,
"disableClasses": false
"disableClasses": false,
"language": "en"
},
"flags": {
"classSelected": true
},
"needsCron": true,
"lastCron": "2024-09-21T22:01:55.586Z"
"lastCron": "2024-09-21T22:01:55.586Z",
"items": {
"gear": {
"equipped": {
"weapon": "weapon_healer_5",
"armor": "armor_healer_5",
"head": "head_healer_5",
"shield": "shield_healer_5",
"back": "heroicAureole",
"headAccessory": "headAccessory_armoire_gogglesOfBookbinding",
"eyewear": "plagueDoctorMask",
"body": "aetherAmulet"
}
}
}
}
}

View File

@ -4,10 +4,10 @@
"profile": { "name": "test-user" },
"stats": {
"buffs": {
"str": 0,
"int": 0,
"per": 0,
"con": 0,
"str": 26,
"int": 26,
"per": 26,
"con": 26,
"stealth": 0,
"streaks": false,
"seafoam": false,
@ -24,16 +24,35 @@
"maxHealth": 50,
"maxMP": 166,
"toNextLevel": 880,
"points": 0
"points": 0,
"str": 15,
"con": 15,
"int": 15,
"per": 15
},
"preferences": {
"sleep": false,
"automaticAllocation": false,
"disableClasses": false
"disableClasses": false,
"language": "en"
},
"flags": {
"classSelected": true
},
"needsCron": false
"needsCron": false,
"items": {
"gear": {
"equipped": {
"weapon": "weapon_healer_5",
"armor": "armor_healer_5",
"head": "head_healer_5",
"shield": "shield_healer_5",
"back": "heroicAureole",
"headAccessory": "headAccessory_armoire_gogglesOfBookbinding",
"eyewear": "plagueDoctorMask",
"body": "aetherAmulet"
}
}
}
}
}

View File

@ -29,7 +29,8 @@
"preferences": {
"sleep": false,
"automaticAllocation": true,
"disableClasses": false
"disableClasses": false,
"language": "en"
},
"flags": {
"classSelected": true

View File

@ -4,10 +4,10 @@
"profile": { "name": "test-user" },
"stats": {
"buffs": {
"str": 0,
"int": 0,
"per": 0,
"con": 0,
"str": 26,
"int": 26,
"per": 26,
"con": 26,
"stealth": 0,
"streaks": false,
"seafoam": false,
@ -24,17 +24,36 @@
"maxHealth": 50,
"maxMP": 166,
"toNextLevel": 880,
"points": 5
"points": 5,
"str": 15,
"con": 15,
"int": 15,
"per": 15
},
"preferences": {
"sleep": false,
"automaticAllocation": true,
"disableClasses": false
"disableClasses": false,
"language": "en"
},
"flags": {
"classSelected": true
},
"needsCron": true,
"lastCron": "2024-09-21T22:01:55.586Z"
"lastCron": "2024-09-21T22:01:55.586Z",
"items": {
"gear": {
"equipped": {
"weapon": "weapon_rogue_5",
"armor": "armor_rogue_5",
"head": "head_rogue_5",
"shield": "shield_rogue_5",
"back": "heroicAureole",
"headAccessory": "headAccessory_armoire_gogglesOfBookbinding",
"eyewear": "plagueDoctorMask",
"body": "aetherAmulet"
}
}
}
}
}

View File

@ -4,10 +4,10 @@
"profile": { "name": "test-user" },
"stats": {
"buffs": {
"str": 0,
"int": 0,
"per": 0,
"con": 0,
"str": 26,
"int": 26,
"per": 26,
"con": 26,
"stealth": 0,
"streaks": true,
"seafoam": false,
@ -24,16 +24,35 @@
"maxHealth": 50,
"maxMP": 166,
"toNextLevel": 880,
"points": 0
"points": 0,
"str": 15,
"con": 15,
"int": 15,
"per": 15
},
"preferences": {
"sleep": false,
"automaticAllocation": false,
"disableClasses": false
"disableClasses": false,
"language": "en"
},
"flags": {
"classSelected": true
},
"needsCron": false
"needsCron": false,
"items": {
"gear": {
"equipped": {
"weapon": "weapon_rogue_5",
"armor": "armor_rogue_5",
"head": "head_rogue_5",
"shield": "shield_rogue_5",
"back": "heroicAureole",
"headAccessory": "headAccessory_armoire_gogglesOfBookbinding",
"eyewear": "plagueDoctorMask",
"body": "aetherAmulet"
}
}
}
}
}

View File

@ -4,10 +4,10 @@
"profile": { "name": "test-user" },
"stats": {
"buffs": {
"str": 0,
"int": 0,
"per": 0,
"con": 0,
"str": 26,
"int": 26,
"per": 26,
"con": 26,
"stealth": 4,
"streaks": false,
"seafoam": false,
@ -24,16 +24,35 @@
"maxHealth": 50,
"maxMP": 166,
"toNextLevel": 880,
"points": 0
"points": 0,
"str": 15,
"con": 15,
"int": 15,
"per": 15
},
"preferences": {
"sleep": false,
"automaticAllocation": false,
"disableClasses": false
"disableClasses": false,
"language": "en"
},
"flags": {
"classSelected": true
},
"needsCron": false
"needsCron": false,
"items": {
"gear": {
"equipped": {
"weapon": "weapon_rogue_5",
"armor": "armor_rogue_5",
"head": "head_rogue_5",
"shield": "shield_rogue_5",
"back": "heroicAureole",
"headAccessory": "headAccessory_armoire_gogglesOfBookbinding",
"eyewear": "plagueDoctorMask",
"body": "aetherAmulet"
}
}
}
}
}

View File

@ -4,10 +4,10 @@
"profile": { "name": "test-user" },
"stats": {
"buffs": {
"str": 0,
"int": 0,
"per": 0,
"con": 0,
"str": 26,
"int": 26,
"per": 26,
"con": 26,
"stealth": 0,
"streaks": false,
"seafoam": false,
@ -24,12 +24,17 @@
"maxHealth": 50,
"maxMP": 166,
"toNextLevel": 880,
"points": 5
"points": 5,
"str": 15,
"con": 15,
"int": 15,
"per": 15
},
"preferences": {
"sleep": false,
"automaticAllocation": true,
"disableClasses": false
"disableClasses": false,
"language": "en"
},
"flags": {
"classSelected": true
@ -59,6 +64,20 @@
}
},
"needsCron": true,
"lastCron": "2024-09-21T22:01:55.586Z"
"lastCron": "2024-09-21T22:01:55.586Z",
"items": {
"gear": {
"equipped": {
"weapon": "weapon_warrior_5",
"armor": "armor_warrior_5",
"head": "head_warrior_5",
"shield": "shield_warrior_5",
"back": "heroicAureole",
"headAccessory": "headAccessory_armoire_gogglesOfBookbinding",
"eyewear": "plagueDoctorMask",
"body": "aetherAmulet"
}
}
}
}
}

View File

@ -4,10 +4,10 @@
"profile": { "name": "test-user" },
"stats": {
"buffs": {
"str": 0,
"int": 0,
"per": 0,
"con": 0,
"str": 26,
"int": 26,
"per": 26,
"con": 26,
"stealth": 0,
"streaks": false,
"seafoam": false,
@ -24,17 +24,36 @@
"maxHealth": 50,
"maxMP": 166,
"toNextLevel": 880,
"points": 5
"points": 5,
"str": 15,
"con": 15,
"int": 15,
"per": 15
},
"preferences": {
"sleep": false,
"automaticAllocation": true,
"disableClasses": false
"disableClasses": false,
"language": "en"
},
"flags": {
"classSelected": true
},
"needsCron": true,
"lastCron": "2024-09-21T22:01:55.586Z"
"lastCron": "2024-09-21T22:01:55.586Z",
"items": {
"gear": {
"equipped": {
"weapon": "weapon_warrior_5",
"armor": "armor_warrior_5",
"head": "head_warrior_5",
"shield": "shield_warrior_5",
"back": "heroicAureole",
"headAccessory": "headAccessory_armoire_gogglesOfBookbinding",
"eyewear": "plagueDoctorMask",
"body": "aetherAmulet"
}
}
}
}
}

View File

@ -4,10 +4,10 @@
"profile": { "name": "test-user" },
"stats": {
"buffs": {
"str": 0,
"int": 0,
"per": 0,
"con": 0,
"str": 26,
"int": 26,
"per": 26,
"con": 26,
"stealth": 0,
"streaks": false,
"seafoam": false,
@ -24,16 +24,35 @@
"maxHealth": 50,
"maxMP": 166,
"toNextLevel": 880,
"points": 0
"points": 0,
"str": 15,
"con": 15,
"int": 15,
"per": 15
},
"preferences": {
"sleep": false,
"automaticAllocation": false,
"disableClasses": false
"disableClasses": false,
"language": "en"
},
"flags": {
"classSelected": true
},
"needsCron": false
"needsCron": false,
"items": {
"gear": {
"equipped": {
"weapon": "weapon_warrior_5",
"armor": "armor_warrior_5",
"head": "head_warrior_5",
"shield": "shield_warrior_5",
"back": "heroicAureole",
"headAccessory": "headAccessory_armoire_gogglesOfBookbinding",
"eyewear": "plagueDoctorMask",
"body": "aetherAmulet"
}
}
}
}
}

View File

@ -4,10 +4,10 @@
"profile": { "name": "test-user" },
"stats": {
"buffs": {
"str": 0,
"int": 0,
"per": 0,
"con": 0,
"str": 26,
"int": 26,
"per": 26,
"con": 26,
"stealth": 0,
"streaks": false,
"seafoam": false,
@ -24,17 +24,36 @@
"maxHealth": 50,
"maxMP": 166,
"toNextLevel": 880,
"points": 5
"points": 5,
"str": 15,
"con": 15,
"int": 15,
"per": 15
},
"preferences": {
"sleep": false,
"automaticAllocation": true,
"disableClasses": false
"disableClasses": false,
"language": "en"
},
"flags": {
"classSelected": true
},
"needsCron": true,
"lastCron": "2024-09-21T22:01:55.586Z"
"lastCron": "2024-09-21T22:01:55.586Z",
"items": {
"gear": {
"equipped": {
"weapon": "weapon_wizard_5",
"armor": "armor_wizard_5",
"head": "head_wizard_5",
"shield": "shield_base_0",
"back": "heroicAureole",
"headAccessory": "headAccessory_armoire_gogglesOfBookbinding",
"eyewear": "plagueDoctorMask",
"body": "aetherAmulet"
}
}
}
}
}

View File

@ -4,10 +4,10 @@
"profile": { "name": "test-user" },
"stats": {
"buffs": {
"str": 0,
"int": 0,
"per": 0,
"con": 0,
"str": 26,
"int": 26,
"per": 26,
"con": 26,
"stealth": 0,
"streaks": true,
"seafoam": false,
@ -24,16 +24,35 @@
"maxHealth": 50,
"maxMP": 166,
"toNextLevel": 880,
"points": 0
"points": 0,
"str": 15,
"con": 15,
"int": 15,
"per": 15
},
"preferences": {
"sleep": false,
"automaticAllocation": false,
"disableClasses": false
"disableClasses": false,
"language": "en"
},
"flags": {
"classSelected": true
},
"needsCron": false
"needsCron": false,
"items": {
"gear": {
"equipped": {
"weapon": "weapon_wizard_5",
"armor": "armor_wizard_5",
"head": "head_wizard_5",
"shield": "shield_base_0",
"back": "heroicAureole",
"headAccessory": "headAccessory_armoire_gogglesOfBookbinding",
"eyewear": "plagueDoctorMask",
"body": "aetherAmulet"
}
}
}
}
}

View File

@ -4,10 +4,10 @@
"profile": { "name": "test-user" },
"stats": {
"buffs": {
"str": 0,
"int": 0,
"per": 0,
"con": 0,
"str": 26,
"int": 26,
"per": 26,
"con": 26,
"stealth": 0,
"streaks": false,
"seafoam": false,
@ -24,16 +24,35 @@
"maxHealth": 50,
"maxMP": 166,
"toNextLevel": 880,
"points": 0
"points": 0,
"str": 15,
"con": 15,
"int": 15,
"per": 15
},
"preferences": {
"sleep": false,
"automaticAllocation": false,
"disableClasses": false
"disableClasses": false,
"language": "en"
},
"flags": {
"classSelected": true
},
"needsCron": false
"needsCron": false,
"items": {
"gear": {
"equipped": {
"weapon": "weapon_wizard_5",
"armor": "armor_wizard_5",
"head": "head_wizard_5",
"shield": "shield_base_0",
"back": "heroicAureole",
"headAccessory": "headAccessory_armoire_gogglesOfBookbinding",
"eyewear": "plagueDoctorMask",
"body": "aetherAmulet"
}
}
}
}
}

View File

@ -59,6 +59,61 @@
'state': 'wizard',
})
# ---
# name: test_sensors[sensor.test_user_constitution-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.test_user_constitution',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Constitution',
'platform': 'habitica',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': <HabitipySensorEntity.CONSTITUTION: 'constitution'>,
'unique_id': '00000000-0000-0000-0000-000000000000_constitution',
'unit_of_measurement': 'CON',
})
# ---
# name: test_sensors[sensor.test_user_constitution-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'allocated': 15,
'buffs': 26,
'class': 0,
'equipment': 20,
'friendly_name': 'test-user Constitution',
'level': 19,
'unit_of_measurement': 'CON',
}),
'context': <ANY>,
'entity_id': 'sensor.test_user_constitution',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '80',
})
# ---
# name: test_sensors[sensor.test_user_dailies-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@ -567,6 +622,61 @@
'state': '0',
})
# ---
# name: test_sensors[sensor.test_user_intelligence-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.test_user_intelligence',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Intelligence',
'platform': 'habitica',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': <HabitipySensorEntity.INTELLIGENCE: 'intelligence'>,
'unique_id': '00000000-0000-0000-0000-000000000000_intelligence',
'unit_of_measurement': 'INT',
})
# ---
# name: test_sensors[sensor.test_user_intelligence-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'allocated': 15,
'buffs': 26,
'class': 0,
'equipment': 0,
'friendly_name': 'test-user Intelligence',
'level': 19,
'unit_of_measurement': 'INT',
}),
'context': <ANY>,
'entity_id': 'sensor.test_user_intelligence',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '60',
})
# ---
# name: test_sensors[sensor.test_user_level-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@ -854,6 +964,61 @@
'state': '880',
})
# ---
# name: test_sensors[sensor.test_user_perception-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.test_user_perception',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Perception',
'platform': 'habitica',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': <HabitipySensorEntity.PERCEPTION: 'perception'>,
'unique_id': '00000000-0000-0000-0000-000000000000_perception',
'unit_of_measurement': 'PER',
})
# ---
# name: test_sensors[sensor.test_user_perception-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'allocated': 15,
'buffs': 26,
'class': 0,
'equipment': 8,
'friendly_name': 'test-user Perception',
'level': 19,
'unit_of_measurement': 'PER',
}),
'context': <ANY>,
'entity_id': 'sensor.test_user_perception',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '68',
})
# ---
# name: test_sensors[sensor.test_user_rewards-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
@ -915,6 +1080,61 @@
'state': '1',
})
# ---
# name: test_sensors[sensor.test_user_strength-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'sensor',
'entity_category': None,
'entity_id': 'sensor.test_user_strength',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
'sensor': dict({
'suggested_display_precision': 0,
}),
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Strength',
'platform': 'habitica',
'previous_unique_id': None,
'supported_features': 0,
'translation_key': <HabitipySensorEntity.STRENGTH: 'strength'>,
'unique_id': '00000000-0000-0000-0000-000000000000_strength',
'unit_of_measurement': 'STR',
})
# ---
# name: test_sensors[sensor.test_user_strength-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'allocated': 15,
'buffs': 26,
'class': 0,
'equipment': 27,
'friendly_name': 'test-user Strength',
'level': 19,
'unit_of_measurement': 'STR',
}),
'context': <ANY>,
'entity_id': 'sensor.test_user_strength',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '87',
})
# ---
# name: test_sensors[sensor.test_user_to_do_s-entry]
EntityRegistryEntrySnapshot({
'aliases': set({

View File

@ -66,7 +66,11 @@ async def test_pending_quest_states(
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),
)
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()

View File

@ -63,6 +63,11 @@ async def test_buttons(
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),
)
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
@ -163,6 +168,11 @@ async def test_button_press(
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),
)
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()

View File

@ -672,6 +672,11 @@ async def test_next_due_date(
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),
)
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)