test_slot_spool_defaults.py 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. """GET /printers/{id}/slots/{ams}/{tray}/spool-defaults
  2. What the Configure AMS Slot dialog opens with. The slot usually already holds
  3. an assigned spool, and that spool carries a filament preset per printer model
  4. and a K profile per hotend -- the values the user set for exactly this
  5. situation. Before this endpoint the dialog defaulted to the slot's last manual
  6. configuration or the tray's RFID data and ignored them.
  7. Everything is resolved for the nozzle THIS slot feeds, so the answer differs
  8. between the two hotends of a dual-nozzle machine.
  9. """
  10. from unittest.mock import MagicMock, patch
  11. import pytest
  12. from httpx import AsyncClient
  13. URL = "/api/v1/printers/{pid}/slots/{ams}/{tray}/spool-defaults"
  14. RIGHT, LEFT = 0, 1
  15. class _Nozzle:
  16. def __init__(self, diameter):
  17. self.nozzle_diameter = diameter
  18. class _State:
  19. """Dual-nozzle, 0.4 on the right and 0.2 on the left, AMS 0 -> left."""
  20. def __init__(self, diameters=("0.4", "0.2")):
  21. self.nozzles = [_Nozzle(d) for d in diameters]
  22. self.ams_extruder_map = {"0": LEFT, "1": RIGHT}
  23. self.ams_switch_inlet = None
  24. self.raw_data = {}
  25. @pytest.fixture
  26. def dual_nozzle_printer_state():
  27. with patch("backend.app.api.routes.printers.printer_manager") as manager:
  28. manager.get_status = MagicMock(return_value=_State())
  29. manager.get_model = MagicMock(return_value="H2D")
  30. yield manager
  31. @pytest.fixture
  32. async def assigned_spool(db_session, printer_factory):
  33. """A spool in AMS 0 tray 0, with a per-model preset and both hotends calibrated."""
  34. from backend.app.models.spool import Spool
  35. from backend.app.models.spool_assignment import SpoolAssignment
  36. from backend.app.models.spool_filament_preset import SpoolFilamentPreset
  37. from backend.app.models.spool_k_profile import SpoolKProfile
  38. printer = await printer_factory(model="H2D")
  39. spool = Spool(
  40. brand="Bambu",
  41. material="PLA",
  42. color_name="Black",
  43. slicer_filament="GFSA00",
  44. slicer_filament_name="Bambu PLA Basic @BBL X1C",
  45. )
  46. db_session.add(spool)
  47. await db_session.commit()
  48. await db_session.refresh(spool)
  49. db_session.add(SpoolAssignment(spool_id=spool.id, printer_id=printer.id, ams_id=0, tray_id=0))
  50. db_session.add(
  51. SpoolFilamentPreset(
  52. spool_id=spool.id,
  53. printer_model="H2D",
  54. nozzle_diameter="0.2",
  55. slicer_filament="GFSA21",
  56. slicer_filament_name="Bambu PLA Basic @BBL H2D 0.2 nozzle",
  57. )
  58. )
  59. db_session.add_all(
  60. [
  61. SpoolKProfile(
  62. spool_id=spool.id,
  63. printer_id=printer.id,
  64. extruder=LEFT,
  65. nozzle_diameter="0.2",
  66. k_value=0.018,
  67. cali_idx=16,
  68. name="PLA left",
  69. ),
  70. SpoolKProfile(
  71. spool_id=spool.id,
  72. printer_id=printer.id,
  73. extruder=RIGHT,
  74. nozzle_diameter="0.4",
  75. k_value=0.020,
  76. cali_idx=15,
  77. name="PLA right",
  78. ),
  79. ]
  80. )
  81. await db_session.commit()
  82. return printer, spool
  83. @pytest.mark.integration
  84. class TestSlotSpoolDefaults:
  85. @pytest.mark.asyncio
  86. async def test_answers_for_the_hotend_this_slot_feeds(
  87. self, async_client: AsyncClient, assigned_spool, dual_nozzle_printer_state
  88. ):
  89. printer, _ = assigned_spool
  90. response = await async_client.get(URL.format(pid=printer.id, ams=0, tray=0))
  91. assert response.status_code == 200, response.text
  92. body = response.json()
  93. # AMS 0 feeds the LEFT hotend, which has the 0.2 fitted.
  94. assert body["extruder"] == LEFT
  95. assert body["nozzle_diameter"] == "0.2"
  96. # So the 0.2 preset override, not the spool's own X1C one...
  97. assert body["slicer_filament"] == "GFSA21"
  98. # ...and the profile calibrated on that hotend, not the other's.
  99. assert body["cali_idx"] == 16
  100. assert body["k_value"] == pytest.approx(0.018)
  101. assert body["profile_name"] == "PLA left"
  102. @pytest.mark.asyncio
  103. async def test_a_slot_with_no_spool_answers_nulls_not_404(
  104. self, async_client: AsyncClient, assigned_spool, dual_nozzle_printer_state
  105. ):
  106. """ "Nothing configured" is an ordinary answer -- the dialog falls back
  107. to what it did before rather than treating it as an error."""
  108. printer, _ = assigned_spool
  109. response = await async_client.get(URL.format(pid=printer.id, ams=1, tray=3))
  110. assert response.status_code == 200
  111. body = response.json()
  112. assert body["slicer_filament"] is None
  113. assert body["cali_idx"] is None
  114. # The nozzle is still resolved -- AMS 1 is the right hotend.
  115. assert body["extruder"] == RIGHT
  116. assert body["nozzle_diameter"] == "0.4"
  117. @pytest.mark.asyncio
  118. async def test_falls_back_to_the_spools_own_preset_without_an_override(
  119. self, async_client: AsyncClient, assigned_spool, dual_nozzle_printer_state, db_session
  120. ):
  121. from backend.app.models.spool_assignment import SpoolAssignment
  122. printer, spool = assigned_spool
  123. # Same spool in a slot on the RIGHT hotend, which has no 0.4 override.
  124. db_session.add(SpoolAssignment(spool_id=spool.id, printer_id=printer.id, ams_id=1, tray_id=0))
  125. await db_session.commit()
  126. body = (await async_client.get(URL.format(pid=printer.id, ams=1, tray=0))).json()
  127. assert body["slicer_filament"] == "GFSA00"
  128. assert body["cali_idx"] == 15
  129. @pytest.mark.asyncio
  130. async def test_an_offline_printer_still_answers(self, async_client: AsyncClient, assigned_spool):
  131. """Opening the dialog on a disconnected printer must not 500 -- it just
  132. cannot say which hotend the slot feeds."""
  133. printer, _ = assigned_spool
  134. with patch("backend.app.api.routes.printers.printer_manager") as manager:
  135. manager.get_status = MagicMock(return_value=None)
  136. manager.get_model = MagicMock(return_value="H2D")
  137. response = await async_client.get(URL.format(pid=printer.id, ams=0, tray=0))
  138. assert response.status_code == 200
  139. body = response.json()
  140. assert body["extruder"] is None
  141. assert body["nozzle_diameter"] == "0.4"