From e8d861f8a9c95e088c3ea872de145ce296b83a53 Mon Sep 17 00:00:00 2001 From: RNL Date: Wed, 19 Aug 2026 22:00:12 +1000 Subject: [PATCH] feat(mcp): add get_metrics_history tool --- bambu_run/mcp/status.py | 48 ++++++++++++++++++++++++++++++++++++ tests/test_mcp_status.py | 53 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/bambu_run/mcp/status.py b/bambu_run/mcp/status.py index 4c6caba..6f9c7b6 100644 --- a/bambu_run/mcp/status.py +++ b/bambu_run/mcp/status.py @@ -137,3 +137,51 @@ def _alerts_for_printer(printer, now, window_hours=ALERT_WINDOW_HOURS): for entry in hms_list: counts[(entry.get("attr"), entry.get("code"))] += 1 return classify_alerts(counts, total) + + +def get_metrics_history( + printer: str | int | None = None, + hours: int = 6, + metrics: list[str] | None = None, + max_points: int = 300, +) -> dict: + """Sampled time-series history for one printer: temps, humidity, progress, fans. + + metrics selects which series to include: any of "temps", "humidity", + "progress", "fans" (omit for all four). Server-side sampled to at most + max_points (hard ceiling 500) evenly across the window -- do not expect + one point per collector poll on a long window. printer defaults to the + first active printer when omitted; pass an id or name explicitly if more + than one printer is registered. + """ + from bambu_run.views import sample_metrics # reuse: same thinning the dashboard already uses + + max_points = min(max_points, 500) + target = resolve_printer(printer).first() + if target is None: + return {"printer": None, "timestamps": [], "series": {}} + + since = timezone.now() - timedelta(hours=hours) + rows = list( + PrinterMetrics.objects.filter(device=target, timestamp__gte=since).order_by("timestamp") + ) + rows = sample_metrics(rows, max_points=max_points) + + wanted = set(metrics) if metrics else {"temps", "humidity", "progress", "fans"} + series = {} + if "temps" in wanted: + series["nozzle_c"] = [float(r.nozzle_temp) if r.nozzle_temp is not None else None for r in rows] + series["bed_c"] = [float(r.bed_temp) if r.bed_temp is not None else None for r in rows] + series["chamber_c"] = [float(r.chamber_temp) if r.chamber_temp is not None else None for r in rows] + if "humidity" in wanted: + series["ams_humidity_raw"] = [r.ams_humidity_raw for r in rows] + if "progress" in wanted: + series["print_percent"] = [r.print_percent for r in rows] + if "fans" in wanted: + series["cooling_fan_percent"] = [r.cooling_fan_speed for r in rows] + + return { + "printer": {"id": target.id, "name": target.name}, + "timestamps": [r.timestamp.isoformat() for r in rows], + "series": series, + } diff --git a/tests/test_mcp_status.py b/tests/test_mcp_status.py index 0c6e191..a9bf160 100644 --- a/tests/test_mcp_status.py +++ b/tests/test_mcp_status.py @@ -129,3 +129,56 @@ def test_get_printer_status_alerts_use_frequency_heuristic_not_hms_emptiness(): result = get_printer_status(printer=p.id) assert result["printers"][0]["alerts"] == [] + + +from bambu_run.mcp.status import get_metrics_history + + +@pytest.mark.django_db +def test_get_metrics_history_returns_series_within_window(): + p = Printer.objects.create(name="RNL-H2C", model="H2C", is_active=True) + now = timezone.now() + for i in range(3): + PrinterMetrics.objects.create( + device=p, timestamp=now - timedelta(hours=i), + nozzle_temp=200 + i, bed_temp=60, chamber_temp=35, + print_percent=50 + i, ams_humidity_raw=5, cooling_fan_speed=100, + ) + PrinterMetrics.objects.create(device=p, timestamp=now - timedelta(hours=10), nozzle_temp=999) + + result = get_metrics_history(printer=p.id, hours=6) + + assert result["printer"]["id"] == p.id + assert len(result["timestamps"]) == 3 + assert 999.0 not in result["series"]["nozzle_c"] + + +@pytest.mark.django_db +def test_get_metrics_history_filters_to_requested_metrics(): + p = Printer.objects.create(name="RNL-H2C", model="H2C", is_active=True) + PrinterMetrics.objects.create(device=p, timestamp=timezone.now(), nozzle_temp=200, print_percent=10) + + result = get_metrics_history(printer=p.id, metrics=["progress"]) + + assert "print_percent" in result["series"] + assert "nozzle_c" not in result["series"] + + +@pytest.mark.django_db +def test_get_metrics_history_hard_ceiling_500_points(): + p = Printer.objects.create(name="RNL-H2C", model="H2C", is_active=True) + now = timezone.now() + PrinterMetrics.objects.bulk_create([ + PrinterMetrics(device=p, timestamp=now - timedelta(minutes=i)) + for i in range(600) + ]) + + result = get_metrics_history(printer=p.id, hours=24, max_points=10000) + + assert len(result["timestamps"]) <= 500 + + +@pytest.mark.django_db +def test_get_metrics_history_no_matching_printer_returns_empty_shape(): + result = get_metrics_history(printer=None) + assert result == {"printer": None, "timestamps": [], "series": {}}