Sync fixes

This commit is contained in:
maziggy
2026-02-12 18:23:55 +01:00
parent 16de29c374
commit a6f6df2e34
10 changed files with 530 additions and 9146 deletions
+11 -3
View File
@@ -345,6 +345,8 @@ async def sync_all_printers(
all_errors = []
# Track tray UUIDs per printer (for clearing removed spools)
printer_tray_uuids: dict[str, set[str]] = {}
# Track synced spool IDs per printer (for location-based cleanup when no UUIDs available)
printer_synced_ids: dict[str, set[int]] = {}
# OPTIMIZATION: Fetch all spools once before processing ALL printers/trays
# This eliminates redundant API calls across all printers
@@ -368,8 +370,9 @@ async def sync_all_printers(
if not ams_data:
continue
# Initialize tray UUID set for this printer
# Initialize tracking sets for this printer
printer_tray_uuids[printer.name] = set()
printer_synced_ids[printer.name] = set()
# Handle different AMS data structures
# Traditional AMS: list of {"id": N, "tray": [...]} dicts
@@ -440,8 +443,10 @@ async def sync_all_printers(
)
if sync_result:
total_synced += 1
# Add newly created spool to cache
# Track synced spool ID for cleanup
if sync_result.get("id"):
printer_synced_ids[printer.name].add(sync_result["id"])
# Add newly created spool to cache
spool_exists = any(s.get("id") == sync_result["id"] for s in cached_spools)
if not spool_exists:
cached_spools.append(sync_result)
@@ -453,7 +458,10 @@ async def sync_all_printers(
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, cached_spools=cached_spools
printer_name,
current_tray_uuids,
cached_spools=cached_spools,
synced_spool_ids=printer_synced_ids.get(printer_name, set()),
)
if cleared > 0:
logger.info("Cleared location for %s spools removed from %s", cleared, printer_name)
+3
View File
@@ -1176,6 +1176,9 @@ async def run_migrations(conn):
)
""")
)
except OperationalError:
pass # Already applied
# Migration: Add open_in_new_tab column to external_links
try:
await conn.execute(text("ALTER TABLE external_links ADD COLUMN open_in_new_tab BOOLEAN DEFAULT 0"))
+3 -2
View File
@@ -941,8 +941,9 @@ class BambuMQTTClient:
# Fields that should always be updated (even with empty/zero values):
# - remain, k, id, cali_idx: status indicators where 0 is valid
# - tray_type, tray_sub_brands, tag_uid, tray_uuid, tray_info_idx,
# tray_color, tray_id_name: slot content indicators that must be
# cleared when a spool is removed (fixes #147 - old AMS empty slot)
# tray_color, tray_id_name: slot content indicators that must
# be cleared when a spool is removed (fixes #147 - old AMS
# empty slot)
always_update_fields = (
"remain",
"k",
+82 -26
View File
@@ -468,6 +468,26 @@ class SpoolmanClient:
return spool
return None
def _find_spool_by_location(self, location: str, cached_spools: list[dict] | None) -> dict | None:
"""Find a spool by exact location match.
Used as fallback when RFID tag data is unavailable (e.g., newer firmware
that doesn't expose tray_uuid/tag_uid via MQTT).
Args:
location: Exact location string (e.g., "H2D-1 - AMS A1")
cached_spools: Pre-fetched list of spools to search
Returns:
Spool dictionary or None if not found.
"""
if not cached_spools:
return None
for spool in cached_spools:
if spool.get("location") == location:
return spool
return None
async def find_spools_by_location_prefix(
self, location_prefix: str, cached_spools: list[dict] | None = None
) -> list[dict]:
@@ -494,17 +514,21 @@ class SpoolmanClient:
printer_name: str,
current_tray_uuids: set[str],
cached_spools: list[dict] | None = None,
synced_spool_ids: set[int] | None = None,
) -> 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.
and clears the location for any that are not in the current_tray_uuids set
and were not synced in this cycle (synced_spool_ids).
Args:
printer_name: The printer name used as location prefix
current_tray_uuids: Set of tray_uuids currently in the AMS
cached_spools: Optional pre-fetched list of spools to search (avoids API call)
synced_spool_ids: Set of spool IDs that were synced in this cycle
(protects location-matched spools when RFID data is unavailable)
Returns:
Number of spools whose location was cleared.
@@ -514,6 +538,12 @@ class SpoolmanClient:
cleared_count = 0
for spool in spools_at_printer:
spool_id = spool.get("id")
# Skip spools that were just synced (matched by location or tag)
if synced_spool_ids and spool_id in synced_spool_ids:
continue
# Get the tray_uuid (stored as "tag" in extra field)
extra = spool.get("extra", {}) or {}
stored_tag = extra.get("tag", "")
@@ -526,10 +556,10 @@ class SpoolmanClient:
# 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"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)
result = await self.update_spool(spool_id=spool_id, clear_location=True)
if result:
cleared_count += 1
@@ -774,48 +804,74 @@ class SpoolmanClient:
tray.tray_uuid if tray.tray_uuid and tray.tray_uuid != "00000000000000000000000000000000" else tray.tag_uid
)
# If no unique identifier available, we can't sync even if it's a Bambu Lab spool
if not spool_tag:
logger.warning(
f"Bambu Lab spool detected but no unique identifier for Spoolman: "
f"{printer_name} AMS {tray.ams_id} tray {tray.tray_id} (tray_info_idx={tray.tray_info_idx})"
)
return None
# Calculate remaining weight
remaining = self.calculate_remaining_weight(tray.remain, tray.tray_weight)
# 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
)
location = f"{printer_name} - {self.convert_ams_slot_to_location(tray.ams_id, tray.tray_id)}"
# Find existing spool by tag (tray_uuid or tag_uid, stored as "tag" in Spoolman)
existing = await self.find_spool_by_tag(spool_tag, cached_spools=cached_spools)
if spool_tag:
# Primary path: match by RFID tag
existing = await self.find_spool_by_tag(spool_tag, cached_spools=cached_spools)
if existing:
logger.info("Updating existing spool %s for tag %s...", existing["id"], spool_tag[:16])
return await self.update_spool(
spool_id=existing["id"],
remaining_weight=None if disable_weight_sync else remaining,
location=location,
)
# Spool not found by tag - auto-create it
logger.info("Creating new spool in Spoolman for %s (tag: %s...)", tray.tray_sub_brands, spool_tag[:16])
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
import json
return await self.create_spool(
filament_id=filament["id"],
remaining_weight=remaining,
location=location,
comment="Created by Bambuddy",
extra={"tag": json.dumps(spool_tag)},
)
# Fallback path: no RFID tag available (newer firmware may not expose UUIDs)
# Match existing Spoolman spools by their location (AMS slot position)
existing = self._find_spool_by_location(location, cached_spools)
if existing:
# Update existing spool
logger.info("Updating existing spool %s for tag %s...", existing["id"], spool_tag[:16])
logger.info(
"Updating spool %s by location match '%s' (no RFID tag available)",
existing["id"],
location,
)
return await self.update_spool(
spool_id=existing["id"],
remaining_weight=None if disable_weight_sync else remaining,
location=location,
)
# Spool not found - auto-create it
logger.info("Creating new spool in Spoolman for %s (tag: %s...)", tray.tray_sub_brands, spool_tag[:16])
# First find or create the filament type
# 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,
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
# Create the spool with identifier stored as "tag" in extra field
# Note: Spoolman extra field values must be valid JSON, so we encode the string
import json
return await self.create_spool(
filament_id=filament["id"],
remaining_weight=remaining,
location=location,
comment="Created by Bambuddy",
extra={"tag": json.dumps(spool_tag)},
)
async def _find_or_create_filament(self, tray: AMSTray) -> dict | None:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -23,8 +23,8 @@
<!-- Splash screens for iOS -->
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
<script type="module" crossorigin src="/assets/index-CDlz__Os.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CK9fZPad.css">
<script type="module" crossorigin src="/assets/index-CxS9CTuG.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DMk3iz3Q.css">
</head>
<body>
<div id="root"></div>