mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 11:12:35 +02:00
An H2D armed five 12-hour drying cycles inside four hours, one of them six seconds after the previous one ended, and none ran more than a couple of hours. Two things combine. The firmware ends a cycle when it decides the filament is dry rather than when the clock runs out, and reports no fault doing it -- across this printer's history the run length tracks how wet the spools were, from nearly the full 12 hours starting at 32% down to minutes once the unit sat at 10-13%. That part is the AMS doing its job. The loop is ours. An AMS reports higher relative humidity while it is warm than once it has cooled: the same unit read 10-13% cold and 15-20% through every cycle. With the threshold at 14% the reading at the moment a cycle ended was always still above it, so the next 30-second pass armed another 12-hour cycle. Nothing counted, nothing waited, and it only stopped when the box finally cooled enough to read 13%. Auto-drying now waits 30 minutes after a cycle ends before arming another on the same unit, and gives up on a unit after two consecutive cycles that bring the reading no lower -- logging why and sending a new notification, on by default because it reports that Bambuddy has stopped acting. Progress is judged against the lowest reading any cycle on that unit has ended at, not against the threshold, so a genuinely wet spool in a humid room coming down 40-37-35 keeps drying however far it still is from the target; comparing against the best so far rather than the previous end stops a sensor wobbling by one point reading as progress every other cycle. The suspension lifts by itself once the reading falls below the threshold. Neither guard can stop a running cycle, and a cycle Bambuddy cut short for a print, or that the user stopped by hand, is not counted against the unit -- so a farm that dries between queue jobs is unaffected. The threshold field now warns below 20%, and every cycle end logs the unit's temperature and humidity, which is what made this diagnosable. The same bundle showed unrelated tasks failing with "database is locked", each inside a 30.000-second Discord connect timeout. Alarms are raised from inside the loop that records sensor history, at a point where the new rows are added but not committed; the first read in the notification path flushed them to satisfy itself, opening a write transaction, and the provider was then contacted over the network with that transaction still open. SQLite allows one writer and 30 seconds outlives the 15-second busy timeout, so every other write in that window failed. The two reads that run before a provider is contacted no longer flush the caller's pending work, and the connect timeout is 5 seconds rather than 30 -- the body keeps the full 30, so image uploads on a slow uplink are unaffected. SQLite only; Postgres has no single-writer limit.
256 lines
10 KiB
Python
256 lines
10 KiB
Python
"""Notification template model for customizable notification messages."""
|
|
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import Boolean, DateTime, Integer, String, Text, func
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from backend.app.core.database import Base
|
|
|
|
|
|
class NotificationTemplate(Base):
|
|
"""Model for notification message templates."""
|
|
|
|
__tablename__ = "notification_templates"
|
|
|
|
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
|
event_type: Mapped[str] = mapped_column(String(50), nullable=False, unique=True)
|
|
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
|
title_template: Mapped[str] = mapped_column(Text, nullable=False)
|
|
body_template: Mapped[str] = mapped_column(Text, nullable=False)
|
|
is_default: Mapped[bool] = mapped_column(Boolean, default=True)
|
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
|
updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), onupdate=func.now())
|
|
|
|
|
|
# Default templates for seeding
|
|
DEFAULT_TEMPLATES = [
|
|
{
|
|
"event_type": "print_start",
|
|
"name": "Print Started",
|
|
"title_template": "Print Started",
|
|
"body_template": "{printer}: {filename}\nEstimated: {estimated_time}",
|
|
},
|
|
{
|
|
"event_type": "print_complete",
|
|
"name": "Print Completed",
|
|
"title_template": "Print Completed",
|
|
"body_template": "{printer}: {filename}\nTime: {duration}\nFilament: {filament_grams}g",
|
|
},
|
|
{
|
|
"event_type": "print_failed",
|
|
"name": "Print Failed",
|
|
"title_template": "Print Failed",
|
|
"body_template": "{printer}: {filename}\nTime: {duration}\nReason: {reason}",
|
|
},
|
|
{
|
|
"event_type": "print_stopped",
|
|
"name": "Print Stopped",
|
|
"title_template": "Print Stopped",
|
|
"body_template": "{printer}: {filename}\nTime: {duration}",
|
|
},
|
|
{
|
|
"event_type": "print_progress",
|
|
"name": "Print Progress",
|
|
"title_template": "Print {progress}% Complete",
|
|
"body_template": "{printer}: {filename}\nRemaining: {remaining_time}",
|
|
},
|
|
{
|
|
"event_type": "print_missing_spool_assignment",
|
|
"name": "Missing Spool Assignment",
|
|
"title_template": "Missing Spool Assignment",
|
|
"body_template": "{printer}: print started with missing spool assignments\nSlots: {missing_slots}\nExpected profile:\n{missing_slot_details}",
|
|
},
|
|
{
|
|
"event_type": "billing_charge_failed",
|
|
"name": "Billing Charge Failed",
|
|
"title_template": "Billing Charge Failed",
|
|
"body_template": "{printer}: {filename}\nThe print charge could not be recorded. The budget reservation was retained.\nArchive: {archive_id}",
|
|
},
|
|
{
|
|
"event_type": "printer_offline",
|
|
"name": "Printer Offline",
|
|
"title_template": "Printer Offline",
|
|
"body_template": "{printer} has disconnected",
|
|
},
|
|
{
|
|
"event_type": "printer_error",
|
|
"name": "Printer Error",
|
|
"title_template": "Printer Error: {error_type}",
|
|
"body_template": "{printer}\n{error_detail}",
|
|
},
|
|
{
|
|
"event_type": "ai_failure_detection",
|
|
"name": "AI Failure Detection",
|
|
"title_template": "Possible Print Failure Detected",
|
|
"body_template": "{printer}: {task_name}\nConfidence: {confidence}\nAction taken: {action}",
|
|
},
|
|
{
|
|
"event_type": "plate_not_empty",
|
|
"name": "Plate Not Empty",
|
|
"title_template": "Plate Not Empty - Print Paused",
|
|
"body_template": "{printer}: Objects detected on build plate. Print has been paused. Clear plate and resume.",
|
|
},
|
|
{
|
|
"event_type": "plate_clear_required",
|
|
"name": "Plate Clear Required",
|
|
"title_template": "Plate Clear Required",
|
|
"body_template": "{printer}: print finished. Confirm the build plate is clear before the queue continues.",
|
|
},
|
|
{
|
|
"event_type": "filament_low",
|
|
"name": "Filament Low",
|
|
"title_template": "Filament Low",
|
|
"body_template": "{printer}: Slot {slot} at {remaining_percent}%",
|
|
},
|
|
{
|
|
"event_type": "maintenance_due",
|
|
"name": "Maintenance Due",
|
|
"title_template": "Maintenance Due",
|
|
"body_template": "{printer}:\n{items}",
|
|
},
|
|
{
|
|
"event_type": "ams_humidity_high",
|
|
"name": "AMS Humidity High",
|
|
"title_template": "AMS Humidity Alert",
|
|
"body_template": "{printer} {ams_label}: Humidity {humidity}% exceeds {threshold}% threshold",
|
|
},
|
|
{
|
|
"event_type": "ams_temperature_high",
|
|
"name": "AMS Temperature High",
|
|
"title_template": "AMS Temperature Alert",
|
|
"body_template": "{printer} {ams_label}: Temperature {temperature}°C exceeds {threshold}°C threshold",
|
|
},
|
|
{
|
|
"event_type": "ams_drying_suspended",
|
|
"name": "Auto-Drying Suspended",
|
|
"title_template": "Auto-Drying Suspended",
|
|
"body_template": (
|
|
"{printer} {ams_label}: stopped automatic drying after {cycles} cycles left humidity at "
|
|
"{humidity}%, still above the {threshold}% threshold. An AMS reads higher while it is warm, "
|
|
"so raise the threshold or dry the spools off the printer."
|
|
),
|
|
},
|
|
{
|
|
"event_type": "bed_cooled",
|
|
"name": "Bed Cooled",
|
|
"title_template": "Bed Cooled",
|
|
"body_template": "{printer}: Bed cooled to {bed_temp}°C (threshold: {threshold}°C)",
|
|
},
|
|
{
|
|
"event_type": "ha_sensor_alert",
|
|
"name": "Home Assistant Sensor Alert",
|
|
"title_template": "Sensor Alert",
|
|
"body_template": "{printer}: {sensor} is {state}",
|
|
},
|
|
{
|
|
"event_type": "first_layer_complete",
|
|
"name": "First Layer Complete",
|
|
"title_template": "First Layer Complete",
|
|
"body_template": "{printer}: {filename}\nLayer 1/{total_layers} done",
|
|
},
|
|
{
|
|
"event_type": "test",
|
|
"name": "Test Notification",
|
|
"title_template": "Bambuddy Test",
|
|
"body_template": "This is a test notification. If you see this, notifications are working!",
|
|
},
|
|
# Queue notifications
|
|
{
|
|
"event_type": "queue_job_added",
|
|
"name": "Queue Job Added",
|
|
"title_template": "Job Queued",
|
|
"body_template": "{job_name} added to queue for {target}",
|
|
},
|
|
{
|
|
"event_type": "queue_job_assigned",
|
|
"name": "Queue Job Assigned",
|
|
"title_template": "Job Assigned",
|
|
"body_template": "{job_name} assigned to {printer} (from Any {target_model} queue)",
|
|
},
|
|
{
|
|
"event_type": "queue_job_started",
|
|
"name": "Queue Job Started",
|
|
"title_template": "Queue Job Started",
|
|
"body_template": "{printer}: {job_name}\nEstimated: {estimated_time}",
|
|
},
|
|
{
|
|
"event_type": "queue_job_waiting",
|
|
"name": "Queue Job Waiting",
|
|
"title_template": "Queue Job Waiting",
|
|
"body_template": "{job_name} waiting for {target_model}\n{waiting_reason}",
|
|
},
|
|
{
|
|
"event_type": "queue_job_skipped",
|
|
"name": "Queue Job Skipped",
|
|
"title_template": "Job Skipped",
|
|
"body_template": "{printer}: {job_name}\nReason: {reason}",
|
|
},
|
|
{
|
|
"event_type": "queue_job_failed",
|
|
"name": "Queue Job Failed",
|
|
"title_template": "Job Failed to Start",
|
|
"body_template": "{printer}: {job_name}\nReason: {reason}",
|
|
},
|
|
{
|
|
"event_type": "queue_completed",
|
|
"name": "Queue Completed",
|
|
"title_template": "Queue Complete",
|
|
"body_template": "All {completed_count} queued jobs have finished",
|
|
},
|
|
{
|
|
"event_type": "user_created",
|
|
"name": "Welcome Email",
|
|
"title_template": "Welcome to {app_name}",
|
|
"body_template": "Welcome {username}!\n\nYour account has been created.\nUsername: {username}\nPassword: {password}\n\nLogin at: {login_url}",
|
|
},
|
|
{
|
|
"event_type": "password_reset",
|
|
"name": "Password Reset",
|
|
"title_template": "{app_name} - Password Reset",
|
|
"body_template": "Hello {username},\n\nYour password has been reset.\nNew Password: {password}\n\nLogin at: {login_url}",
|
|
},
|
|
# Inventory stock alert templates
|
|
{
|
|
"event_type": "stock_reorder_alert",
|
|
"name": "Stock Reorder Alert",
|
|
"title_template": "Reorder Alert: {material}",
|
|
"body_template": "{material} ({brand}) has reached the reorder point.\nStock: {stock_g}g | Rate: {rate_g_day}g/day | Days left: {days_left}d\nReorder now to avoid a stock break.",
|
|
},
|
|
{
|
|
"event_type": "stock_break_alert",
|
|
"name": "Stock Break Alert",
|
|
"title_template": "Stock Break Risk: {material}",
|
|
"body_template": "{material} ({brand}) will run out before replenishment arrives.\nStock: {stock_g}g | Rate: {rate_g_day}g/day | Lead time: {lead_time_days}d\nOnly {days_left}d of stock remaining — order immediately.",
|
|
},
|
|
# User email notification templates (sent to the print job owner).
|
|
# Names include " Email" so they aren't confused with the provider-level
|
|
# `print_*` templates above, which share the same body shape but are
|
|
# broadcast to admin-configured providers (ntfy/pushover/telegram/discord/
|
|
# etc.) rather than mailed to a specific user.
|
|
{
|
|
"event_type": "user_print_start",
|
|
"name": "User Print Started Email",
|
|
"title_template": "Your Print Has Started",
|
|
"body_template": "Hello {username},\n\nYour print job has started on {printer}.\n\nFile: {filename}\n\nYou will be notified when it completes.",
|
|
},
|
|
{
|
|
"event_type": "user_print_complete",
|
|
"name": "User Print Completed Email",
|
|
"title_template": "Your Print Is Complete",
|
|
"body_template": "Hello {username},\n\nYour print job has completed on {printer}.\n\nFile: {filename}",
|
|
},
|
|
{
|
|
"event_type": "user_print_failed",
|
|
"name": "User Print Failed Email",
|
|
"title_template": "Your Print Has Failed",
|
|
"body_template": "Hello {username},\n\nYour print job has failed on {printer}.\n\nFile: {filename}",
|
|
},
|
|
{
|
|
"event_type": "user_print_stopped",
|
|
"name": "User Print Stopped Email",
|
|
"title_template": "Your Print Has Been Stopped",
|
|
"body_template": "Hello {username},\n\nYour print job was stopped on {printer}.\n\nFile: {filename}",
|
|
},
|
|
]
|