This commit is contained in:
Home Assistant Version Control
2026-09-25 04:10:30 +00:00
parent 56c80f896e
commit ad634c1b84
60 changed files with 2895 additions and 794 deletions
+37
View File
@@ -54,6 +54,24 @@ def parse_datetime(value: str | datetime | None) -> datetime | None:
return None
def parse_optional_points(value: Any) -> int | None:
"""Parse a stored points figure, or None for "not recorded".
Used for fields that snapshot a value at the moment it applied. ``None``
marks a record written before the field existed, so callers can fall back
to recalculating; anything unusable is treated the same way rather than
being trusted as a number.
"""
if value is None or isinstance(value, bool):
return None
try:
points = int(value)
except (ValueError, TypeError, OverflowError):
_LOGGER.warning("Ignoring unusable stored points value %r", value)
return None
return None if points < 0 else points
def format_datetime(dt: datetime | None) -> str | None:
"""Format a datetime as ISO string with timezone info.
@@ -260,6 +278,12 @@ class Chore:
time_category: str = "anytime" # morning, afternoon, evening, night, anytime
claim_allowance_minutes: int = 0 # Grace minutes past period end during which the chore stays claimable; 0 = no grace. Night chores still cap at midnight.
daily_limit: int = 1
# Weekly target (#883): "do this N times this week, on whichever days you
# like" — instrument practice, reading, exercise. 0 = off. Monday-anchored
# to match Challenges. It caps the week the way daily_limit caps the day,
# and counts pending completions too, so a slow approval never hands the
# child a spare go.
weekly_target: int = 0
completion_sound: str = "coin" # Sound to play on completion
# Optional picture for the chore. Text-free pre-reader mode (#683) needs
# one per chore; everything else falls back to the time-of-day icon.
@@ -375,6 +399,7 @@ class Chore:
time_category=data.get("time_category", "anytime"),
claim_allowance_minutes=max(0, int(data.get("claim_allowance_minutes", 0) or 0)),
daily_limit=data.get("daily_limit", 1),
weekly_target=int(data.get("weekly_target", 0) or 0),
completion_sound=data.get("completion_sound", "coin"),
icon=str(data.get("icon", "") or ""),
image_url=str(data.get("image_url", "") or ""),
@@ -442,6 +467,7 @@ class Chore:
"time_category": self.time_category,
"claim_allowance_minutes": self.claim_allowance_minutes,
"daily_limit": self.daily_limit,
"weekly_target": self.weekly_target,
"completion_sound": self.completion_sound,
"icon": self.icon,
"image_url": self.image_url,
@@ -721,6 +747,10 @@ class ChoreCompletion:
note: str = "" # child's own description of the work (open-ended chores, #832)
suggested_points: int = 0 # what the child reckons it was worth (#832)
id: str = field(default_factory=generate_id)
# The award this submission was worth when it was made, before the streak
# and weekend multipliers that _award_points adds on top. None on records
# written before the field existed.
submitted_points: int | None = None
@classmethod
def from_dict(cls, data: dict[str, Any]) -> ChoreCompletion:
@@ -741,6 +771,7 @@ class ChoreCompletion:
note=data.get("note", ""),
suggested_points=int(data.get("suggested_points", 0) or 0),
id=data.get("id") or generate_id(),
submitted_points=parse_optional_points(data.get("submitted_points")),
)
def to_dict(self) -> dict[str, Any]:
@@ -758,6 +789,7 @@ class ChoreCompletion:
"note": self.note,
"suggested_points": self.suggested_points,
"id": self.id,
"submitted_points": self.submitted_points,
}
@@ -820,6 +852,9 @@ class RewardClaim:
approved: bool = False
approved_at: datetime | None = None
id: str = field(default_factory=generate_id)
# What this purchase actually cost, recorded when it was approved. None on
# a pending claim, and on claims approved before the field existed.
approved_cost: int | None = None
@classmethod
def from_dict(cls, data: dict[str, Any]) -> RewardClaim:
@@ -834,6 +869,7 @@ class RewardClaim:
approved=data.get("approved", False),
approved_at=approved_at,
id=data.get("id") or generate_id(),
approved_cost=parse_optional_points(data.get("approved_cost")),
)
def to_dict(self) -> dict[str, Any]:
@@ -845,6 +881,7 @@ class RewardClaim:
"approved": self.approved,
"approved_at": format_datetime(self.approved_at),
"id": self.id,
"approved_cost": self.approved_cost,
}