mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-09 23:45:40 +02:00
Use the vendor's empty spool weight as tare in Spoolman mode (#3195)
Spoolman resolves a spool's tare from the spool, then the filament, then the vendor's empty_spool_weight. Bambuddy skipped the vendor and fell back to 250 g, so weighing a spool whose tare was set only on its vendor gave the wrong remaining weight. The inventory weigh action, the SpoolBuddy scale and the displayed core weight now share one lookup, and "keep old weight" on a filament change stamps a vendor-inherited tare.
This commit is contained in:
@@ -144,6 +144,38 @@ def _safe_optional_float(value: object) -> float | None:
|
||||
return None
|
||||
|
||||
|
||||
# Used only when Spoolman has no empty-spool weight at any level.
|
||||
SPOOLMAN_FALLBACK_TARE = 250.0
|
||||
|
||||
|
||||
def spoolman_tare(spool: dict) -> tuple[float, str]:
|
||||
"""The empty-spool weight of a Spoolman spool, and where it came from.
|
||||
|
||||
Spoolman resolves the tare as the spool's own ``spool_weight``, then the
|
||||
filament's ``spool_weight``, then the vendor's ``empty_spool_weight``, and
|
||||
its own ``/measure`` endpoint follows that order. Skipping the vendor
|
||||
level made a spool whose tare lives only on its vendor weigh against
|
||||
250 g instead (#3195). Every Spoolman-mode tare in Bambuddy goes through
|
||||
here so the weigh endpoints and the displayed core weight cannot drift
|
||||
apart again.
|
||||
|
||||
Returns ``(grams, source)`` with source one of ``"spool"``,
|
||||
``"filament"``, ``"vendor"`` or ``"fallback"``. 0 is a real tare, not a
|
||||
missing one; a value that is not a finite number counts as missing.
|
||||
"""
|
||||
filament = spool.get("filament") or {}
|
||||
vendor = filament.get("vendor") or {}
|
||||
for source, raw in (
|
||||
("spool", spool.get("spool_weight")),
|
||||
("filament", filament.get("spool_weight")),
|
||||
("vendor", vendor.get("empty_spool_weight")),
|
||||
):
|
||||
value = _safe_optional_float(raw)
|
||||
if value is not None:
|
||||
return value, source
|
||||
return SPOOLMAN_FALLBACK_TARE, "fallback"
|
||||
|
||||
|
||||
def _extract_extra_str(extra: dict, key: str) -> str:
|
||||
"""Extract a JSON-encoded string from a Spoolman extra dict.
|
||||
|
||||
@@ -380,11 +412,9 @@ def _map_spoolman_spool(spool: dict) -> MappedSpoolFields:
|
||||
"effect_type": None,
|
||||
"brand": vendor.get("name") or None,
|
||||
"label_weight": label_weight,
|
||||
"core_weight": _safe_int(
|
||||
spool.get("spool_weight") if spool.get("spool_weight") is not None else filament.get("spool_weight"), 250
|
||||
),
|
||||
"core_weight": _safe_int(spoolman_tare(spool)[0], 250),
|
||||
# True when the spool has no spool_weight of its own and core_weight is
|
||||
# the filament type's (or the 250 g fallback). The spool form needs it
|
||||
# the filament type's, the vendor's or the 250 g fallback. The spool form needs it
|
||||
# to copy a spool without dropping an own tare or stamping an
|
||||
# inherited one (#2908).
|
||||
"core_weight_is_inherited": spool.get("spool_weight") is None,
|
||||
|
||||
@@ -891,7 +891,7 @@ async def update_spool_weight(
|
||||
update while the Spoolman row the user is actually looking at stayed
|
||||
unchanged (#1530). Mirrors the routing already used by ``nfc/tag-scanned``.
|
||||
"""
|
||||
from backend.app.api.routes._spoolman_helpers import _safe_float
|
||||
from backend.app.api.routes._spoolman_helpers import _safe_float, spoolman_tare
|
||||
from backend.app.models.spool import Spool
|
||||
|
||||
sm_client = await _get_spoolman_client_or_none(db)
|
||||
@@ -921,18 +921,17 @@ async def update_spool_weight(
|
||||
sm_spool = await sm_client.get_spool(req.spool_id)
|
||||
|
||||
filament = sm_spool.get("filament") or {}
|
||||
spool_tare = sm_spool.get("spool_weight")
|
||||
raw_tare = spool_tare if spool_tare is not None else filament.get("spool_weight")
|
||||
core_weight, tare_source = spoolman_tare(sm_spool)
|
||||
spool_weight_warning: str | None = None
|
||||
if raw_tare is None:
|
||||
if tare_source == "fallback":
|
||||
logger.warning(
|
||||
"Spoolman spool %d has no spool_weight set; using 250g fallback for tare",
|
||||
"Spoolman spool %d has no spool_weight or vendor empty_spool_weight set; using 250g fallback for tare",
|
||||
req.spool_id,
|
||||
)
|
||||
spool_weight_warning = (
|
||||
"spool_weight_not_set: Spoolman filament has no spool_weight configured; weight estimate uses 250g fallback"
|
||||
"spool_weight_not_set: Spoolman spool, filament and vendor have no empty-spool weight configured; "
|
||||
"weight estimate uses 250g fallback"
|
||||
)
|
||||
core_weight = _safe_float(raw_tare, 250.0)
|
||||
label_weight = _safe_float(filament.get("weight"), 1000.0)
|
||||
remaining_weight = max(0.0, req.weight_grams - core_weight)
|
||||
|
||||
|
||||
@@ -26,10 +26,10 @@ from backend.app.api.routes._spoolman_helpers import (
|
||||
NormalizedFilament,
|
||||
NormalizedVendorRef,
|
||||
_map_spoolman_spool,
|
||||
_safe_float,
|
||||
_safe_int,
|
||||
_safe_optional_float,
|
||||
assert_safe_spoolman_url,
|
||||
spoolman_tare,
|
||||
)
|
||||
from backend.app.core.auth import RequirePermissionIfAuthEnabled
|
||||
from backend.app.core.database import get_db
|
||||
@@ -1188,18 +1188,15 @@ async def sync_spool_weight(
|
||||
) -> dict:
|
||||
"""Update a spool's remaining weight from a measured gross weight.
|
||||
|
||||
Computes remaining = gross_weight - tare, where tare = spool.spool_weight
|
||||
if set, else filament.spool_weight; falls back to 250 g when both unset.
|
||||
Computes remaining = gross_weight - tare, with the tare resolved the way
|
||||
Spoolman does it (spool, filament, vendor, then 250 g; see spoolman_tare).
|
||||
"""
|
||||
client = await _get_client(db)
|
||||
|
||||
async with _translate_spoolman_errors():
|
||||
current = await client.get_spool(spool_id)
|
||||
|
||||
cur_filament = current.get("filament") or {}
|
||||
spool_tare = current.get("spool_weight")
|
||||
raw_tare = spool_tare if spool_tare is not None else cur_filament.get("spool_weight")
|
||||
core_weight = _safe_float(raw_tare, 250.0)
|
||||
core_weight, _source = spoolman_tare(current)
|
||||
remaining = max(0.0, data.weight_grams - core_weight)
|
||||
|
||||
async with _translate_spoolman_errors():
|
||||
@@ -2061,7 +2058,8 @@ async def patch_spoolman_filament(
|
||||
"""Update a Spoolman filament's name and/or spool_weight.
|
||||
|
||||
When spool_weight changes, Option A (keep_existing_spools=True) stamps the old
|
||||
weight onto spools currently inheriting it (spool.spool_weight is None) so their
|
||||
weight (the filament's, or its vendor's empty_spool_weight when the filament had
|
||||
none) onto spools currently inheriting it (spool.spool_weight is None) so their
|
||||
tare calculations are unaffected by the filament change.
|
||||
Option B (keep_existing_spools=False, the default): when spool_weight is a
|
||||
concrete value, stamps it onto every affected spool explicitly; when spool_weight
|
||||
@@ -2089,7 +2087,13 @@ async def patch_spoolman_filament(
|
||||
|
||||
if affected_spools:
|
||||
if body.keep_existing_spools:
|
||||
# What the inheriting spools weigh against today: the filament's
|
||||
# own value, or when it has none, its vendor's (#3195). With
|
||||
# neither they sit on the 250 g fallback, which is Bambuddy's
|
||||
# alone, so there is nothing real to keep.
|
||||
old_weight = _safe_optional_float(current.get("spool_weight"))
|
||||
if old_weight is None:
|
||||
old_weight = _safe_optional_float((current.get("vendor") or {}).get("empty_spool_weight"))
|
||||
if old_weight is not None:
|
||||
spools_to_fix = [s for s in affected_spools if s.get("spool_weight") is None]
|
||||
if spools_to_fix:
|
||||
@@ -2118,7 +2122,7 @@ async def patch_spoolman_filament(
|
||||
_raise_if_partial_failure(affected_spools, results, "spool_weight stamp (option B)")
|
||||
else:
|
||||
# Filament weight is being cleared — remove any per-spool override
|
||||
# so spools fall back to whatever the filament now provides.
|
||||
# so spools fall back to the vendor's empty_spool_weight, or 250 g.
|
||||
spools_to_clear = [s for s in affected_spools if s.get("spool_weight") is not None]
|
||||
if spools_to_clear:
|
||||
async with _translate_spoolman_errors():
|
||||
|
||||
Reference in New Issue
Block a user