fix(inventory): emulate state=9 for bare-tray empty-slot signal on P1S/A1 (#1322)

Follow-up to the #1322 root fix. Reporter @RosdasHH traced the raw MQTT
  payload and found that P1S and A1 Mini send only {"id": N} for a
  physically empty slot — no state, no tray_type, no other fields. Without
  that signal, the assign-spool path was firing one wasted MQTT publish per
  click on a truly-empty slot (firmware dropped it silently, but still).

  The AMS parser in printer_manager.py now detects the bare-tray shape and
  promotes it to state=9 — the firmware's explicit "no spool" code — which
  lets the existing state in {9, 10} short-circuit in the inventory route
  apply automatically.

  The detection is intentionally narrow:

      len(tray) == 1 and "id" in tray and state is None

  so the post-Reset-Slot A1 Mini BMCU case (populated payload with state=3
  and tray_type="") has more than one key and stays unaffected — the #1322
  root fix is preserved.
This commit is contained in:
maziggy
2026-05-15 12:27:01 +02:00
parent bd552d5775
commit 7d3af9834c
3 changed files with 69 additions and 2 deletions
+1 -1
View File
File diff suppressed because one or more lines are too long
+14 -1
View File
@@ -768,6 +768,19 @@ def printer_state_to_dict(state: PrinterState, printer_id: int | None = None, mo
if k_value is None and cali_idx is not None and cali_idx in kprofile_map:
k_value = kprofile_map[cali_idx]
# P1S / A1 Mini physically-empty-slot signal (#1322 follow-up by
# @RosdasHH): for a truly empty slot the firmware sends only
# {"id": N} — no state, no tray_type, no anything else. Treat
# that as the firmware's "no spool" indicator (state=9) so the
# assign-spool path in inventory.py can short-circuit a MQTT
# publish the firmware would silently drop anyway. The
# post-"Reset Slot" A1 Mini BMCU case sends a populated payload
# (state=3, tray_type="") — different shape, doesn't match this
# guard, still attempts the MQTT push per the #1322 fix.
state_val = tray.get("state")
if state_val is None and len(tray) == 1 and "id" in tray:
state_val = 9
trays.append(
{
"id": int(tray.get("id", 0)),
@@ -785,7 +798,7 @@ def printer_state_to_dict(state: PrinterState, printer_id: int | None = None, mo
"nozzle_temp_max": tray.get("nozzle_temp_max"),
"drying_temp": tray.get("drying_temp"),
"drying_time": tray.get("drying_time"),
"state": tray.get("state"),
"state": state_val,
}
)
# Prefer humidity_raw (actual percentage) over humidity (index 1-5)
@@ -741,6 +741,60 @@ class TestPrinterStateToDict:
assert result["ams"][0]["tray"][0]["tag_uid"] is None
assert result["ams"][0]["tray"][0]["tray_uuid"] is None
def test_bare_tray_emulates_state_9(self, mock_state):
"""P1S / A1 Mini physically-empty-slot signal (#1322 follow-up by @RosdasHH):
the firmware sends only `{"id": N}` for a truly empty slot. Treat that as
the firmware's "no spool" state (state=9) so the inventory assign-spool
path can short-circuit the doomed MQTT publish.
"""
mock_state.raw_data = {
"ams": [
{
"id": 0,
"tray": [
{"id": 0, "state": 11, "tray_type": "PLA"}, # loaded slot
{"id": 1}, # P1S empty-slot signal — only id
],
}
]
}
result = printer_state_to_dict(mock_state)
trays = result["ams"][0]["tray"]
assert trays[0]["state"] == 11, "loaded slot keeps its firmware state"
assert trays[1]["state"] == 9, "bare {id} tray must be promoted to state=9"
def test_populated_payload_with_empty_state_3_is_not_promoted(self, mock_state):
"""A1 Mini BMCU / P1S Standard AMS post-Reset-Slot case (#1322 root):
firmware sends state=3 + tray_type="" but with the FULL field set
populated. Must NOT be confused with the bare-tray empty signal —
else inventory.py would short-circuit MQTT and we'd reintroduce the
deadlock the #1322 fix removed.
"""
mock_state.raw_data = {
"ams": [
{
"id": 0,
"tray": [
{
"id": 0,
"state": 3,
"tray_type": "", # cleared
"tray_color": "",
"tag_uid": "0000000000000000",
"remain": 0,
}
],
}
]
}
result = printer_state_to_dict(mock_state)
# state stays at 3 — the bare-tray promotion requires the dict to have
# ONLY the id key, not just empty/falsy values for the other fields.
assert result["ams"][0]["tray"][0]["state"] == 3
def test_zero_tag_uid_becomes_none(self, mock_state):
"""Verify zero tag_uid is converted to None."""
mock_state.raw_data = {