mirror of
https://github.com/RunLit/Bambu-Run.git
synced 2026-10-21 15:37:38 +01:00
feat(mcp): add list_filaments tool
This commit is contained in:
82
bambu_run/mcp/filaments.py
Normal file
82
bambu_run/mcp/filaments.py
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
"""list_filaments, get_filament -- the read_filament capability tools."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from bambu_run.mcp.shapes import normalize_consumed, remaining_status
|
||||||
|
from bambu_run.models import Filament
|
||||||
|
|
||||||
|
|
||||||
|
def list_filaments(
|
||||||
|
material: str | None = None,
|
||||||
|
sub_type: str | None = None,
|
||||||
|
brand: str | None = None,
|
||||||
|
color: str | None = None,
|
||||||
|
loaded_only: bool = False,
|
||||||
|
min_remaining_percent: int | None = None,
|
||||||
|
min_remaining_grams: int | None = None,
|
||||||
|
include_history: bool = False,
|
||||||
|
limit: int = 50,
|
||||||
|
) -> dict:
|
||||||
|
"""Filament spool inventory with composable filters.
|
||||||
|
|
||||||
|
"PLA Matte Black" is material="PLA", sub_type="PLA Matte", color="black".
|
||||||
|
"250g of black PLA" is material="PLA", color="black",
|
||||||
|
min_remaining_grams=250. remaining_percent/remaining_grams are null when
|
||||||
|
the spool's RFID has never been read (remaining_status is "unread" in
|
||||||
|
that case, not "low" or "empty"). include_history (default off) adds
|
||||||
|
each spool's last 3 uses inline; call get_filament for the complete
|
||||||
|
history of one spool. limit hard ceiling is 100.
|
||||||
|
"""
|
||||||
|
limit = min(limit, 100)
|
||||||
|
qs = Filament.objects.all()
|
||||||
|
if material:
|
||||||
|
qs = qs.filter(type__iexact=material)
|
||||||
|
if sub_type:
|
||||||
|
qs = qs.filter(sub_type__iexact=sub_type)
|
||||||
|
if brand:
|
||||||
|
qs = qs.filter(brand__icontains=brand)
|
||||||
|
if color:
|
||||||
|
qs = qs.filter(color__icontains=color)
|
||||||
|
if loaded_only:
|
||||||
|
qs = qs.filter(is_loaded_in_ams=True)
|
||||||
|
if min_remaining_percent is not None:
|
||||||
|
qs = qs.exclude(remaining_percent=-1).filter(remaining_percent__gte=min_remaining_percent)
|
||||||
|
if min_remaining_grams is not None:
|
||||||
|
qs = qs.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}
|
||||||
|
|
||||||
|
|
||||||
|
def _spool_summary(f, include_history):
|
||||||
|
value, status = remaining_status(f.remaining_percent)
|
||||||
|
grams = f.remaining_weight_grams
|
||||||
|
entry = {
|
||||||
|
"id": f.id,
|
||||||
|
"material": f.type,
|
||||||
|
"sub_type": f.sub_type,
|
||||||
|
"brand": f.brand,
|
||||||
|
"color_name": f.color,
|
||||||
|
"color_hex": f.color_hex,
|
||||||
|
"is_transparent": f.is_transparent,
|
||||||
|
"remaining_percent": value,
|
||||||
|
"remaining_status": status,
|
||||||
|
"remaining_grams": grams if (grams is not None and grams >= 0) else None,
|
||||||
|
"ams_location": (
|
||||||
|
{"unit_id": f.ams_unit_id, "unit_type": f.ams_type, "slot": f.current_tray_id}
|
||||||
|
if f.is_loaded_in_ams else None
|
||||||
|
),
|
||||||
|
"last_used": f.last_used.isoformat() if f.last_used else None,
|
||||||
|
}
|
||||||
|
if include_history:
|
||||||
|
entry["recent_usage"] = [
|
||||||
|
{
|
||||||
|
"job_id": u.print_job_id,
|
||||||
|
"job_name": u.print_job.project_name,
|
||||||
|
"started_at": u.print_job.start_time.isoformat(),
|
||||||
|
"consumed_grams": normalize_consumed(
|
||||||
|
u.starting_percent, u.ending_percent, f.initial_weight_grams
|
||||||
|
)[1],
|
||||||
|
}
|
||||||
|
for u in f.print_usages.select_related("print_job").order_by("-print_job__start_time")[:3]
|
||||||
|
]
|
||||||
|
return entry
|
||||||
79
tests/test_mcp_filaments.py
Normal file
79
tests/test_mcp_filaments.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import pytest
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from bambu_run.models import Filament, Printer, PrintJob, FilamentUsage
|
||||||
|
from bambu_run.mcp.filaments import list_filaments
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_list_filaments_filters_material_sub_type_color():
|
||||||
|
Filament.objects.create(type="PLA", sub_type="PLA Matte", brand="Bambu Lab", color="Black", remaining_percent=50)
|
||||||
|
Filament.objects.create(type="PLA", sub_type="PLA Basic", brand="Bambu Lab", color="Black", remaining_percent=50)
|
||||||
|
Filament.objects.create(type="ABS", sub_type="ABS", brand="Bambu Lab", color="Black", remaining_percent=50)
|
||||||
|
|
||||||
|
result = list_filaments(material="PLA", sub_type="PLA Matte", color="black")
|
||||||
|
|
||||||
|
assert len(result["filaments"]) == 1
|
||||||
|
assert result["filaments"][0]["sub_type"] == "PLA Matte"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_list_filaments_min_remaining_grams_excludes_unread_sentinel():
|
||||||
|
Filament.objects.create(type="ABS", brand="Bambu Lab", color="Mint", remaining_percent=-1, remaining_weight_grams=-10)
|
||||||
|
Filament.objects.create(type="ABS", brand="Bambu Lab", color="Orange", remaining_percent=68, remaining_weight_grams=680)
|
||||||
|
|
||||||
|
result = list_filaments(material="ABS", min_remaining_grams=250)
|
||||||
|
|
||||||
|
assert len(result["filaments"]) == 1
|
||||||
|
assert result["filaments"][0]["color_name"] == "Orange"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_list_filaments_unread_sentinel_status_not_low():
|
||||||
|
Filament.objects.create(type="ABS", brand="Bambu Lab", color="Mint", remaining_percent=-1)
|
||||||
|
|
||||||
|
result = list_filaments(material="ABS")
|
||||||
|
|
||||||
|
spool = result["filaments"][0]
|
||||||
|
assert spool["remaining_percent"] is None
|
||||||
|
assert spool["remaining_status"] == "unread"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_list_filaments_limit_hard_ceiling_100():
|
||||||
|
Filament.objects.bulk_create([
|
||||||
|
Filament(type="PLA", brand="Bambu Lab", color=f"Color {i}", remaining_percent=50)
|
||||||
|
for i in range(150)
|
||||||
|
])
|
||||||
|
|
||||||
|
result = list_filaments(limit=1000)
|
||||||
|
|
||||||
|
assert len(result["filaments"]) == 100
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_list_filaments_include_history_adds_recent_usage():
|
||||||
|
f = Filament.objects.create(type="PLA", brand="Bambu Lab", color="White", initial_weight_grams=1000, remaining_percent=80)
|
||||||
|
p = Printer.objects.create(name="RNL-H2C", model="H2C", is_active=True)
|
||||||
|
job = PrintJob.objects.create(device=p, project_name="Vase", start_time=timezone.now(), final_status="FINISH")
|
||||||
|
FilamentUsage.objects.create(print_job=job, filament=f, tray_id=0, starting_percent=90, ending_percent=80)
|
||||||
|
|
||||||
|
result = list_filaments(material="PLA", include_history=True)
|
||||||
|
|
||||||
|
history = result["filaments"][0]["recent_usage"]
|
||||||
|
assert history[0]["job_name"] == "Vase"
|
||||||
|
assert history[0]["consumed_grams"] == 100
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_list_filaments_never_exposes_rfid_identifiers():
|
||||||
|
Filament.objects.create(
|
||||||
|
type="PLA", brand="Bambu Lab", color="White",
|
||||||
|
tray_uuid="SECRET-UUID", tag_uid="SECRET-TAG",
|
||||||
|
)
|
||||||
|
|
||||||
|
result = list_filaments(material="PLA")
|
||||||
|
|
||||||
|
entry_str = repr(result["filaments"][0])
|
||||||
|
assert "SECRET-UUID" not in entry_str
|
||||||
|
assert "SECRET-TAG" not in entry_str
|
||||||
Reference in New Issue
Block a user