mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-19 06:38:45 +02:00
This commit is contained in:
@@ -158,6 +158,51 @@ def _extract_extra_str(extra: dict, key: str) -> str:
|
||||
return decoded if isinstance(decoded, str) else ""
|
||||
|
||||
|
||||
# Spool.extra key holding the consumed-counter baseline: the value Spoolman's
|
||||
# used_weight had when the user last pressed "Reset usage to 0". Displayed
|
||||
# consumed is used_weight minus this, which is what internal mode has always
|
||||
# done with its own weight_used_baseline column (#1644). See #2906.
|
||||
BAMBU_WEIGHT_USED_BASELINE_KEY = "bambu_weight_used_baseline"
|
||||
|
||||
|
||||
def _extract_extra_float(extra: dict, key: str) -> float | None:
|
||||
"""Extract a JSON-encoded number from a Spoolman extra dict.
|
||||
|
||||
Same storage convention as :func:`_extract_extra_str` — Spoolman keeps
|
||||
extra values as JSON text, so 250.0 is stored as ``"250.0"``. Returns
|
||||
``None`` for missing keys, non-finite values, or anything that does not
|
||||
decode to a number, so the caller can tell "no baseline recorded" from
|
||||
"a baseline of zero".
|
||||
"""
|
||||
raw = extra.get(key)
|
||||
if raw is None:
|
||||
return None
|
||||
if isinstance(raw, (int, float)) and not isinstance(raw, bool):
|
||||
value = float(raw)
|
||||
elif isinstance(raw, str):
|
||||
try:
|
||||
decoded = json.loads(raw)
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
return None
|
||||
if isinstance(decoded, str):
|
||||
# Spoolman's extra fields default to field_type "text", which only
|
||||
# accepts values that decode to a str, so numbers written through
|
||||
# it arrive as '"263.0"' rather than '263.0'. Both spellings have
|
||||
# to read back the same, or a baseline written by the reset would
|
||||
# be invisible to the code that subtracts it.
|
||||
try:
|
||||
value = float(decoded)
|
||||
except ValueError:
|
||||
return None
|
||||
elif isinstance(decoded, (int, float)) and not isinstance(decoded, bool):
|
||||
value = float(decoded)
|
||||
else:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
return value if math.isfinite(value) else None
|
||||
|
||||
|
||||
def parse_spoolman_multi_colors(filament: dict) -> list[str]:
|
||||
"""Spoolman's ``multi_color_hexes`` as a list of bare 6/8-char hex tokens.
|
||||
|
||||
@@ -251,14 +296,37 @@ def _map_spoolman_spool(spool: dict) -> MappedSpoolFields:
|
||||
# When remaining_weight is unset (legacy spools, or filament linked but
|
||||
# never primed), fall back to the old behaviour: weight_used =
|
||||
# used_weight, baseline = 0.
|
||||
#
|
||||
# A "Reset usage to 0" records the then-current used_weight under
|
||||
# BAMBU_WEIGHT_USED_BASELINE_KEY in spool.extra and touches nothing else,
|
||||
# so displayed consumed is used_weight minus that. It is folded into the
|
||||
# baseline here because the frontend's arithmetic is fixed: it always shows
|
||||
# `weight_used - weight_used_baseline`. The reset used to PATCH Spoolman's
|
||||
# used_weight to 0 instead, and Spoolman recomputes remaining from initial
|
||||
# minus used, so the spool jumped back to full (#2906).
|
||||
# `or 0.0` would collapse None and 0.0, which is the one distinction
|
||||
# _extract_extra_float exists to preserve; spell the absent case out.
|
||||
# Clamp the low end too: the write side already refuses a negative
|
||||
# baseline, and without the same rule here a negative value stored by hand
|
||||
# would be subtracted, inflating the displayed counter. min() below only
|
||||
# bounds the other end.
|
||||
stored = _extract_extra_float(extra, BAMBU_WEIGHT_USED_BASELINE_KEY)
|
||||
stored_baseline = 0.0 if stored is None else max(0.0, stored)
|
||||
remaining_raw = spool.get("remaining_weight")
|
||||
if remaining_raw is not None:
|
||||
remaining_weight: float = _safe_float(remaining_raw, 0.0)
|
||||
used_weight: float = max(0.0, float(label_weight) - remaining_weight)
|
||||
weight_used_baseline: float = max(0.0, used_weight - real_used_weight)
|
||||
weight_used_baseline: float = max(0.0, used_weight - real_used_weight + stored_baseline)
|
||||
else:
|
||||
used_weight = real_used_weight
|
||||
weight_used_baseline = 0.0
|
||||
weight_used_baseline = stored_baseline # already clamped to >= 0 above
|
||||
# Never let the displayed counter read negative. used_weight can fall below
|
||||
# the baseline after a reset without anyone touching it: the AMS sync
|
||||
# writes remaining_weight from the tray percentage, and Spoolman derives
|
||||
# used_weight from that. The counter then reads 0 until consumption climbs
|
||||
# past the baseline again. Internal mode behaves the same way, and it is
|
||||
# far better than before #2906, when the next sync undid the reset outright.
|
||||
weight_used_baseline = min(weight_used_baseline, used_weight)
|
||||
|
||||
# Archived state – Spoolman uses a boolean ``archived`` field
|
||||
archived: bool = spool.get("archived", False)
|
||||
|
||||
@@ -950,18 +950,21 @@ async def reset_spool_consumed_counter(
|
||||
) -> dict:
|
||||
"""Zero the displayed "Total Consumed" counter for a Spoolman spool.
|
||||
|
||||
Spoolman doesn't have a native "baseline" field, so the implementation
|
||||
reaches for the closest equivalent: PATCH `used_weight=0` upstream.
|
||||
The read mapping in ``_map_spoolman_spool`` then derives Bambuddy's
|
||||
`weight_used = label - remaining_weight` and `baseline = weight_used -
|
||||
real_used_weight`, so the Inventory page's `weight_used - baseline`
|
||||
display lands at 0 while remaining (= label - weight_used) is preserved
|
||||
— parity with the internal-mode endpoint (#1390, see also
|
||||
Spoolman has no native baseline field, so the baseline lives in
|
||||
``spool.extra`` — the same mechanism internal mode uses for its
|
||||
``weight_used_baseline`` column. ``_map_spoolman_spool`` folds it back in,
|
||||
so the Inventory page's ``weight_used - baseline`` reads 0 while every
|
||||
native Spoolman field is left exactly as it was.
|
||||
|
||||
It used to PATCH ``used_weight = 0`` upstream, which is not the same thing:
|
||||
Spoolman recomputes ``remaining_weight`` from initial minus used, so the
|
||||
spool jumped back to full and the measured remaining filament was gone
|
||||
(#2906). Parity with the internal-mode endpoint (#1644, #1390, see also
|
||||
``backend/app/api/routes/inventory.py::reset_spool_consumed_counter``).
|
||||
"""
|
||||
client = await _get_client(db)
|
||||
async with _translate_spoolman_errors():
|
||||
spool = await client.reset_spool_usage(spool_id)
|
||||
spool = await client.reset_spool_consumed_counter(spool_id)
|
||||
try:
|
||||
mapped = _map_spoolman_spool(spool)
|
||||
except ValueError as exc:
|
||||
@@ -1112,7 +1115,7 @@ async def bulk_reset_spool_consumed_counter(
|
||||
for spool_id in spool_ids:
|
||||
try:
|
||||
async with _translate_spoolman_errors():
|
||||
await client.reset_spool_usage(spool_id)
|
||||
await client.reset_spool_consumed_counter(spool_id)
|
||||
reset_count += 1
|
||||
except HTTPException as exc:
|
||||
logger.warning("Spoolman reset-consumed-counter failed for spool %s: %s", spool_id, exc.detail)
|
||||
|
||||
Reference in New Issue
Block a user