mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 21:47:39 +02:00
Sync
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -771,6 +771,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:
|
||||
@@ -783,11 +803,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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
# 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 (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
|
||||
)
|
||||
# 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.
|
||||
|
||||
Reference in New Issue
Block a user