test_nozzle_print_time_1908.py 3.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485
  1. """H2C per-nozzle print time (#1908).
  2. Recent H2C firmware reports how long each nozzle has printed as ``p_t`` (seconds)
  3. on its ``device.nozzle.info`` entry. It is carried as ``print_time_s`` through
  4. the MQTT state, the WebSocket status push and the REST status, and stays None
  5. where the firmware does not report it.
  6. """
  7. import pytest
  8. from backend.app.schemas.printer import NozzleRackSlot
  9. from backend.app.services.bambu_mqtt import BambuMQTTClient, PrinterState, nozzle_print_time_s
  10. from backend.app.services.printer_manager import printer_state_to_dict
  11. def _nozzle(nozzle_id: int, **extra) -> dict:
  12. return {"id": nozzle_id, "type": "HS01", "diameter": 0.4, "tm": 350, "wear": 0, "stat": 184, **extra}
  13. def _ingested(*nozzles: dict) -> BambuMQTTClient:
  14. client = BambuMQTTClient(ip_address="10.0.0.1", serial_number="H2C", access_code="c", model="H2C")
  15. client._process_message({"print": {"device": {"nozzle": {"info": list(nozzles)}}}})
  16. return client
  17. class TestParse:
  18. @pytest.mark.parametrize(
  19. ("raw", "expected"),
  20. [
  21. (0, 0),
  22. (7265, 7265),
  23. (7265.9, 7265),
  24. (None, None),
  25. (-1, None),
  26. ("7265", None),
  27. (True, None),
  28. ],
  29. )
  30. def test_only_a_non_negative_number_is_a_print_time(self, raw, expected):
  31. assert nozzle_print_time_s(raw) == expected
  32. def test_each_nozzle_keeps_its_own_print_time(self):
  33. rack = _ingested(_nozzle(0, p_t=3600), _nozzle(16, p_t=90061), _nozzle(17)).state.nozzle_rack
  34. assert {n["id"]: n["print_time_s"] for n in rack} == {0: 3600, 16: 90061, 17: None}
  35. def test_firmware_without_p_t_leaves_it_unset(self):
  36. (nozzle,) = _ingested(_nozzle(0)).state.nozzle_rack
  37. assert nozzle["print_time_s"] is None
  38. class TestSerialise:
  39. def test_the_websocket_push_carries_it(self):
  40. state = PrinterState(connected=True, state="IDLE")
  41. state.nozzle_rack = _ingested(_nozzle(16, p_t=7200)).state.nozzle_rack
  42. (slot,) = printer_state_to_dict(state)["nozzle_rack"]
  43. assert slot["print_time_s"] == 7200
  44. def test_the_push_and_the_rest_schema_have_the_same_fields(self):
  45. # The frontend shallow-merges pushes over the REST status, so a field
  46. # the push leaves out goes stale on the first push.
  47. state = PrinterState(connected=True, state="IDLE")
  48. state.nozzle_rack = _ingested(_nozzle(16, p_t=7200)).state.nozzle_rack
  49. (slot,) = printer_state_to_dict(state)["nozzle_rack"]
  50. assert set(slot) == set(NozzleRackSlot.model_fields)
  51. assert NozzleRackSlot(**slot).print_time_s == 7200
  52. class TestRestStatus:
  53. @pytest.mark.asyncio
  54. async def test_the_status_route_carries_it(self, async_client, printer_factory):
  55. # What the page has before the first push arrives.
  56. from unittest.mock import MagicMock, patch
  57. printer = await printer_factory()
  58. state = _ingested(_nozzle(0, p_t=3600), _nozzle(16)).state
  59. state.connected = True
  60. state.state = "IDLE"
  61. with patch("backend.app.api.routes.printers.printer_manager") as mock_pm:
  62. mock_pm.get_status = MagicMock(return_value=state)
  63. mock_pm.is_awaiting_plate_clear = MagicMock(return_value=False)
  64. response = await async_client.get(f"/api/v1/printers/{printer.id}/status")
  65. assert response.status_code == 200
  66. assert {n["id"]: n["print_time_s"] for n in response.json()["nozzle_rack"]} == {0: 3600, 16: None}