Browse Source

Merge branch '0.2.0b' into fixes/327

MartinNYHC 3 months ago
parent
commit
7c36283665
3 changed files with 92 additions and 28 deletions
  1. 36 0
      backend/app/api/routes/spoolman.py
  2. 22 0
      backend/app/main.py
  3. 34 28
      backend/app/services/spoolman.py

+ 36 - 0
backend/app/api/routes/spoolman.py

@@ -6,12 +6,14 @@ from fastapi import APIRouter, Depends, HTTPException
 from pydantic import BaseModel
 from sqlalchemy import select
 from sqlalchemy.ext.asyncio import AsyncSession
+from sqlalchemy.orm import selectinload
 
 from backend.app.core.auth import RequirePermissionIfAuthEnabled
 from backend.app.core.database import get_db
 from backend.app.core.permissions import Permission
 from backend.app.models.printer import Printer
 from backend.app.models.settings import Settings
+from backend.app.models.spool_assignment import SpoolAssignment
 from backend.app.models.user import User
 from backend.app.services.printer_manager import printer_manager
 from backend.app.services.spoolman import (
@@ -230,6 +232,22 @@ async def sync_printer_ams(
             detail=f"Failed to connect to Spoolman after multiple retries: {str(e)}",
         )
 
+    # Load inventory weights as fallback (when AMS MQTT data lacks remain values)
+    inv_weights: dict[tuple[int, int], float] = {}
+    try:
+        assign_result = await db.execute(
+            select(SpoolAssignment)
+            .options(selectinload(SpoolAssignment.spool))
+            .where(SpoolAssignment.printer_id == printer_id)
+        )
+        for assignment in assign_result.scalars().all():
+            spool = assignment.spool
+            if spool and spool.label_weight > 0:
+                remaining = max(0.0, spool.label_weight - (spool.weight_used or 0))
+                inv_weights[(assignment.ams_id, assignment.tray_id)] = remaining
+    except Exception as e:
+        logger.debug("Could not load inventory weights for printer %s: %s", printer_id, e)
+
     for ams_unit in ams_units:
         if not isinstance(ams_unit, dict):
             continue
@@ -270,11 +288,13 @@ async def sync_printer_ams(
                 current_tray_uuids.add(spool_tag.upper())
 
             try:
+                inv_remaining = inv_weights.get((ams_id, tray.tray_id))
                 sync_result = await client.sync_ams_tray(
                     tray,
                     printer.name,
                     disable_weight_sync=disable_weight_sync,
                     cached_spools=cached_spools,
+                    inventory_remaining=inv_remaining,
                 )
                 if sync_result:
                     synced += 1
@@ -361,6 +381,19 @@ async def sync_all_printers(
             detail=f"Failed to connect to Spoolman after multiple retries: {str(e)}",
         )
 
+    # Load inventory assignments for weight fallback (when AMS MQTT data lacks remain values)
+    # Key: (printer_id, ams_id, tray_id) → remaining_weight in grams
+    inventory_weights: dict[tuple[int, int, int], float] = {}
+    try:
+        assign_result = await db.execute(select(SpoolAssignment).options(selectinload(SpoolAssignment.spool)))
+        for assignment in assign_result.scalars().all():
+            spool = assignment.spool
+            if spool and spool.label_weight > 0:
+                remaining = max(0.0, spool.label_weight - (spool.weight_used or 0))
+                inventory_weights[(assignment.printer_id, assignment.ams_id, assignment.tray_id)] = remaining
+    except Exception as e:
+        logger.debug("Could not load inventory assignments for weight fallback: %s", e)
+
     for printer in printers:
         state = printer_manager.get_status(printer.id)
         if not state or not state.raw_data:
@@ -435,11 +468,14 @@ async def sync_all_printers(
                     printer_tray_uuids[printer.name].add(spool_tag.upper())
 
                 try:
+                    # Look up inventory weight as fallback when AMS data is invalid
+                    inv_remaining = inventory_weights.get((printer.id, ams_id, tray.tray_id))
                     sync_result = await client.sync_ams_tray(
                         tray,
                         printer.name,
                         disable_weight_sync=disable_weight_sync,
                         cached_spools=cached_spools,
+                        inventory_remaining=inv_remaining,
                     )
                     if sync_result:
                         total_synced += 1

+ 22 - 0
backend/app/main.py

@@ -772,6 +772,26 @@ async def on_ams_change(printer_id: int, ams_data: list):
                 )
                 return
 
+            # Load inventory weights as fallback (when AMS MQTT data lacks remain values)
+            from sqlalchemy.orm import selectinload
+
+            from backend.app.models.spool_assignment import SpoolAssignment
+
+            inventory_weights: dict[tuple[int, int], float] = {}
+            try:
+                assign_result = await db.execute(
+                    select(SpoolAssignment)
+                    .options(selectinload(SpoolAssignment.spool))
+                    .where(SpoolAssignment.printer_id == printer_id)
+                )
+                for assignment in assign_result.scalars().all():
+                    spool = assignment.spool
+                    if spool and spool.label_weight > 0:
+                        remaining = max(0.0, spool.label_weight - (spool.weight_used or 0))
+                        inventory_weights[(assignment.ams_id, assignment.tray_id)] = remaining
+            except Exception as e:
+                logger.debug("Could not load inventory weights for printer %s: %s", printer_id, e)
+
             # Sync each AMS tray
             synced = 0
             for ams_unit in ams_data:
@@ -784,11 +804,13 @@ async def on_ams_change(printer_id: int, ams_data: list):
                         continue  # Empty tray
 
                     try:
+                        inv_remaining = inventory_weights.get((ams_id, tray.tray_id))
                         result = await client.sync_ams_tray(
                             tray,
                             printer_name,
                             disable_weight_sync=disable_weight_sync,
                             cached_spools=cached_spools,
+                            inventory_remaining=inv_remaining,
                         )
                         if result:
                             synced += 1

+ 34 - 28
backend/app/services/spoolman.py

@@ -658,8 +658,8 @@ class SpoolmanClient:
         # Get tray_info_idx (Bambu filament preset ID like "GFA00")
         tray_info_idx = tray_data.get("tray_info_idx", "") or ""
 
-        # Get remaining percentage, ensure non-negative
-        remain = max(0, int(tray_data.get("remain", 0)))
+        # Get remaining percentage (-1 means unknown/not read by AMS)
+        remain = int(tray_data.get("remain", -1))
 
         return AMSTray(
             ams_id=ams_id,
@@ -760,6 +760,7 @@ class SpoolmanClient:
         printer_name: str,
         disable_weight_sync: bool = False,
         cached_spools: list[dict] | None = None,
+        inventory_remaining: float | None = None,
     ) -> dict | None:
         """Sync a single AMS tray to Spoolman.
 
@@ -777,6 +778,8 @@ class SpoolmanClient:
             cached_spools: Optional pre-fetched list of spools to search (avoids API calls).
                 When provided, this cache is passed to find_spool_by_tag to avoid redundant
                 API calls during batch sync operations.
+            inventory_remaining: Optional fallback remaining weight (grams) from the built-in
+                inventory when AMS MQTT data has invalid remain/tray_weight values.
 
         Returns:
             Synced spool dictionary or None if skipped or failed.
@@ -800,17 +803,31 @@ class SpoolmanClient:
             return None
 
         # Determine which identifier to use for Spoolman (prefer tray_uuid, fallback to tag_uid)
-        spool_tag = (
-            tray.tray_uuid if tray.tray_uuid and tray.tray_uuid != "00000000000000000000000000000000" else tray.tag_uid
-        )
-
-        # Calculate remaining weight (skip if data is invalid/unavailable)
-        # Some firmware sends remain=-1 (→0 after max) and tray_weight=0, making weight unreliable
-        remaining = (
-            self.calculate_remaining_weight(tray.remain, tray.tray_weight)
-            if tray.remain > 0 and tray.tray_weight > 0
-            else None
-        )
+        # Zero-filled values mean the AMS hasn't read the RFID tag — treat as no tag
+        zero_uuid = "00000000000000000000000000000000"
+        zero_tag = "0000000000000000"
+        spool_tag = None
+        if tray.tray_uuid and tray.tray_uuid != zero_uuid:
+            spool_tag = tray.tray_uuid
+        elif tray.tag_uid and tray.tag_uid != zero_tag:
+            spool_tag = tray.tag_uid
+
+        # Calculate remaining weight
+        # Primary: AMS MQTT data (remain percentage + tray_weight)
+        # Fallback: Built-in inventory tracked weight (when firmware sends invalid remain/tray_weight)
+        if tray.remain >= 0 and tray.tray_weight > 0:
+            remaining = self.calculate_remaining_weight(tray.remain, tray.tray_weight)
+        elif inventory_remaining is not None:
+            remaining = inventory_remaining
+            logger.debug(
+                "Using inventory weight fallback for %s AMS %s tray %s: %.1fg",
+                printer_name,
+                tray.ams_id,
+                tray.tray_id,
+                remaining,
+            )
+        else:
+            remaining = None
         location = f"{printer_name} - {self.convert_ams_slot_to_location(tray.ams_id, tray.tray_id)}"
 
         if spool_tag:
@@ -842,7 +859,8 @@ class SpoolmanClient:
             )
 
         # Fallback path: no RFID tag available (newer firmware may not expose UUIDs)
-        # Match existing Spoolman spools by their location (AMS slot position)
+        # Only update existing spools matched by location — never create new ones without a tag
+        # to avoid duplicates when old spools exist from previous RFID-based syncs
         existing = self._find_spool_by_location(location, cached_spools)
         if existing:
             logger.info(
@@ -856,23 +874,11 @@ class SpoolmanClient:
                 location=location,
             )
 
-        # No existing spool at this location — create a new one without a tag
         logger.info(
-            "Creating new spool in Spoolman for %s at %s (no RFID tag available)",
-            tray.tray_sub_brands,
+            "No existing spool found at '%s' — skipping (no RFID tag to create with)",
             location,
         )
-        filament = await self._find_or_create_filament(tray)
-        if not filament:
-            logger.error("Failed to find or create filament for %s", tray.tray_sub_brands)
-            return None
-
-        return await self.create_spool(
-            filament_id=filament["id"],
-            remaining_weight=remaining,
-            location=location,
-            comment="Created by Bambuddy",
-        )
+        return None
 
     async def _find_or_create_filament(self, tray: AMSTray) -> dict | None:
         """Find existing filament or create new one.