393 files

This commit is contained in:
Home Assistant Version Control
2026-09-27 17:16:54 +00:00
parent b05b7a897e
commit 39d97ac2db
404 changed files with 54006 additions and 11034 deletions
@@ -19,7 +19,7 @@ from ..const import (
ScheduleType,
)
from ..helpers.dates import parse_iso_date
from ..helpers.history import completed_entries
from ..helpers.history import completed_entries, finite_amount
from ..helpers.phases import current_phase
from ..helpers.schedule import Schedule, read_legacy_fields
from ..helpers.status import compute_status, effective_warning_days, is_past_schedule_time
@@ -296,26 +296,22 @@ class MaintenanceTask:
"""Sum of all costs in history."""
total = 0.0
for entry in self.history:
cost = entry.get("cost")
if cost is None:
continue
try:
total += float(cost)
except (ValueError, TypeError):
continue
cost = finite_amount(entry.get("cost")) if isinstance(entry, dict) else None
if cost is not None:
total += cost
return total
@property
def average_duration(self) -> float | None:
"""Average duration of completed maintenance in minutes."""
durations = [
entry["duration"]
for entry in self.history
if entry.get("type") == HistoryEntryType.COMPLETED and entry.get("duration") is not None
minutes
for entry in completed_entries(self.history)
if (minutes := finite_amount(entry.get("duration"))) is not None
]
if not durations:
return None
return float(sum(durations)) / len(durations)
return sum(durations) / len(durations)
@property
def last_entry(self) -> dict[str, Any] | None:
@@ -336,7 +332,14 @@ class MaintenanceTask:
a backfill must not drag that anchor backwards either. A full
timestamp on the same day sorts after the bare date, so a same-day
completion still counts as latest."""
anchors = [h.get("timestamp") or "" for h in self.history if h.get("type") in LIFECYCLE_HISTORY_TYPES]
# Only string stamps: an imported history carrying an epoch number
# made max() compare int with str and every completion of the task
# raised TypeError (bug audit 2026-09-27).
anchors = [
stamp
for h in self.history
if isinstance(h, dict) and h.get("type") in LIFECYCLE_HISTORY_TYPES and isinstance(stamp := h.get("timestamp"), str)
]
if self.last_performed:
anchors.append(self.last_performed)
return ts.isoformat() >= max(anchors, default="")
@@ -402,8 +405,10 @@ class MaintenanceTask:
if is_latest:
# Save the PLANNED grid date as the anchor before resetting — not
# next_due, which returns a postpone override and would shift the
# cadence permanently (see _planned_grid_due).
if self.interval_anchor == "planned":
# cadence permanently (see _planned_grid_due). Calendar kinds keep
# it too: it is the occurrence this completion covers (early
# completion — Schedule.next_due).
if self.interval_anchor == "planned" or self._schedule().is_calendar_kind:
grid = self._planned_grid_due()
if grid is not None:
self.last_planned_due = grid.isoformat()
@@ -500,8 +505,9 @@ class MaintenanceTask:
"""
# Save the PLANNED grid date as the anchor before resetting — not
# next_due, which returns a postpone override (same cadence-shift bug
# as complete(); see _planned_grid_due).
if self.interval_anchor == "planned":
# as complete(); see _planned_grid_due). Calendar kinds: the skipped
# occurrence (see complete()).
if self.interval_anchor == "planned" or self._schedule().is_calendar_kind:
grid = self._planned_grid_due()
if grid is not None:
self.last_planned_due = grid.isoformat()