feat: dual external spool support, AMS slot model filtering & pre-population

Backend:
- Add dual external spool support for H2D (vt_tray as list: Ext-L/Ext-R)
- Add cloud filament ID map endpoint (/cloud/filament-id-map)
- Fix RFID spool data erased by periodic AMS updates (skip tag matcher
  for RFID-tagged trays)
- Fix AMS slot config overwrites RFID spool state
- Fix K-profile selection corrupts existing profiles on X1C/P1S
- Resolve K-profiles filament name via cloud filament ID map
- Update print scheduler and usage tracker for dual external spools

Frontend:
- Add printer model filtering to ConfigureAmsSlotModal (cloud/local/builtin
  presets filtered by @BBL model suffix and compatible_printers)
- Add pre-population for configured slots (preset, color, K-profile)
- Add K-Profiles view with accurate filament name resolution
- Internationalize all ConfigureAmsSlotModal strings (en/de/fr/it/ja — 21 keys)
- Add 5 new ConfigureAmsSlotModal tests (model filtering, pre-selection,
  color pre-population, i18n)
- Update PrintersPage for dual external spool rendering

Docs:
- Update CHANGELOG, README, website features, and wiki AMS docs
This commit is contained in:
maziggy
2026-02-15 08:04:01 +01:00
parent 12e92a301d
commit a37dfaf7fb
40 changed files with 10007 additions and 9175 deletions
@@ -734,23 +734,26 @@ class TestPrinterStateToDict:
assert result["ams"][0]["tray"][0]["tag_uid"] is None
def test_vt_tray_parsing(self, mock_state):
"""Verify virtual tray is parsed correctly."""
"""Verify virtual tray is parsed correctly as a list."""
mock_state.raw_data = {
"vt_tray": {
"tray_color": "00FF00",
"tray_type": "PETG",
"tray_sub_brands": "Generic",
"remain": 60,
"tag_uid": "VT123",
}
"vt_tray": [
{
"tray_color": "00FF00",
"tray_type": "PETG",
"tray_sub_brands": "Generic",
"remain": 60,
"tag_uid": "VT123",
}
]
}
result = printer_state_to_dict(mock_state)
assert result["vt_tray"] is not None
assert result["vt_tray"]["id"] == 254
assert result["vt_tray"]["tray_color"] == "00FF00"
assert result["vt_tray"]["tray_type"] == "PETG"
assert isinstance(result["vt_tray"], list)
assert len(result["vt_tray"]) == 1
assert result["vt_tray"][0]["id"] == 254
assert result["vt_tray"][0]["tray_color"] == "00FF00"
assert result["vt_tray"][0]["tray_type"] == "PETG"
def test_hms_errors_conversion(self, mock_state):
"""Verify HMS errors are converted correctly."""
@@ -99,14 +99,14 @@ class TestBuildAmsTrayLookup:
def test_external_spool(self):
raw = {
"ams": [],
"vt_tray": {"tray_uuid": "EXT", "tag_uid": "X", "tray_type": "TPU"},
"vt_tray": [{"tray_uuid": "EXT", "tag_uid": "X", "tray_type": "TPU"}],
}
lookup = build_ams_tray_lookup(raw)
assert 254 in lookup
assert lookup[254]["tray_type"] == "TPU"
def test_empty_external_spool_skipped(self):
raw = {"ams": [], "vt_tray": {"tray_type": ""}}
raw = {"ams": [], "vt_tray": [{"tray_type": ""}]}
lookup = build_ams_tray_lookup(raw)
assert 254 not in lookup
@@ -140,7 +140,7 @@ class TestBuildLoadedFilaments:
"""Should include external spool."""
class MockStatus:
raw_data = {"vt_tray": {"tray_type": "TPU", "tray_color": "0000FF"}}
raw_data = {"vt_tray": [{"tray_type": "TPU", "tray_color": "0000FF"}]}
result = scheduler._build_loaded_filaments(MockStatus())
assert len(result) == 1
@@ -466,7 +466,7 @@ class TestBuildLoadedFilamentsTrayInfoIdx:
"""Should extract tray_info_idx from external spool."""
class MockStatus:
raw_data = {"vt_tray": {"tray_type": "TPU", "tray_color": "0000FF", "tray_info_idx": "P4d64437"}}
raw_data = {"vt_tray": [{"tray_type": "TPU", "tray_color": "0000FF", "tray_info_idx": "P4d64437"}]}
result = scheduler._build_loaded_filaments(MockStatus())
assert len(result) == 1
@@ -624,7 +624,7 @@ class TestNozzleAwareMapping:
class MockStatus:
raw_data = {
"vt_tray": {"tray_type": "TPU", "tray_color": "0000FF"},
"vt_tray": [{"tray_type": "TPU", "tray_color": "0000FF"}],
"ams_extruder_map": {"0": 0},
}
@@ -637,7 +637,7 @@ class TestNozzleAwareMapping:
"""External spool extruder_id should be None without ams_extruder_map."""
class MockStatus:
raw_data = {"vt_tray": {"tray_type": "TPU", "tray_color": "0000FF"}}
raw_data = {"vt_tray": [{"tray_type": "TPU", "tray_color": "0000FF"}]}
result = scheduler._build_loaded_filaments(MockStatus())
assert len(result) == 1