mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-03 20:56:18 +02:00
Two tightly-coupled deliverables in one drop -- a new AMS Filament Backup status/control surface, and the #1766 fix that depends on it. Added -- AMS Filament Backup status + control - Parse bit 18 of top-level print.cfg into PrinterState.ams_filament_backup on every push_status. Verified against OrcaSlicer source (DeviceManager.cpp:4961) and a live H2D ON/OFF capture. Tri-state (None = A1 family / pre-cfg push) preserves today's behaviour. - Hold-timer guard (3 s) prevents stale frames from flickering the badge back to the printer's old cfg after a user-initiated toggle. - POST /printers/{id}/ams-backup toggle, set_ams_filament_backup() client method calling _set_print_option("auto_switch_filament", enabled). - GET /printers/{id}/inventory-remain endpoint exposes the same map the dispatcher uses (internal and Spoolman modes both work uniformly). - Small icon badge in the printer card's "Filaments" section header (placement reads as printer-wide because the cfg bit is printer-wide, not per-AMS). Click to toggle, success toast. - 5 i18n keys x 11 locales for the badge UI. Fixed -- #1766: prefer_lowest didn't pick lowest, ignored backup state - Backend gate in _compute_ams_mapping_for_printer: coerce prefer_lowest to False when status.ams_filament_backup is False; log the skip. - New effectivePreferLowest(setting, backup) helper applied at every frontend sort entry point: single-printer PrintModal, multi-printer hook per-printer, PrinterSelector InlineMappingEditor, FilamentMapping standalone editor (the last had NO preferLowest awareness at all before this change). - New preferLowestSortKey(f, inventoryByTrayId) mirrors backend's two-tier key exactly, including the banding tie-break (regular AMS < AMS-HT < external) so the client-side pre-compute matches the dispatch-time pick. An earlier draft used a flat `amsId * 4 + trayId` priority which gave external slots (ams_id = -1) a NEGATIVE priority -- caught in code review before commit. - Settings -> Filament -> "Prefer lowest remaining filament" gets an explanatory note about the printer-side AMS Backup dependency, with i18n key in all 11 locales.
87 lines
3.2 KiB
Python
87 lines
3.2 KiB
Python
"""Tests for GET /printers/{id}/inventory-remain (#1766).
|
|
|
|
The endpoint exposes the same `_build_inventory_remain_overrides` map the
|
|
dispatcher uses so PrintModal's client-side "Prefer Lowest Remaining Filament"
|
|
sort agrees with what gets dispatched — closes the gap where Spoolman-mode
|
|
users couldn't see inventory grams from the frontend.
|
|
"""
|
|
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from backend.app.api.routes.printers import get_inventory_remain
|
|
|
|
|
|
@pytest.fixture
|
|
def db():
|
|
return MagicMock()
|
|
|
|
|
|
async def _call_endpoint(db, printer_id=1):
|
|
return await get_inventory_remain(printer_id=printer_id, _=None, db=db)
|
|
|
|
|
|
class TestGetInventoryRemain:
|
|
@pytest.mark.asyncio
|
|
async def test_returns_empty_when_printer_has_no_status(self, db):
|
|
# Printer disconnected / unknown — endpoint must not error, return {}.
|
|
with patch(
|
|
"backend.app.services.printer_manager.printer_manager.get_status",
|
|
return_value=None,
|
|
):
|
|
result = await _call_endpoint(db)
|
|
assert result == {"inventory_remain_g": {}}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_serialises_globaltrayid_keys_as_strings(self, db):
|
|
# JSON requires string keys; client converts back to Number on receive.
|
|
# Asserts the key-shape contract the frontend depends on.
|
|
state = SimpleNamespace(raw_data={})
|
|
with (
|
|
patch(
|
|
"backend.app.services.printer_manager.printer_manager.get_status",
|
|
return_value=state,
|
|
),
|
|
patch(
|
|
"backend.app.services.print_scheduler.PrintScheduler._build_loaded_filaments",
|
|
return_value=[
|
|
{"ams_id": 0, "tray_id": 0, "global_tray_id": 0, "is_external": False},
|
|
{"ams_id": 0, "tray_id": 3, "global_tray_id": 3, "is_external": False},
|
|
],
|
|
),
|
|
patch(
|
|
"backend.app.services.print_scheduler.PrintScheduler._build_inventory_remain_overrides",
|
|
new=AsyncMock(return_value={0: 950.0, 3: 50.0}),
|
|
),
|
|
):
|
|
result = await _call_endpoint(db)
|
|
|
|
assert result == {"inventory_remain_g": {"0": 950.0, "3": 50.0}}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_returns_empty_dict_when_no_bound_slots(self, db):
|
|
# Loaded filaments exist but none are bound to an inventory spool.
|
|
# Backend returns {}; route serialises it unchanged.
|
|
state = SimpleNamespace(raw_data={})
|
|
with (
|
|
patch(
|
|
"backend.app.services.printer_manager.printer_manager.get_status",
|
|
return_value=state,
|
|
),
|
|
patch(
|
|
"backend.app.services.print_scheduler.PrintScheduler._build_loaded_filaments",
|
|
return_value=[
|
|
{"ams_id": 0, "tray_id": 0, "global_tray_id": 0, "is_external": False},
|
|
],
|
|
),
|
|
patch(
|
|
"backend.app.services.print_scheduler.PrintScheduler._build_inventory_remain_overrides",
|
|
new=AsyncMock(return_value={}),
|
|
),
|
|
):
|
|
result = await _call_endpoint(db)
|
|
|
|
assert result == {"inventory_remain_g": {}}
|