|
|
@@ -822,7 +822,22 @@ async def on_printer_status_change(printer_id: int, state: PrinterState):
|
|
|
# WebSocket dedup / broadcast logic below, and the connected edge is
|
|
|
# marked True BEFORE the await so concurrent status updates inside
|
|
|
# the same connection don't re-trigger reconciliation.
|
|
|
- if state.connected and not _printer_reconciled_since_connect.get(printer_id, False):
|
|
|
+ #
|
|
|
+ # Wait for a real push_status before reconciling (#1679): MQTT
|
|
|
+ # `_on_connect` broadcasts `state` IMMEDIATELY after the broker accepts
|
|
|
+ # the connection, BEFORE `_request_push_all` round-trips. At that
|
|
|
+ # instant the `PrinterState` is still on construction defaults — most
|
|
|
+ # importantly `state.state == "unknown"` and `state.subtask_name == ""`.
|
|
|
+ # If reconcile spawns here, every in-flight archive falls through to
|
|
|
+ # the empty-subtask_name trigger and gets synthesised `aborted`, which
|
|
|
+ # creates a duplicate archive on the real PRINT COMPLETE and
|
|
|
+ # double-counts filament. Gating on `state.state ∉ ("", "unknown")`
|
|
|
+ # keeps the #1542 mechanism intact: once the first real push_status
|
|
|
+ # updates `state.state` (RUNNING / IDLE / FINISH / …), this handler
|
|
|
+ # fires again with the flag still False — reconcile then runs against
|
|
|
+ # actual evidence.
|
|
|
+ state_known = bool(state.state) and state.state.upper() not in ("", "UNKNOWN")
|
|
|
+ if state.connected and state_known and not _printer_reconciled_since_connect.get(printer_id, False):
|
|
|
_printer_reconciled_since_connect[printer_id] = True
|
|
|
spawn_background_task(
|
|
|
reconcile_stale_active_prints(printer_id),
|
|
|
@@ -3371,11 +3386,28 @@ def _is_active_archive_stale(archive, state) -> tuple[bool, str]:
|
|
|
|
|
|
Conservative on purpose: PAUSE / PREPARE / SLICING and any RUNNING state
|
|
|
with matching subtask_id+subtask_name is left alone. The cost of a false
|
|
|
- positive is a single misreported "aborted" status that the next real
|
|
|
- PRINT COMPLETE would have overwritten with the correct status anyway.
|
|
|
- The cost of a false negative is the ghost-print loop in #1542.
|
|
|
+ positive is a duplicate archive on the next real PRINT COMPLETE — the
|
|
|
+ reactive handler uses ``_active_prints`` for lookup, which the reconcile
|
|
|
+ clears on synthesis, so the real completion creates a fresh row instead
|
|
|
+ of overwriting the synthesised one (#1679). The cost of a false negative
|
|
|
+ is the ghost-print loop in #1542.
|
|
|
+
|
|
|
+ Pre-push guard (#1679): when ``state.state`` is empty or ``"unknown"``,
|
|
|
+ MQTT has connected but the first ``push_status`` response hasn't been
|
|
|
+ applied yet — ``PrinterState`` is sitting on its construction defaults.
|
|
|
+ The reconcile caller in ``on_printer_status_change`` is already gated
|
|
|
+ on a real ``state.state``, so in normal operation this branch is
|
|
|
+ unreachable; it's kept as belt-and-braces for future callers and for
|
|
|
+ the narrow window where a partial state update could arrive
|
|
|
+ (``state.state`` set but ``subtask_name`` not yet populated). Returning
|
|
|
+ ``not stale`` on degenerate input is strictly conservative: a real
|
|
|
+ stale archive will still be caught by the next push_status arriving
|
|
|
+ with terminal state.
|
|
|
"""
|
|
|
current_state = (state.state or "").upper()
|
|
|
+ if current_state in ("", "UNKNOWN"):
|
|
|
+ # No real push_status yet — PrinterState defaults are not evidence.
|
|
|
+ return False, ""
|
|
|
if current_state in ("IDLE", "FINISH", "FAILED"):
|
|
|
return True, f"printer state {current_state}"
|
|
|
# Below here the printer is in a running / pre-running state (RUNNING /
|