test_ams_alarm_gating.py 3.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
  1. """Tests for the empty-AMS alarm gate (#1619).
  2. Empty AMS units still emit humidity/temperature sensor readings, but those
  3. readings are ambient and not actionable — there's no filament to dry. Without
  4. the gate every empty AMS spammed an hourly alarm. ``_ams_has_filament``
  5. inspects the firmware-reported ``tray_exist_bits`` bitmap (fallback: ``tray``
  6. array's ``tray_type`` strings) so the alarm dispatch in ``record_ams_history``
  7. can skip empty units while still alarming on loaded ones in the same printer.
  8. """
  9. from backend.app.main import _ams_has_filament
  10. class TestAmsHasFilament:
  11. def test_tray_exist_bits_zero_means_empty(self):
  12. assert _ams_has_filament({"tray_exist_bits": "0"}) is False
  13. # Real firmware sometimes pads with extra zeros or prefixes; all
  14. # parseable forms of zero should resolve to "empty".
  15. assert _ams_has_filament({"tray_exist_bits": "00"}) is False
  16. assert _ams_has_filament({"tray_exist_bits": "0x0"}) is False
  17. def test_tray_exist_bits_nonzero_means_loaded(self):
  18. # Single tray loaded — e.g. AMS-Lite or AMS-HT.
  19. assert _ams_has_filament({"tray_exist_bits": "1"}) is True
  20. # Four-slot AMS with all slots full (bitmap 0xf == 0b1111).
  21. assert _ams_has_filament({"tray_exist_bits": "f"}) is True
  22. # Mixed — 0xa == 0b1010, two slots loaded.
  23. assert _ams_has_filament({"tray_exist_bits": "a"}) is True
  24. # The exact bitmap seen in #1622 / #1602 logs.
  25. assert _ams_has_filament({"tray_exist_bits": "ed"}) is True
  26. def test_falls_back_to_tray_array_when_bits_missing(self):
  27. # Empty tray_type strings across the whole tray array → empty AMS.
  28. ams_empty = {
  29. "tray": [
  30. {"id": 0, "tray_type": ""},
  31. {"id": 1, "tray_type": ""},
  32. ]
  33. }
  34. assert _ams_has_filament(ams_empty) is False
  35. # Any non-empty tray_type → loaded AMS.
  36. ams_loaded = {
  37. "tray": [
  38. {"id": 0, "tray_type": ""},
  39. {"id": 1, "tray_type": "PLA"},
  40. ]
  41. }
  42. assert _ams_has_filament(ams_loaded) is True
  43. def test_missing_both_signals_returns_false(self):
  44. # No tray_exist_bits AND no tray array — early-pushall shape; we
  45. # treat it as "no info → don't alarm" rather than guessing loaded.
  46. assert _ams_has_filament({}) is False
  47. def test_unparseable_bitmap_falls_back_to_tray_array(self):
  48. # Garbage in tray_exist_bits — must not raise and must fall through
  49. # to the tray array check.
  50. loaded = {"tray_exist_bits": "garbage", "tray": [{"id": 0, "tray_type": "PETG"}]}
  51. assert _ams_has_filament(loaded) is True
  52. empty = {"tray_exist_bits": "garbage", "tray": []}
  53. assert _ams_has_filament(empty) is False
  54. def test_empty_bits_string_falls_back_to_tray_array(self):
  55. # Some pre-handshake pushall shapes set the field but leave it blank.
  56. loaded = {"tray_exist_bits": "", "tray": [{"id": 0, "tray_type": "ABS"}]}
  57. assert _ams_has_filament(loaded) is True
  58. def test_whitespace_tray_type_is_not_loaded(self):
  59. # A tray_type that's all whitespace doesn't count as a real material.
  60. assert _ams_has_filament({"tray": [{"id": 0, "tray_type": " "}]}) is False
  61. def test_non_dict_tray_entries_are_skipped(self):
  62. # Defensive: malformed tray array shouldn't crash the helper.
  63. assert _ams_has_filament({"tray": [None, "junk", 42]}) is False
  64. def test_non_string_bits_falls_back(self):
  65. # Some MQTT shapes send tray_exist_bits as int; we only parse strings,
  66. # so an int falls through to the tray array.
  67. loaded = {"tray_exist_bits": 0xED, "tray": [{"id": 0, "tray_type": "PLA"}]}
  68. assert _ams_has_filament(loaded) is True
  69. empty_int = {"tray_exist_bits": 0xED} # no tray array, int ignored
  70. assert _ams_has_filament(empty_int) is False