Clear Spoolman location when spool is removed from AMS

Previously, when syncing to Spoolman, the location field was set to the
  printer/AMS slot but was never cleared when the spool was removed from
  the AMS.

  Now during sync, the system tracks which tray UUIDs are currently in the
  AMS and clears the location for any spools that were previously at this
  printer but are no longer present.

  Changes:
  - Add clear_location parameter to update_spool() method
  - Add find_spools_by_location_prefix() to find spools at a printer
  - Add clear_location_for_removed_spools() to clear stale locations
  - Update sync_printer_ams and sync_all_printers endpoints to clear
    locations for removed spools after syncing
This commit is contained in:
maziggy
2026-01-09 07:43:40 +01:00
parent 03f473f7ab
commit b4bc046684
2 changed files with 97 additions and 2 deletions
+30
View File
@@ -169,6 +169,8 @@ async def sync_printer_ams(
synced = 0
skipped: list[SkippedSpool] = []
errors = []
# Track tray UUIDs currently in the AMS (for clearing removed spools)
current_tray_uuids: set[str] = set()
# Handle different AMS data structures
# Traditional AMS: list of {"id": N, "tray": [...]} dicts
@@ -222,6 +224,9 @@ async def sync_printer_ams(
)
continue
# Track this tray UUID as currently present in the AMS
current_tray_uuids.add(tray.tray_uuid.upper())
try:
sync_result = await client.sync_ams_tray(tray, printer.name)
if sync_result:
@@ -235,6 +240,14 @@ async def sync_printer_ams(
logger.error(error_msg)
errors.append(error_msg)
# Clear location for spools that were removed from this printer's AMS
try:
cleared = await client.clear_location_for_removed_spools(printer.name, current_tray_uuids)
if cleared > 0:
logger.info(f"Cleared location for {cleared} spools removed from {printer.name}")
except Exception as e:
logger.error(f"Error clearing locations for removed spools: {e}")
return SyncResult(
success=len(errors) == 0,
synced_count=synced,
@@ -269,6 +282,8 @@ async def sync_all_printers(db: AsyncSession = Depends(get_db)):
total_synced = 0
all_skipped: list[SkippedSpool] = []
all_errors = []
# Track tray UUIDs per printer (for clearing removed spools)
printer_tray_uuids: dict[str, set[str]] = {}
for printer in printers:
state = printer_manager.get_status(printer.id)
@@ -279,6 +294,9 @@ async def sync_all_printers(db: AsyncSession = Depends(get_db)):
if not ams_data:
continue
# Initialize tray UUID set for this printer
printer_tray_uuids[printer.name] = set()
# Handle different AMS data structures
# Traditional AMS: list of {"id": N, "tray": [...]} dicts
# H2D/newer printers: dict with different structure
@@ -330,6 +348,9 @@ async def sync_all_printers(db: AsyncSession = Depends(get_db)):
)
continue
# Track this tray UUID as currently present in the AMS
printer_tray_uuids[printer.name].add(tray.tray_uuid.upper())
try:
sync_result = await client.sync_ams_tray(tray, printer.name)
if sync_result:
@@ -337,6 +358,15 @@ async def sync_all_printers(db: AsyncSession = Depends(get_db)):
except Exception as e:
all_errors.append(f"{printer.name} AMS {ams_id}:{tray.tray_id}: {e}")
# Clear location for spools that were removed from each printer's AMS
for printer_name, current_tray_uuids in printer_tray_uuids.items():
try:
cleared = await client.clear_location_for_removed_spools(printer_name, current_tray_uuids)
if cleared > 0:
logger.info(f"Cleared location for {cleared} spools removed from {printer_name}")
except Exception as e:
logger.error(f"Error clearing locations for {printer_name}: {e}")
return SyncResult(
success=len(all_errors) == 0,
synced_count=total_synced,
+67 -2
View File
@@ -327,6 +327,7 @@ class SpoolmanClient:
spool_id: int,
remaining_weight: float | None = None,
location: str | None = None,
clear_location: bool = False,
extra: dict | None = None,
) -> dict | None:
"""Update an existing spool in Spoolman.
@@ -334,7 +335,8 @@ class SpoolmanClient:
Args:
spool_id: ID of the spool to update
remaining_weight: New remaining weight in grams
location: New location
location: New location (ignored if clear_location is True)
clear_location: If True, clears the location field
extra: Extra fields to update
Returns:
@@ -344,7 +346,9 @@ class SpoolmanClient:
data = {}
if remaining_weight is not None:
data["remaining_weight"] = remaining_weight
if location:
if clear_location:
data["location"] = None
elif location:
data["location"] = location
if extra:
data["extra"] = extra
@@ -407,6 +411,67 @@ class SpoolmanClient:
return spool
return None
async def find_spools_by_location_prefix(self, location_prefix: str) -> list[dict]:
"""Find all spools with locations starting with a given prefix.
Args:
location_prefix: The location prefix to search for (e.g., "PrinterName - ")
Returns:
List of spool dictionaries with matching locations.
"""
spools = await self.get_spools()
matching = []
for spool in spools:
location = spool.get("location", "")
if location and location.startswith(location_prefix):
matching.append(spool)
return matching
async def clear_location_for_removed_spools(
self,
printer_name: str,
current_tray_uuids: set[str],
) -> int:
"""Clear location for spools that are no longer in the AMS.
When a spool is removed from the AMS, its location should be cleared
in Spoolman. This method finds all spools with locations for this printer
and clears the location for any that are not in the current_tray_uuids set.
Args:
printer_name: The printer name used as location prefix
current_tray_uuids: Set of tray_uuids currently in the AMS
Returns:
Number of spools whose location was cleared.
"""
location_prefix = f"{printer_name} - "
spools_at_printer = await self.find_spools_by_location_prefix(location_prefix)
cleared_count = 0
for spool in spools_at_printer:
# Get the tray_uuid (stored as "tag" in extra field)
extra = spool.get("extra", {}) or {}
stored_tag = extra.get("tag", "")
if stored_tag:
# Normalize: strip quotes and uppercase
spool_uuid = stored_tag.strip('"').upper()
else:
spool_uuid = ""
# If this spool's UUID is not in the current AMS, clear its location
if spool_uuid not in current_tray_uuids:
logger.info(
f"Clearing location for spool {spool['id']} "
f"(was: {spool.get('location')}, uuid: {spool_uuid[:16] if spool_uuid else 'none'}...)"
)
result = await self.update_spool(spool_id=spool["id"], clear_location=True)
if result:
cleared_count += 1
return cleared_count
async def ensure_bambu_vendor(self) -> int | None:
"""Ensure Bambu Lab vendor exists and return its ID.