test_external_spool_change.py 3.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  1. """External-spool (vt_tray) change detection (#2575).
  2. The AMS change-hash in ``_handle_ams_data`` is built only from AMS units, so a
  3. filament swap on the external spool alone (e.g. generic TPU -> generic ABS on
  4. the printer) used to never re-trigger ``on_ams_change``. That left a stale
  5. inventory assignment on the ``ams_id=255`` slot: Bambuddy kept showing the old
  6. filament after the physical type had changed.
  7. These tests drive full MQTT messages through ``_process_message`` and assert the
  8. callback fires exactly when the external spool's *identity* changes — and not on
  9. every push (e.g. a steadily-dropping ``remain`` percentage during a print).
  10. """
  11. import pytest
  12. from backend.app.services.bambu_mqtt import BambuMQTTClient
  13. def _ext_spool_msg(tray_type: str, remain: int = 100, color: str = "000000FF"):
  14. """A realistic print message carrying only external-spool (vt_tray) data."""
  15. return {
  16. "print": {
  17. "vt_tray": {
  18. "id": "254",
  19. "tray_type": tray_type,
  20. "tray_color": color,
  21. "tray_info_idx": "",
  22. "tag_uid": "0000000000000000",
  23. "tray_uuid": "00000000000000000000000000000000",
  24. "remain": remain,
  25. }
  26. }
  27. }
  28. class TestExternalSpoolChangeDetection:
  29. @pytest.fixture
  30. def mqtt_client(self):
  31. return BambuMQTTClient(
  32. ip_address="192.168.1.100",
  33. serial_number="TEST123",
  34. access_code="12345678",
  35. )
  36. def test_type_swap_fires_callback(self, mqtt_client):
  37. """Swapping the external filament type re-triggers the sync callback."""
  38. calls: list = []
  39. mqtt_client.on_ams_change = lambda ams_data: calls.append(ams_data)
  40. # First observation of the external spool (TPU) — fires once.
  41. mqtt_client._process_message(_ext_spool_msg("TPU"))
  42. assert len(calls) == 1
  43. # Physical filament changed to ABS — must fire again so the stale
  44. # ams_id=255 assignment gets reconciled.
  45. mqtt_client._process_message(_ext_spool_msg("ABS"))
  46. assert len(calls) == 2
  47. # The callback receives the merged AMS list (never None).
  48. assert all(isinstance(c, list) for c in calls)
  49. def test_identical_push_does_not_refire(self, mqtt_client):
  50. """Repeated identical vt_tray pushes fire the callback only once."""
  51. calls: list = []
  52. mqtt_client.on_ams_change = lambda ams_data: calls.append(ams_data)
  53. mqtt_client._process_message(_ext_spool_msg("ABS"))
  54. mqtt_client._process_message(_ext_spool_msg("ABS"))
  55. mqtt_client._process_message(_ext_spool_msg("ABS"))
  56. assert len(calls) == 1
  57. def test_remain_only_change_does_not_refire(self, mqtt_client):
  58. """A dropping fill percentage must not spam the reconciliation callback."""
  59. calls: list = []
  60. mqtt_client.on_ams_change = lambda ams_data: calls.append(ams_data)
  61. mqtt_client._process_message(_ext_spool_msg("PLA", remain=100))
  62. assert len(calls) == 1
  63. # remain drops during a print — identity unchanged, no refire.
  64. mqtt_client._process_message(_ext_spool_msg("PLA", remain=87))
  65. mqtt_client._process_message(_ext_spool_msg("PLA", remain=42))
  66. assert len(calls) == 1
  67. def test_reset_to_empty_fires_callback(self, mqtt_client):
  68. """Resetting the external spool (empty tray_type) is an identity change."""
  69. calls: list = []
  70. mqtt_client.on_ams_change = lambda ams_data: calls.append(ams_data)
  71. mqtt_client._process_message(_ext_spool_msg("TPU"))
  72. assert len(calls) == 1
  73. mqtt_client._process_message(_ext_spool_msg("")) # reset / unloaded
  74. assert len(calls) == 2