Files
bambuddy/backend/tests/unit/services/test_tray_change_callback.py
T
maziggy 454457a0af Attribute filament correctly when AMS backup swaps spools mid-print
Everything the completion path needs to split a print's filament across
the trays it fed from lived only in memory: the dispatched plate and
slot-to-tray mapping, the spool-assignment snapshot, and the tray-change
log. A print that outlived a restart lost all of it and fell back to
what the printer reports at completion -- which, with AMS Filament
Backup on, is the substitute tray. The whole print was charged to the
spool that only finished it while the spool that ran dry was charged
nothing.

Persist that context in a new active_print_sessions row, append tray
changes as they happen, and restore both the session and the printer's
tray-change log at restart recovery. Seed the log from the current tray
when there is nothing to restore, since last_loaded_tray advances even
when no change is logged.

Rank the queue item's stored ams_mapping above the printer's live
mapping field, which is what backup rewrites. Recover plate_id from the
archive or queue item, and give extract_layer_filament_usage_from_3mf a
plate_id instead of taking the first .gcode member -- a Bambu Studio
export stores plate 2 first, so per-layer figures were measured against
the wrong plate for both inventory backends.

Stop auto-unlinking a spool assignment when its slot reports empty
during a running print. At a runout the spool is still in the AMS, and
dropping the link leaves the completion path nothing to charge.

Capture the print-start context for both inventory backends. Spoolman's
own durable row (#1820) carries its plate-scoped figures and dispatched
mapping but not the tray-change log, and its slot assignments -- the
way. Registration in _active_sessions stays gated, since on_ams_change
reads it to decide whether to skip the remain%-based weight sync (#880).
2026-08-13 08:37:16 +02:00

75 lines
2.7 KiB
Python

"""The tray-change log has to leave the process.
``PrinterState.tray_change_log`` is what the usage tracker splits filament
weight on when AMS filament backup swaps in a fresh spool mid-print. It lived
only in memory, so a restart during a long print erased the segment boundaries
and the whole job got charged to the tray that finished it. The client now
reports every appended entry so main.py can persist it.
"""
import pytest
from backend.app.services.bambu_mqtt import BambuMQTTClient
def _tray_msg(tray_now: int):
"""A partial AMS update carrying only tray_now, as P-series and H2D send."""
return {"print": {"ams": {"tray_now": str(tray_now)}}}
class TestTrayChangeCallback:
@pytest.fixture
def mqtt_client(self):
client = BambuMQTTClient(
ip_address="192.168.1.100",
serial_number="TEST123",
access_code="12345678",
)
client._was_running = True
client._completion_triggered = False
return client
def test_every_logged_change_is_reported(self, mqtt_client):
seen: list[tuple[int, int]] = []
mqtt_client.on_tray_change = lambda tray, layer: seen.append((tray, layer))
mqtt_client.state.layer_num = 0
mqtt_client._process_message(_tray_msg(2))
mqtt_client.state.layer_num = 670
mqtt_client._process_message(_tray_msg(254))
mqtt_client.state.layer_num = 675
mqtt_client._process_message(_tray_msg(3))
assert seen == [(2, 0), (254, 670), (3, 675)]
assert mqtt_client.state.tray_change_log == [(2, 0), (254, 670), (3, 675)]
def test_repeat_of_the_same_tray_is_not_reported(self, mqtt_client):
"""The printer republishes tray_now on every push; only transitions
are segment boundaries."""
seen: list[tuple[int, int]] = []
mqtt_client.on_tray_change = lambda tray, layer: seen.append((tray, layer))
mqtt_client._process_message(_tray_msg(2))
mqtt_client.state.layer_num = 40
mqtt_client._process_message(_tray_msg(2))
assert seen == [(2, 0)]
def test_no_callback_outside_a_running_print(self, mqtt_client):
seen: list[tuple[int, int]] = []
mqtt_client.on_tray_change = lambda tray, layer: seen.append((tray, layer))
mqtt_client._was_running = False
mqtt_client._process_message(_tray_msg(2))
assert seen == []
assert mqtt_client.state.tray_change_log == []
def test_missing_callback_does_not_break_logging(self, mqtt_client):
"""The callback is optional — the in-memory log still has to work."""
mqtt_client.on_tray_change = None
mqtt_client._process_message(_tray_msg(2))
assert mqtt_client.state.tray_change_log == [(2, 0)]