Add notification for no spool assigned for active trays, improve usage tracker logic in edge cases (#789)

Add notification for no spool assigned for active trays, improve usage tracker logic in edge cases (#789)
This commit is contained in:
Keybored
2026-03-24 11:31:32 +01:00
committed by GitHub
parent dd75c8f0c9
commit 3f7d0e2d55
28 changed files with 726 additions and 70 deletions
+99 -47
View File
@@ -164,6 +164,70 @@ class PrintSession:
_active_sessions: dict[int, PrintSession] = {}
def _to_epoch_seconds(value: datetime | None) -> float | None:
"""Convert datetime to epoch seconds, assuming UTC for naive values."""
if value is None:
return None
dt = value
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt.timestamp()
async def _resolve_spool_id_for_tray(
printer_id: int,
ams_id: int,
tray_id: int,
db: AsyncSession,
spool_assignments_snapshot: dict[tuple[int, int], int] | None = None,
print_started_at: datetime | None = None,
) -> int | None:
"""Resolve spool ID for a tray with safe support for mid-print reassignment.
Resolution order:
1. If snapshot exists and live assignment changed *during this print*, use live spool.
2. Otherwise use snapshot spool when available.
3. Fall back to live assignment.
"""
key = (ams_id, tray_id)
snapshot_spool_id = spool_assignments_snapshot.get(key) if spool_assignments_snapshot else None
# Backward-compatible fast path: if we have a snapshot but no print-start
# timestamp, preserve legacy behavior and avoid extra DB lookups.
if snapshot_spool_id is not None and print_started_at is None:
return snapshot_spool_id
result = await db.execute(
select(SpoolAssignment).where(
SpoolAssignment.printer_id == printer_id,
SpoolAssignment.ams_id == ams_id,
SpoolAssignment.tray_id == tray_id,
)
)
live_assignment = result.scalar_one_or_none()
if snapshot_spool_id is not None:
if live_assignment and live_assignment.spool_id != snapshot_spool_id:
live_created_ts = _to_epoch_seconds(getattr(live_assignment, "created_at", None))
started_ts = _to_epoch_seconds(print_started_at)
if live_created_ts is not None and started_ts is not None and live_created_ts >= started_ts:
logger.info(
"[UsageTracker] Assignment changed during print for printer %d AMS%d-T%d: snapshot spool %d -> live spool %d",
printer_id,
ams_id,
tray_id,
snapshot_spool_id,
live_assignment.spool_id,
)
return live_assignment.spool_id
return snapshot_spool_id
if live_assignment:
return live_assignment.spool_id
return None
async def on_print_start(printer_id: int, data: dict, printer_manager, db: AsyncSession | None = None) -> None:
"""Capture AMS tray remain% and spool assignments at print start."""
state = printer_manager.get_status(printer_id)
@@ -323,6 +387,7 @@ async def on_print_complete(
last_layer_num=data.get("last_layer_num", 0),
default_filament_cost=default_filament_cost,
spool_assignments=session.spool_assignments if session else None,
print_started_at=session.started_at if session else None,
)
results.extend(threemf_results)
@@ -357,20 +422,16 @@ async def on_print_complete(
if delta_pct <= 0:
continue # No consumption or tray was refilled
# Look up spool: prefer snapshot (survives mid-print unlink), fall back to live query
spool_id = session.spool_assignments.get(key) if session.spool_assignments else None
spool_id = await _resolve_spool_id_for_tray(
printer_id=printer_id,
ams_id=ams_id,
tray_id=tray_id,
db=db,
spool_assignments_snapshot=session.spool_assignments,
print_started_at=session.started_at,
)
if spool_id is None:
result = await db.execute(
select(SpoolAssignment).where(
SpoolAssignment.printer_id == printer_id,
SpoolAssignment.ams_id == ams_id,
SpoolAssignment.tray_id == tray_id,
)
)
assignment = result.scalar_one_or_none()
if not assignment:
continue
spool_id = assignment.spool_id
continue
# Load spool
spool_result = await db.execute(select(Spool).where(Spool.id == spool_id))
@@ -463,6 +524,7 @@ async def _track_from_3mf(
last_layer_num: int = 0,
default_filament_cost: float = 0.0,
spool_assignments: dict[tuple[int, int], int] | None = None,
print_started_at: datetime | None = None,
) -> list[dict]:
"""Track usage from 3MF per-filament slicer data (primary path).
@@ -726,26 +788,22 @@ async def _track_from_3mf(
segment_grams,
)
# Find spool for this tray
seg_spool_id = spool_assignments.get(seg_key) if spool_assignments else None
seg_spool_id = await _resolve_spool_id_for_tray(
printer_id=printer_id,
ams_id=seg_ams_id,
tray_id=seg_tray_id,
db=db,
spool_assignments_snapshot=spool_assignments,
print_started_at=print_started_at,
)
if seg_spool_id is None:
assign_result = await db.execute(
select(SpoolAssignment).where(
SpoolAssignment.printer_id == printer_id,
SpoolAssignment.ams_id == seg_ams_id,
SpoolAssignment.tray_id == seg_tray_id,
)
logger.info(
"[UsageTracker] 3MF split: no spool at printer %d AMS%d-T%d, skipping segment",
printer_id,
seg_ams_id,
seg_tray_id,
)
assignment = assign_result.scalar_one_or_none()
if not assignment:
logger.info(
"[UsageTracker] 3MF split: no spool at printer %d AMS%d-T%d, skipping segment",
printer_id,
seg_ams_id,
seg_tray_id,
)
continue
seg_spool_id = assignment.spool_id
continue
spool_result = await db.execute(select(Spool).where(Spool.id == seg_spool_id))
spool = spool_result.scalar_one_or_none()
@@ -851,23 +909,17 @@ async def _track_from_3mf(
if key in handled_trays:
continue
# Find spool: prefer snapshot (survives mid-print unlink), fall back to live query
spool_id = spool_assignments.get(key) if spool_assignments else None
spool_id = await _resolve_spool_id_for_tray(
printer_id=printer_id,
ams_id=ams_id,
tray_id=tray_id,
db=db,
spool_assignments_snapshot=spool_assignments,
print_started_at=print_started_at,
)
if spool_id is None:
assign_result = await db.execute(
select(SpoolAssignment).where(
SpoolAssignment.printer_id == printer_id,
SpoolAssignment.ams_id == ams_id,
SpoolAssignment.tray_id == tray_id,
)
)
assignment = assign_result.scalar_one_or_none()
if not assignment:
logger.info(
"[UsageTracker] 3MF: no spool assignment at printer %d AMS%d-T%d", printer_id, ams_id, tray_id
)
continue
spool_id = assignment.spool_id
logger.info("[UsageTracker] 3MF: no spool assignment at printer %d AMS%d-T%d", printer_id, ams_id, tray_id)
continue
# Load spool
spool_result = await db.execute(select(Spool).where(Spool.id == spool_id))