Fix bed cooled notification never triggering (#497)

The bed cooldown monitor was defined at the end of on_print_complete,
after an early return that exits when no archive is found. Prints
started from BambuStudio or the printer's touchscreen have no archive,
so the function returned before the bed cooldown task was ever created.

Moved the bed cooldown block (function def + task creation) to before
the archive_id early-return so it fires for all completed prints.
Also hardened the temperature dict check from truthiness to isinstance.
This commit is contained in:
maziggy
2026-02-23 13:57:58 +01:00
parent 24c4363620
commit faff453dec
3 changed files with 89 additions and 80 deletions
+1
View File
@@ -5,6 +5,7 @@ All notable changes to Bambuddy will be documented in this file.
## [0.2.1b3] - Unreleased
### Fixed
- **Print Bed Cooled Notification Never Triggers** ([#497](https://github.com/maziggy/bambuddy/issues/497)) — The bed cooldown monitor (which polls bed temperature after a print and sends a notification when it drops below the configured threshold) was defined at the end of the `on_print_complete` callback, after an early `return` that exits when no archive is found for the print. Prints started from BambuStudio or the printer's touchscreen typically have no archive in Bambuddy, so the function returned before the bed cooldown task was ever created. Moved the bed cooldown monitor to before the archive lookup early-return so it fires for all completed prints regardless of archive state. Also hardened the temperature dict check from truthiness (`if status.temperatures:`) to type check (`isinstance(status.temperatures, dict)`) to avoid false negatives on empty dicts.
- **IP Addresses Not Redacted From Support Bundle Logs** — The `_sanitize_log_content()` function redacted emails, serials, and credentials but left raw IPv4 addresses in log output. Now adds known printer IPs to the sensitive string list for exact matching, and applies an IPv4 regex that replaces addresses with `[IP]` while preserving firmware version strings (which use leading-zero octets like `01.09.01.00`). Updated the system info page privacy disclaimer to list IP addresses as redacted.
- **"Unknown stage (74)" on H2D During Print Preparation** — The H2D firmware reports `stg_cur=74` during print preparation, but this stage was not in the stage name lookup table (which went up to 66, sourced from BambuStudio). Now maps stage 74 to "Preparing". Also added stage 77 ("Preparing AMS") which was present in BambuStudio but missing from the lookup.
- **Wrong Documentation Link for "Lubricate Carbon Rods" on P2S** ([#490](https://github.com/maziggy/bambuddy/issues/490)) — The "Lubricate Carbon Rods" maintenance task linked to the belt tension wiki page instead of the XYZ axis lubrication page for P2S printers.
+82 -80
View File
@@ -2282,6 +2282,88 @@ async def on_print_complete(printer_id: int, data: dict):
log_timing("Queue item update")
# Start bed cooldown monitor (polls bed temp until it drops below threshold)
# Must run before archive_id early-return so it fires for all prints (including
# prints started from BambuStudio/touchscreen that have no archive).
async def _background_bed_cooldown():
"""Monitor bed temperature after print and notify when cooled."""
try:
from backend.app.api.routes.settings import get_setting
# Check threshold setting
async with async_session() as db:
threshold_str = await get_setting(db, "bed_cooled_threshold")
threshold = float(threshold_str) if threshold_str else 35.0
# Check if any provider has on_bed_cooled enabled (early exit if none)
async with async_session() as db:
providers = await notification_service._get_providers_for_event(db, "on_bed_cooled", printer_id)
if not providers:
logger.debug("[BED-COOL] No providers enabled for bed_cooled on printer %s", printer_id)
return
logger.info("[BED-COOL] Monitoring bed temp for printer %s (threshold: %.0f°C)", printer_id, threshold)
max_polls = 120 # 120 * 15s = 30 min timeout
for _ in range(max_polls):
await asyncio.sleep(15)
# Check if printer is still connected
status = printer_manager.get_status(printer_id)
if status is None:
logger.info("[BED-COOL] Printer %s disconnected, stopping monitor", printer_id)
return
# Check if a new print started (state == RUNNING)
if hasattr(status, "state") and status.state == "RUNNING":
logger.info("[BED-COOL] New print started on printer %s, stopping monitor", printer_id)
return
# Get bed temperature
bed_temp = None
if hasattr(status, "temperatures") and isinstance(status.temperatures, dict):
bed_temp = status.temperatures.get("bed")
if bed_temp is None:
continue
if bed_temp <= threshold:
logger.info(
"[BED-COOL] Bed cooled to %.1f°C on printer %s (threshold: %.0f°C)",
bed_temp,
printer_id,
threshold,
)
printer_info = printer_manager.get_printer(printer_id)
p_name = printer_info.name if printer_info else "Unknown"
async with async_session() as db:
await notification_service.on_bed_cooled(
printer_id=printer_id,
printer_name=p_name,
bed_temp=bed_temp,
threshold=threshold,
filename=filename or subtask_name or "",
db=db,
)
return
logger.info("[BED-COOL] Timeout waiting for bed to cool on printer %s", printer_id)
except asyncio.CancelledError:
logger.info("[BED-COOL] Bed cooldown monitor cancelled for printer %s", printer_id)
except Exception as e:
logger.warning("[BED-COOL] Failed: %s", e)
finally:
_bed_cooldown_tasks.pop(printer_id, None)
# Only start bed cooldown for completed prints
if data.get("status") == "completed":
# Cancel any existing task for this printer
existing_task = _bed_cooldown_tasks.pop(printer_id, None)
if existing_task and not existing_task.done():
existing_task.cancel()
task = asyncio.create_task(_background_bed_cooldown())
_bed_cooldown_tasks[printer_id] = task
if not archive_id:
logger.warning("Could not find archive for print complete: filename=%s, subtask=%s", filename, subtask_name)
return
@@ -2791,86 +2873,6 @@ async def on_print_complete(printer_id: int, data: dict):
asyncio.create_task(_background_layer_timelapse())
# Start bed cooldown monitor (polls bed temp until it drops below threshold)
async def _background_bed_cooldown():
"""Monitor bed temperature after print and notify when cooled."""
try:
from backend.app.api.routes.settings import get_setting
# Check threshold setting
async with async_session() as db:
threshold_str = await get_setting(db, "bed_cooled_threshold")
threshold = float(threshold_str) if threshold_str else 35.0
# Check if any provider has on_bed_cooled enabled (early exit if none)
async with async_session() as db:
providers = await notification_service._get_providers_for_event(db, "on_bed_cooled", printer_id)
if not providers:
logger.debug("[BED-COOL] No providers enabled for bed_cooled on printer %s", printer_id)
return
logger.info("[BED-COOL] Monitoring bed temp for printer %s (threshold: %.0f°C)", printer_id, threshold)
max_polls = 120 # 120 * 15s = 30 min timeout
for _ in range(max_polls):
await asyncio.sleep(15)
# Check if printer is still connected
status = printer_manager.get_status(printer_id)
if status is None:
logger.info("[BED-COOL] Printer %s disconnected, stopping monitor", printer_id)
return
# Check if a new print started (state == RUNNING)
if hasattr(status, "state") and status.state == "RUNNING":
logger.info("[BED-COOL] New print started on printer %s, stopping monitor", printer_id)
return
# Get bed temperature
bed_temp = None
if hasattr(status, "temperatures") and status.temperatures:
bed_temp = status.temperatures.get("bed")
if bed_temp is None:
continue
if bed_temp <= threshold:
logger.info(
"[BED-COOL] Bed cooled to %.1f°C on printer %s (threshold: %.0f°C)",
bed_temp,
printer_id,
threshold,
)
printer_info = printer_manager.get_printer(printer_id)
p_name = printer_info.name if printer_info else "Unknown"
async with async_session() as db:
await notification_service.on_bed_cooled(
printer_id=printer_id,
printer_name=p_name,
bed_temp=bed_temp,
threshold=threshold,
filename=filename or subtask_name or "",
db=db,
)
return
logger.info("[BED-COOL] Timeout waiting for bed to cool on printer %s", printer_id)
except asyncio.CancelledError:
logger.info("[BED-COOL] Bed cooldown monitor cancelled for printer %s", printer_id)
except Exception as e:
logger.warning("[BED-COOL] Failed: %s", e)
finally:
_bed_cooldown_tasks.pop(printer_id, None)
# Only start bed cooldown for completed prints
if data.get("status") == "completed":
# Cancel any existing task for this printer
existing_task = _bed_cooldown_tasks.pop(printer_id, None)
if existing_task and not existing_task.done():
existing_task.cancel()
task = asyncio.create_task(_background_bed_cooldown())
_bed_cooldown_tasks[printer_id] = task
log_timing("All background tasks scheduled")
# Auto-scan for timelapse if recording was active during the print
+6
View File
@@ -50,6 +50,8 @@ def event_loop():
"""Create an instance of the default event loop for each test session."""
loop = asyncio.get_event_loop_policy().new_event_loop()
yield loop
# Drain pending callbacks so aiosqlite threads can finish before loop closes
loop.run_until_complete(asyncio.sleep(0.05))
loop.close()
@@ -89,6 +91,10 @@ async def test_engine():
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await engine.dispose()
# Allow aiosqlite's background thread to finish processing the close
# response before the per-function event loop shuts down, preventing
# "RuntimeError: Event loop is closed" in call_soon_threadsafe.
await asyncio.sleep(0.01)
@pytest.fixture