185 files

This commit is contained in:
Home Assistant Version Control
2026-08-19 11:38:13 +00:00
parent 5f9330abd7
commit a718af6423
185 changed files with 16868 additions and 3088 deletions
+60 -43
View File
@@ -389,6 +389,15 @@ class ChoresMixin:
approved. If None/empty, every currently-unapproved completion is approved.
Reuses async_approve_chore so all award/badge/quest/celebration side-effects
match single approval exactly.
Each approval is told not to refresh (#794). ``async_approve_chore``
normally ends in a full coordinator rebuild plus a state write for every
TaskMate entity, which at ~0.13s a time made a 60-chore Approve All block
the websocket call for ~8s with nothing pushed to the frontend meanwhile —
the panel and the approvals card sit unchanged for that whole window, so
the button reads as broken. The batch refreshes once at the end instead.
Safe because the per-approval side-effects (badges, quests, challenges,
the all-done check) all read `storage` directly, never `self.data`.
"""
pending = {c.id for c in self.storage.get_completions() if not c.approved}
if completion_ids:
@@ -398,8 +407,10 @@ class ChoresMixin:
targets = [c.id for c in self.storage.get_completions() if not c.approved]
count = 0
for cid in targets:
await self.async_approve_chore(cid)
await self.async_approve_chore(cid, refresh=False)
count += 1
if count:
await self.async_refresh()
return count
async def async_add_chores_bulk(
@@ -716,32 +727,37 @@ class ChoresMixin:
# assignee. Parents (as_parent) may complete on behalf of any pool
# member — e.g. ticking it off for the off-rotation child. first_come
# keeps its competitive semantics (every pool member may race).
if not as_parent and assignment_mode != "first_come":
if child_id not in self._compute_active_children(chore):
_LOGGER.debug(
"complete_chore no-op: '%s' not assigned to %s today",
chore.name,
child.name,
)
return None
if (
not as_parent
and assignment_mode != "first_come"
and child_id not in self._compute_active_children(chore)
):
_LOGGER.debug(
"complete_chore no-op: '%s' not assigned to %s today",
chore.name,
child.name,
)
return None
# Check recurrence window for Mode B chores
if getattr(chore, "schedule_mode", "specific_days") == "recurring":
if not self.is_chore_available_for_child(chore, child_id):
_LOGGER.debug(
"complete_chore no-op: '%s' not available yet (recurrence window)",
chore.name,
)
return None
if getattr(chore, "schedule_mode", "specific_days") == "recurring" and not self.is_chore_available_for_child(
chore, child_id
):
_LOGGER.debug(
"complete_chore no-op: '%s' not available yet (recurrence window)",
chore.name,
)
return None
# Check availability for one-shot chores
if getattr(chore, "schedule_mode", "specific_days") == "one_shot":
if not self.is_chore_available_for_child(chore, child_id):
_LOGGER.debug(
"complete_chore no-op: '%s' not available (one-shot done or expired)",
chore.name,
)
return None
if getattr(chore, "schedule_mode", "specific_days") == "one_shot" and not self.is_chore_available_for_child(
chore, child_id
):
_LOGGER.debug(
"complete_chore no-op: '%s' not available (one-shot done or expired)",
chore.name,
)
return None
# Check daily limit (only count parent completions, not bonus sub-tasks)
all_completions = self.storage.get_completions()
@@ -876,10 +892,7 @@ class ChoresMixin:
# Determine child pool — empty assigned_to means all children
assigned = getattr(chore, "assigned_to", []) or []
if assigned:
child_ids = list(assigned)
else:
child_ids = [c.id for c in self.storage.get_children()]
child_ids = list(assigned) if assigned else [c.id for c in self.storage.get_children()]
if not child_ids:
raise ValueError(f"Chore '{chore.name}' has no children to suppress")
@@ -903,14 +916,15 @@ class ChoresMixin:
# Clear today's cached current assignee for rotation-mode chores so
# the "Current" column / child-stats card stops pointing at the
# original child. The pointer recomputes at the next midnight refresh.
if getattr(chore, "assignment_mode", "everyone") != "everyone":
if getattr(chore, "assignment_current_child_id", "") or getattr(chore, "assignment_swap_date", ""):
chore.assignment_current_child_id = ""
# Drop the swap override too, or it would re-point the column
# at the swapped-to child on the next assignment pass.
chore.assignment_swap_child_id = ""
chore.assignment_swap_date = ""
self.storage.update_chore(chore)
if getattr(chore, "assignment_mode", "everyone") != "everyone" and (
getattr(chore, "assignment_current_child_id", "") or getattr(chore, "assignment_swap_date", "")
):
chore.assignment_current_child_id = ""
# Drop the swap override too, or it would re-point the column
# at the swapped-to child on the next assignment pass.
chore.assignment_swap_child_id = ""
chore.assignment_swap_date = ""
self.storage.update_chore(chore)
await self.storage.async_save()
await self.async_refresh()
@@ -988,8 +1002,13 @@ class ChoresMixin:
await self.async_refresh()
return completion
async def async_approve_chore(self, completion_id: str) -> None:
"""Approve a chore completion."""
async def async_approve_chore(self, completion_id: str, refresh: bool = True) -> None:
"""Approve a chore completion.
``refresh=False`` is used by ``async_approve_chores_bulk`` so a batch
pays for one coordinator rebuild rather than one per completion (#794).
The caller must refresh once it is done.
"""
completions = self.storage.get_completions()
for completion in completions:
if completion.id == completion_id:
@@ -1055,7 +1074,8 @@ class ChoresMixin:
self.storage.update_chore(chore)
await self.storage.async_save()
await self.async_refresh()
if refresh:
await self.async_refresh()
# Trigger badge evaluation after approval awards points/chore count/streak
if getattr(self, "badges", None):
@@ -1290,11 +1310,8 @@ class ChoresMixin:
def _check_one_shot_fully_disabled(self, chore) -> None:
"""Check if a one-shot chore should be fully disabled (all children done)."""
if chore.assigned_to:
target_children = set(chore.assigned_to)
else:
# assigned_to=[] means all children
target_children = {c.id for c in self.storage.get_children()}
# assigned_to=[] means all children
target_children = set(chore.assigned_to) or {c.id for c in self.storage.get_children()}
if target_children and target_children.issubset(set(chore.disabled_for)):
chore.enabled = False