mirror of
https://github.com/RunLit/Bambu-Run.git
synced 2026-10-21 15:37:38 +01:00
fix(mcp): address final-review findings (sentinel exclusion, per-unit AMS data, generated_at, gating test, docs, cleanup)
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
"""list_filaments, get_filament -- the read_filament capability tools."""
|
||||
from __future__ import annotations
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
from bambu_run.mcp.shapes import normalize_consumed, remaining_status
|
||||
from bambu_run.models import Filament
|
||||
|
||||
@@ -44,7 +46,7 @@ def list_filaments(
|
||||
qs = qs.exclude(remaining_percent=-1).exclude(remaining_weight_grams__isnull=True).filter(remaining_weight_grams__gte=min_remaining_grams)
|
||||
|
||||
spools = [_spool_summary(f, include_history) for f in qs.order_by("-remaining_percent")[:limit]]
|
||||
return {"filaments": spools}
|
||||
return {"generated_at": timezone.now().isoformat(), "filaments": spools}
|
||||
|
||||
|
||||
def _spool_summary(f, include_history):
|
||||
@@ -110,6 +112,7 @@ def get_filament(filament_id: int) -> dict:
|
||||
|
||||
return {
|
||||
**summary,
|
||||
"generated_at": timezone.now().isoformat(),
|
||||
"purchase_date": f.purchase_date.isoformat() if f.purchase_date else None,
|
||||
"usage_history": usages,
|
||||
}
|
||||
|
||||
@@ -31,10 +31,11 @@ def list_print_jobs(
|
||||
returned by one call; narrow days or increase limit deliberately.
|
||||
"""
|
||||
limit = min(limit, 100)
|
||||
now = timezone.now()
|
||||
qs = PrintJob.objects.select_related("cloud_task", "device")
|
||||
if printer is not None:
|
||||
qs = qs.filter(device__in=resolve_printer(printer))
|
||||
since = timezone.now() - timedelta(days=days)
|
||||
since = now - timedelta(days=days)
|
||||
qs = qs.filter(start_time__gte=since)
|
||||
if status == "running":
|
||||
qs = qs.filter(final_status__isnull=True)
|
||||
@@ -44,7 +45,7 @@ def list_print_jobs(
|
||||
qs = qs.filter(project_name__icontains=name_contains)
|
||||
|
||||
jobs = list(qs.order_by("-start_time")[:limit])
|
||||
return {"jobs": [_job_summary(j) for j in jobs]}
|
||||
return {"generated_at": now.isoformat(), "jobs": [_job_summary(j) for j in jobs]}
|
||||
|
||||
|
||||
def _job_summary(job):
|
||||
@@ -79,6 +80,7 @@ def get_print_job(job_id: int) -> dict:
|
||||
per-slot filament breakdown, estimate-vs-actual delta, and linked cloud
|
||||
metadata. Raises PrintJob.DoesNotExist if job_id is unknown.
|
||||
"""
|
||||
now = timezone.now()
|
||||
job = PrintJob.objects.select_related("cloud_task", "device").get(id=job_id)
|
||||
summary = _job_summary(job)
|
||||
|
||||
@@ -109,6 +111,7 @@ def get_print_job(job_id: int) -> dict:
|
||||
cloud = job.cloud_task
|
||||
return {
|
||||
**summary,
|
||||
"generated_at": now.isoformat(),
|
||||
"estimate_delta_percent": delta_percent,
|
||||
"filament_usages": usages,
|
||||
"cloud_design_title": cloud.design_title if cloud else None,
|
||||
@@ -125,7 +128,8 @@ def get_print_statistics(days: int = 30, printer: str | int | None = None) -> di
|
||||
most_used_material answers "what did I print most" directly for the
|
||||
requested window (days=7 for last week, days=30 for last month).
|
||||
"""
|
||||
since = timezone.now() - timedelta(days=days)
|
||||
now = timezone.now()
|
||||
since = now - timedelta(days=days)
|
||||
qs = PrintJob.objects.filter(start_time__gte=since)
|
||||
if printer is not None:
|
||||
qs = qs.filter(device__in=resolve_printer(printer))
|
||||
@@ -142,7 +146,7 @@ def get_print_statistics(days: int = 30, printer: str | int | None = None) -> di
|
||||
|
||||
cloud_qs = BambuCloudTask.objects.filter(print_jobs__in=qs).distinct()
|
||||
total_grams = cloud_qs.aggregate(s=Sum("weight_grams"))["s"]
|
||||
jobs_without_cloud = total_jobs - cloud_qs.count()
|
||||
jobs_without_cloud = qs.filter(cloud_task__isnull=True).count()
|
||||
|
||||
# Same exclusion rules as normalize_consumed (starting_percent must be a
|
||||
# real reading, and consumption must be non-negative), expressed as a DB
|
||||
@@ -150,6 +154,7 @@ def get_print_statistics(days: int = 30, printer: str | int | None = None) -> di
|
||||
valid_usage = (
|
||||
Q(starting_percent__gt=F("ending_percent"))
|
||||
& ~Q(starting_percent=-1)
|
||||
& ~Q(ending_percent=-1)
|
||||
& Q(filament__initial_weight_grams__isnull=False)
|
||||
)
|
||||
material_rows = (
|
||||
@@ -186,6 +191,7 @@ def get_print_statistics(days: int = 30, printer: str | int | None = None) -> di
|
||||
median_ratio = statistics.median(ratios) if ratios else None
|
||||
|
||||
return {
|
||||
"generated_at": now.isoformat(),
|
||||
"window_days": days,
|
||||
"total_jobs": total_jobs,
|
||||
"success_rate": success_rate,
|
||||
|
||||
@@ -89,9 +89,25 @@ def _temperatures(metric):
|
||||
}
|
||||
|
||||
|
||||
def _safe_tray_id(value, default=-1):
|
||||
"""Coerce a tray_id from the filaments JSON to int, defaulting on anything
|
||||
that isn't a clean int (missing, None, or malformed) rather than crashing
|
||||
the whole tool from one bad row."""
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
|
||||
def _ams_units(printer, latest_metric):
|
||||
"""Build ams_units from the latest metric's filaments, with remain_percent
|
||||
smoothed against the printer's 5 most recent metric rows."""
|
||||
smoothed against the printer's 5 most recent metric rows.
|
||||
|
||||
Per-unit humidity/temp come from PrinterMetrics.ams_units (a JSONField with
|
||||
one entry per physical AMS unit) -- NOT from PrinterMetrics.ams_humidity_raw/
|
||||
.ams_temp, which the collector only ever populates from unit 0's reading and
|
||||
would otherwise stamp one unit's humidity onto every unit.
|
||||
"""
|
||||
recent_filament_lists = list(
|
||||
PrinterMetrics.objects.filter(device=printer)
|
||||
.order_by("-timestamp")
|
||||
@@ -100,19 +116,24 @@ def _ams_units(printer, latest_metric):
|
||||
readings = defaultdict(list)
|
||||
for filaments in recent_filament_lists:
|
||||
for f in filaments or []:
|
||||
key = (f.get("ams_unit_id"), int(f.get("tray_id", -1)))
|
||||
key = (f.get("ams_unit_id"), _safe_tray_id(f.get("tray_id")))
|
||||
readings[key].append(f.get("remain_percent"))
|
||||
|
||||
unit_readings = {
|
||||
u.get("unit_id"): u for u in (latest_metric.ams_units or [])
|
||||
}
|
||||
|
||||
units = {}
|
||||
for f in latest_metric.filaments or []:
|
||||
unit_id = f.get("ams_unit_id")
|
||||
tray_id = int(f.get("tray_id", -1))
|
||||
tray_id = _safe_tray_id(f.get("tray_id"))
|
||||
value, status = smoothed_remaining_percent(readings[(unit_id, tray_id)])
|
||||
unit_reading = unit_readings.get(unit_id, {})
|
||||
unit = units.setdefault(unit_id, {
|
||||
"unit_id": unit_id,
|
||||
"type": f.get("ams_type", ""),
|
||||
"humidity_raw": latest_metric.ams_humidity_raw,
|
||||
"temp_c": float(latest_metric.ams_temp) if latest_metric.ams_temp is not None else None,
|
||||
"humidity_raw": unit_reading.get("humidity_raw"),
|
||||
"temp_c": float(unit_reading["temp"]) if unit_reading.get("temp") is not None else None,
|
||||
"slots": [],
|
||||
})
|
||||
color = f.get("color")
|
||||
@@ -156,12 +177,13 @@ def get_metrics_history(
|
||||
"""
|
||||
from bambu_run.views import sample_metrics # reuse: same thinning the dashboard already uses
|
||||
|
||||
now = timezone.now()
|
||||
max_points = min(max_points, 500)
|
||||
target = resolve_printer(printer).first()
|
||||
if target is None:
|
||||
return {"printer": None, "timestamps": [], "series": {}}
|
||||
return {"generated_at": now.isoformat(), "printer": None, "timestamps": [], "series": {}}
|
||||
|
||||
since = timezone.now() - timedelta(hours=hours)
|
||||
since = now - timedelta(hours=hours)
|
||||
rows = list(
|
||||
PrinterMetrics.objects.filter(device=target, timestamp__gte=since).order_by("timestamp")
|
||||
)
|
||||
@@ -181,6 +203,7 @@ def get_metrics_history(
|
||||
series["cooling_fan_percent"] = [r.cooling_fan_speed for r in rows]
|
||||
|
||||
return {
|
||||
"generated_at": now.isoformat(),
|
||||
"printer": {"id": target.id, "name": target.name},
|
||||
"timestamps": [r.timestamp.isoformat() for r in rows],
|
||||
"series": series,
|
||||
|
||||
Reference in New Issue
Block a user