|
|
@@ -734,6 +734,207 @@ class TestAMSDataMerging:
|
|
|
)
|
|
|
|
|
|
|
|
|
+class TestAMSTrayStateClearning:
|
|
|
+ """Tests for AMS tray state-based clearing (#784).
|
|
|
+
|
|
|
+ Some printers (e.g. H2D) only send {id, state} in incremental MQTT
|
|
|
+ updates when a tray is not fully loaded. state=11 means loaded;
|
|
|
+ other values (9=empty, 10=spool present but filament not in feeder)
|
|
|
+ should clear stale tray data that was set from an earlier pushall.
|
|
|
+ """
|
|
|
+
|
|
|
+ @pytest.fixture
|
|
|
+ def mqtt_client(self):
|
|
|
+ from backend.app.services.bambu_mqtt import BambuMQTTClient
|
|
|
+
|
|
|
+ client = BambuMQTTClient(
|
|
|
+ ip_address="192.168.1.100",
|
|
|
+ serial_number="TEST_H2D",
|
|
|
+ access_code="12345678",
|
|
|
+ )
|
|
|
+ return client
|
|
|
+
|
|
|
+ def _seed_loaded_tray(self, mqtt_client):
|
|
|
+ """Seed AMS 0 with a fully loaded tray (state=11) and an empty slot."""
|
|
|
+ initial = {
|
|
|
+ "ams": [
|
|
|
+ {
|
|
|
+ "id": 0,
|
|
|
+ "tray": [
|
|
|
+ {
|
|
|
+ "id": 0,
|
|
|
+ "tray_type": "PETG",
|
|
|
+ "tray_sub_brands": "PETG HF",
|
|
|
+ "tray_color": "00FF00FF",
|
|
|
+ "tray_id_name": "A00-G1",
|
|
|
+ "tray_info_idx": "GFG99",
|
|
|
+ "tag_uid": "AABBCCDD11223344",
|
|
|
+ "tray_uuid": "AABBCCDD11223344AABBCCDD11223344",
|
|
|
+ "remain": 75,
|
|
|
+ "k": 0.02,
|
|
|
+ "cali_idx": 5,
|
|
|
+ "state": 11,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "id": 1,
|
|
|
+ "tray_type": "PLA",
|
|
|
+ "tray_color": "FF0000FF",
|
|
|
+ "remain": 50,
|
|
|
+ "state": 11,
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ "power_on_flag": False, # H2D always sends False
|
|
|
+ }
|
|
|
+ mqtt_client._handle_ams_data(initial)
|
|
|
+ ams = mqtt_client.state.raw_data["ams"]
|
|
|
+ assert ams[0]["tray"][0]["tray_type"] == "PETG"
|
|
|
+ assert ams[0]["tray"][1]["tray_type"] == "PLA"
|
|
|
+
|
|
|
+ def test_state_10_clears_stale_tray_data(self, mqtt_client):
|
|
|
+ """Incremental update with state=10 (spool present, not loaded) clears tray."""
|
|
|
+ self._seed_loaded_tray(mqtt_client)
|
|
|
+
|
|
|
+ # H2D sends only {id, state} when filament is retracted
|
|
|
+ update = {
|
|
|
+ "ams": [
|
|
|
+ {
|
|
|
+ "id": 0,
|
|
|
+ "tray": [
|
|
|
+ {"id": 0, "state": 10},
|
|
|
+ {"id": 1, "state": 11}, # slot 1 still loaded
|
|
|
+ ],
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ "power_on_flag": False,
|
|
|
+ }
|
|
|
+ mqtt_client._handle_ams_data(update)
|
|
|
+
|
|
|
+ ams = mqtt_client.state.raw_data["ams"]
|
|
|
+ tray0 = ams[0]["tray"][0]
|
|
|
+ tray1 = ams[0]["tray"][1]
|
|
|
+
|
|
|
+ # Tray 0 should be cleared
|
|
|
+ assert tray0["tray_type"] == "", "tray_type must be cleared on state=10"
|
|
|
+ assert tray0["tray_color"] == "", "tray_color must be cleared"
|
|
|
+ assert tray0["tray_sub_brands"] == "", "tray_sub_brands must be cleared"
|
|
|
+ assert tray0["tray_id_name"] == "", "tray_id_name must be cleared"
|
|
|
+ assert tray0["tray_info_idx"] == "", "tray_info_idx must be cleared"
|
|
|
+ assert tray0["tag_uid"] == "0000000000000000", "tag_uid must be cleared"
|
|
|
+ assert tray0["tray_uuid"] == "00000000000000000000000000000000", "tray_uuid must be cleared"
|
|
|
+ assert tray0["remain"] == 0, "remain must be 0"
|
|
|
+ assert tray0["k"] is None, "k must be cleared"
|
|
|
+ assert tray0["cali_idx"] is None, "cali_idx must be cleared"
|
|
|
+ assert tray0["state"] == 10, "state should be preserved"
|
|
|
+
|
|
|
+ # Tray 1 should be untouched
|
|
|
+ assert tray1["tray_type"] == "PLA", "Loaded slot must be preserved"
|
|
|
+ assert tray1["remain"] == 50
|
|
|
+
|
|
|
+ def test_state_9_clears_stale_tray_data(self, mqtt_client):
|
|
|
+ """Incremental update with state=9 (empty, no spool) clears tray."""
|
|
|
+ self._seed_loaded_tray(mqtt_client)
|
|
|
+
|
|
|
+ update = {
|
|
|
+ "ams": [
|
|
|
+ {
|
|
|
+ "id": 0,
|
|
|
+ "tray": [
|
|
|
+ {"id": 0, "state": 9},
|
|
|
+ {"id": 1, "state": 11},
|
|
|
+ ],
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ "power_on_flag": False,
|
|
|
+ }
|
|
|
+ mqtt_client._handle_ams_data(update)
|
|
|
+
|
|
|
+ tray0 = mqtt_client.state.raw_data["ams"][0]["tray"][0]
|
|
|
+ assert tray0["tray_type"] == "", "state=9 must clear tray_type"
|
|
|
+ assert tray0["remain"] == 0
|
|
|
+
|
|
|
+ def test_state_11_preserves_tray_data(self, mqtt_client):
|
|
|
+ """Incremental update with state=11 (loaded) must NOT clear tray."""
|
|
|
+ self._seed_loaded_tray(mqtt_client)
|
|
|
+
|
|
|
+ update = {
|
|
|
+ "ams": [
|
|
|
+ {
|
|
|
+ "id": 0,
|
|
|
+ "tray": [
|
|
|
+ {"id": 0, "state": 11},
|
|
|
+ {"id": 1, "state": 11},
|
|
|
+ ],
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ "power_on_flag": False,
|
|
|
+ }
|
|
|
+ mqtt_client._handle_ams_data(update)
|
|
|
+
|
|
|
+ tray0 = mqtt_client.state.raw_data["ams"][0]["tray"][0]
|
|
|
+ assert tray0["tray_type"] == "PETG", "state=11 must preserve tray data"
|
|
|
+ assert tray0["tray_color"] == "00FF00FF"
|
|
|
+ assert tray0["remain"] == 75
|
|
|
+
|
|
|
+ def test_no_clearing_when_tray_type_already_empty(self, mqtt_client):
|
|
|
+ """Don't re-clear a tray that's already empty (avoids log spam)."""
|
|
|
+ self._seed_loaded_tray(mqtt_client)
|
|
|
+
|
|
|
+ # First unload clears
|
|
|
+ update = {
|
|
|
+ "ams": [{"id": 0, "tray": [{"id": 0, "state": 10}, {"id": 1, "state": 11}]}],
|
|
|
+ "power_on_flag": False,
|
|
|
+ }
|
|
|
+ mqtt_client._handle_ams_data(update)
|
|
|
+ assert mqtt_client.state.raw_data["ams"][0]["tray"][0]["tray_type"] == ""
|
|
|
+
|
|
|
+ # Second identical update should not trigger clearing again
|
|
|
+ # (merged_tray.get("tray_type") is already empty/falsy)
|
|
|
+ mqtt_client._handle_ams_data(update)
|
|
|
+ assert mqtt_client.state.raw_data["ams"][0]["tray"][0]["tray_type"] == ""
|
|
|
+
|
|
|
+ def test_reload_after_unload_restores_data(self, mqtt_client):
|
|
|
+ """After clearing via state=10, a full update with state=11 restores data."""
|
|
|
+ self._seed_loaded_tray(mqtt_client)
|
|
|
+
|
|
|
+ # Unload
|
|
|
+ mqtt_client._handle_ams_data(
|
|
|
+ {
|
|
|
+ "ams": [{"id": 0, "tray": [{"id": 0, "state": 10}, {"id": 1, "state": 11}]}],
|
|
|
+ "power_on_flag": False,
|
|
|
+ }
|
|
|
+ )
|
|
|
+ assert mqtt_client.state.raw_data["ams"][0]["tray"][0]["tray_type"] == ""
|
|
|
+
|
|
|
+ # Reload — full tray data arrives again
|
|
|
+ mqtt_client._handle_ams_data(
|
|
|
+ {
|
|
|
+ "ams": [
|
|
|
+ {
|
|
|
+ "id": 0,
|
|
|
+ "tray": [
|
|
|
+ {
|
|
|
+ "id": 0,
|
|
|
+ "tray_type": "PETG",
|
|
|
+ "tray_sub_brands": "PETG HF",
|
|
|
+ "tray_color": "00FF00FF",
|
|
|
+ "remain": 75,
|
|
|
+ "state": 11,
|
|
|
+ },
|
|
|
+ {"id": 1, "state": 11},
|
|
|
+ ],
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ "power_on_flag": False,
|
|
|
+ }
|
|
|
+ )
|
|
|
+ tray0 = mqtt_client.state.raw_data["ams"][0]["tray"][0]
|
|
|
+ assert tray0["tray_type"] == "PETG", "Reload must restore tray data"
|
|
|
+ assert tray0["tray_color"] == "00FF00FF"
|
|
|
+ assert tray0["remain"] == 75
|
|
|
+
|
|
|
+
|
|
|
class TestNozzleRackData:
|
|
|
"""Tests for nozzle rack data parsing from H2 series device.nozzle.info."""
|
|
|
|