mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-21 10:15:04 +02:00
fix(notifications): wire on_printer_offline dispatch on disconnect edge (#1752)
The provider toggle, schema, template, and NotificationService.on_printer_offline
all shipped, but no caller invoked the dispatcher — the offline event was an
orphan toggle. Edge detection in on_printer_status_change now schedules a
debounced (60s) background task on the connected→disconnected transition;
reconnect before the window elapses cancels it. Covers both upstream paths
(smart-plug power-off via mark_printer_offline, and MQTT staleness via
check_staleness), both of which already route through the status callback.
The "back online" channel is the existing print-failure notification on
firmware FAILED report — no symmetric on_printer_online needed.
This commit is contained in:
@@ -395,6 +395,22 @@ _bed_cool_waiters: dict[int, dict] = {}
|
||||
# as "cancelled" (stopped by user) so the correct notification email is sent.
|
||||
_user_stopped_printers: set[int] = set()
|
||||
|
||||
# Offline-notification edge state (#1752): fire `on_printer_offline` exactly
|
||||
# once when a printer transitions connected → disconnected. `_printer_last_connected`
|
||||
# holds the previous observation so we only fire on the True → False edge (a
|
||||
# False → False repeat doesn't notify; an initial False at startup doesn't
|
||||
# notify either, since there's no prior True). `_printer_offline_notify_tasks`
|
||||
# holds the per-printer pending asyncio task that fires the notification
|
||||
# after a debounce window — cancelled if the printer reconnects before the
|
||||
# window elapses, so transient MQTT blips don't flood the user.
|
||||
_printer_last_connected: dict[int, bool] = {}
|
||||
_printer_offline_notify_tasks: dict[int, asyncio.Task] = {}
|
||||
# Debounce: a printer must stay offline this long before we notify. Sized
|
||||
# against the staleness path (`bambu_mqtt.py::STALE_RECONNECT_COOLDOWN = 30s`)
|
||||
# so a single stale-trigger cooldown isn't enough to fire — only a real
|
||||
# offline that survives one reconnect attempt notifies.
|
||||
_PRINTER_OFFLINE_NOTIFY_DEBOUNCE_SECONDS = 60.0
|
||||
|
||||
|
||||
# HMS short-code → human-readable failure reason. Used by _dispatch_archive_update
|
||||
# when status="failed" to label the print's failure_reason in archives.
|
||||
@@ -843,6 +859,53 @@ _last_status_broadcast: dict[int, str] = {}
|
||||
_nozzle_count_updated: set[int] = set()
|
||||
|
||||
|
||||
async def _maybe_notify_printer_offline(printer_id: int) -> None:
|
||||
"""Wait the debounce window then fire `on_printer_offline` if the printer
|
||||
is still offline.
|
||||
|
||||
Scheduled by `on_printer_status_change` on the connected → disconnected
|
||||
edge (#1752). Cancelled by the same handler if the printer reconnects
|
||||
before the window elapses, so a single MQTT blip + recovery doesn't
|
||||
notify. Both the staleness-detector path (`bambu_mqtt.py::check_staleness`)
|
||||
and the smart-plug power-off path (`printer_manager.mark_printer_offline`)
|
||||
route through the same status-change callback, so this covers both.
|
||||
"""
|
||||
logger = logging.getLogger(__name__)
|
||||
try:
|
||||
await asyncio.sleep(_PRINTER_OFFLINE_NOTIFY_DEBOUNCE_SECONDS)
|
||||
still_offline = not printer_manager.is_connected(printer_id)
|
||||
logger.info(
|
||||
"[#1752] Printer %s offline debounce elapsed: still_offline=%s",
|
||||
printer_id,
|
||||
still_offline,
|
||||
)
|
||||
if not still_offline:
|
||||
return
|
||||
async with async_session() as db:
|
||||
from backend.app.models.printer import Printer
|
||||
|
||||
result = await db.execute(select(Printer).where(Printer.id == printer_id))
|
||||
printer = result.scalar_one_or_none()
|
||||
if not printer:
|
||||
logger.warning(
|
||||
"[#1752] Printer %s missing from DB at offline-notify time; skipping",
|
||||
printer_id,
|
||||
)
|
||||
return
|
||||
logger.info(
|
||||
"[#1752] Dispatching on_printer_offline for printer %s (%s)",
|
||||
printer_id,
|
||||
printer.name,
|
||||
)
|
||||
await notification_service.on_printer_offline(printer_id, printer.name, db)
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.warning("Printer offline notification failed for printer %s: %s", printer_id, e)
|
||||
finally:
|
||||
_printer_offline_notify_tasks.pop(printer_id, None)
|
||||
|
||||
|
||||
async def on_printer_status_change(printer_id: int, state: PrinterState):
|
||||
"""Handle printer status changes - broadcast via WebSocket."""
|
||||
# Connected-edge reconciliation (#1542 follow-up). When the printer
|
||||
@@ -883,6 +946,34 @@ async def on_printer_status_change(printer_id: int, state: PrinterState):
|
||||
# Re-arm so the next reconnect triggers reconciliation again.
|
||||
_printer_reconciled_since_connect[printer_id] = False
|
||||
|
||||
# Offline-notification edge (#1752): schedule `on_printer_offline` on
|
||||
# connected → disconnected. The "back online" channel is already covered
|
||||
# by the print-failure notification (firmware reports gcode_state=FAILED
|
||||
# on reconnect of an interrupted print), so we don't add a symmetric
|
||||
# online event here.
|
||||
prev_connected = _printer_last_connected.get(printer_id)
|
||||
_printer_last_connected[printer_id] = state.connected
|
||||
if prev_connected is True and not state.connected:
|
||||
existing = _printer_offline_notify_tasks.get(printer_id)
|
||||
if existing is None or existing.done():
|
||||
logging.getLogger(__name__).info(
|
||||
"[#1752] Printer %s connected→disconnected edge; scheduling offline notification in %.0fs",
|
||||
printer_id,
|
||||
_PRINTER_OFFLINE_NOTIFY_DEBOUNCE_SECONDS,
|
||||
)
|
||||
_printer_offline_notify_tasks[printer_id] = asyncio.create_task(
|
||||
_maybe_notify_printer_offline(printer_id),
|
||||
name=f"printer-offline-notify-{printer_id}",
|
||||
)
|
||||
elif state.connected:
|
||||
pending = _printer_offline_notify_tasks.pop(printer_id, None)
|
||||
if pending is not None and not pending.done():
|
||||
logging.getLogger(__name__).info(
|
||||
"[#1752] Printer %s reconnected before debounce; cancelling pending offline notification",
|
||||
printer_id,
|
||||
)
|
||||
pending.cancel()
|
||||
|
||||
# Only broadcast if something meaningful changed (reduce WebSocket spam)
|
||||
# Include rounded temperatures to detect meaningful temp changes (within 1 degree)
|
||||
temps = state.temperatures or {}
|
||||
|
||||
Reference in New Issue
Block a user