192 lines
5.2 KiB
Python
192 lines
5.2 KiB
Python
"""Utility functions for Habitica."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import datetime
|
|
from math import floor
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from dateutil.rrule import (
|
|
DAILY,
|
|
FR,
|
|
MO,
|
|
MONTHLY,
|
|
SA,
|
|
SU,
|
|
TH,
|
|
TU,
|
|
WE,
|
|
WEEKLY,
|
|
YEARLY,
|
|
rrule,
|
|
)
|
|
|
|
from homeassistant.components.automation import automations_with_entity
|
|
from homeassistant.components.script import scripts_with_entity
|
|
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:
|
|
"""Calculate due date for dailies and yesterdailies."""
|
|
|
|
if task["everyX"] == 0 or not task.get("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
|
|
|
|
if task["isDue"] and not task["completed"]:
|
|
return to_date(last_cron)
|
|
|
|
if startdate > today:
|
|
if task["frequency"] == "daily" or (
|
|
task["frequency"] in ("monthly", "yearly") and task["daysOfMonth"]
|
|
):
|
|
return startdate
|
|
|
|
if (
|
|
task["frequency"] in ("weekly", "monthly")
|
|
and (nextdue := to_date(task["nextDue"][0]))
|
|
and startdate > nextdue
|
|
):
|
|
return to_date(task["nextDue"][1])
|
|
|
|
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
|
|
|
|
|
|
def entity_used_in(hass: HomeAssistant, entity_id: str) -> list[str]:
|
|
"""Get list of related automations and scripts."""
|
|
used_in = automations_with_entity(hass, entity_id)
|
|
used_in += scripts_with_entity(hass, entity_id)
|
|
return used_in
|
|
|
|
|
|
FREQUENCY_MAP = {"daily": DAILY, "weekly": WEEKLY, "monthly": MONTHLY, "yearly": 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:
|
|
"""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
|
|
]
|
|
bymonthday = (
|
|
task["daysOfMonth"]
|
|
if rrule_frequency == MONTHLY and task["daysOfMonth"]
|
|
else None
|
|
)
|
|
|
|
bysetpos = None
|
|
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"])
|
|
),
|
|
byweekday=weekdays if rrule_frequency in [WEEKLY, MONTHLY] else None,
|
|
bymonthday=bymonthday,
|
|
bysetpos=bysetpos,
|
|
)
|
|
|
|
|
|
def get_recurrence_rule(recurrence: rrule) -> str:
|
|
r"""Extract and return the recurrence rule portion of an RRULE.
|
|
|
|
This function takes an RRULE representing a task's recurrence pattern,
|
|
builds the RRULE string, and extracts the recurrence rule part.
|
|
|
|
'DTSTART:YYYYMMDDTHHMMSS\nRRULE:FREQ=YEARLY;INTERVAL=2'
|
|
|
|
Parameters
|
|
----------
|
|
recurrence : rrule
|
|
An RRULE object.
|
|
|
|
Returns
|
|
-------
|
|
str
|
|
The recurrence rule portion of the RRULE string, starting with 'FREQ='.
|
|
|
|
Example
|
|
-------
|
|
>>> rule = get_recurrence_rule(task)
|
|
>>> print(rule)
|
|
'FREQ=YEARLY;INTERVAL=2'
|
|
|
|
"""
|
|
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(floor(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())
|
|
)
|