notification.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. """Notification provider and log models for push notifications."""
  2. from datetime import datetime
  3. from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, Text
  4. from sqlalchemy.orm import relationship
  5. from backend.app.core.database import Base
  6. class NotificationDigestQueue(Base):
  7. """Model for queuing notifications to be sent in daily digest."""
  8. __tablename__ = "notification_digest_queue"
  9. id = Column(Integer, primary_key=True, index=True)
  10. provider_id = Column(Integer, ForeignKey("notification_providers.id", ondelete="CASCADE"), nullable=False)
  11. event_type = Column(String(50), nullable=False) # print_start, print_complete, etc.
  12. title = Column(String(255), nullable=False)
  13. message = Column(Text, nullable=False)
  14. printer_id = Column(Integer, ForeignKey("printers.id", ondelete="SET NULL"), nullable=True)
  15. printer_name = Column(String(100), nullable=True)
  16. created_at = Column(DateTime, default=datetime.utcnow, index=True)
  17. # Relationships
  18. provider = relationship("NotificationProvider", back_populates="digest_queue")
  19. class NotificationLog(Base):
  20. """Model for logging sent notifications."""
  21. __tablename__ = "notification_logs"
  22. id = Column(Integer, primary_key=True, index=True)
  23. provider_id = Column(Integer, ForeignKey("notification_providers.id", ondelete="CASCADE"), nullable=False)
  24. event_type = Column(String(50), nullable=False) # print_start, print_complete, etc.
  25. title = Column(String(255), nullable=False)
  26. message = Column(Text, nullable=False)
  27. success = Column(Boolean, default=True)
  28. error_message = Column(Text, nullable=True)
  29. printer_id = Column(Integer, ForeignKey("printers.id", ondelete="SET NULL"), nullable=True)
  30. printer_name = Column(String(100), nullable=True) # Store name in case printer is deleted
  31. created_at = Column(DateTime, default=datetime.utcnow, index=True)
  32. # Relationships
  33. provider = relationship("NotificationProvider", back_populates="logs")
  34. class NotificationProvider(Base):
  35. """Model for notification providers (WhatsApp, ntfy, Pushover, etc.)."""
  36. __tablename__ = "notification_providers"
  37. id = Column(Integer, primary_key=True, index=True)
  38. name = Column(String(100), nullable=False) # User-defined name
  39. provider_type = Column(String(50), nullable=False) # callmebot, ntfy, pushover, telegram, email
  40. enabled = Column(Boolean, default=True)
  41. # Provider-specific configuration stored as JSON string
  42. config = Column(Text, nullable=False)
  43. # Event triggers - print lifecycle
  44. on_print_start = Column(Boolean, default=False)
  45. on_print_complete = Column(Boolean, default=True)
  46. on_print_failed = Column(Boolean, default=True)
  47. on_print_stopped = Column(Boolean, default=True) # User cancelled/stopped print
  48. on_print_progress = Column(Boolean, default=False) # 25%, 50%, 75% milestones
  49. on_print_missing_spool_assignment = Column(Boolean, default=False) # Print started with unassigned required tray(s)
  50. on_billing_charge_failed = Column(Boolean, default=True) # A completed/stopped print could not be charged
  51. # Event triggers - printer status
  52. on_printer_offline = Column(Boolean, default=False)
  53. on_printer_error = Column(Boolean, default=False) # AMS issues, etc.
  54. on_ai_failure_detection = Column(Boolean, default=False) # Obico spaghetti / failure detection (#1794)
  55. on_filament_low = Column(Boolean, default=False)
  56. on_maintenance_due = Column(Boolean, default=False) # Maintenance reminder
  57. # Event triggers - AMS environmental alarms (regular AMS with 4 slots)
  58. on_ams_humidity_high = Column(Boolean, default=False) # AMS humidity above threshold
  59. on_ams_temperature_high = Column(Boolean, default=False) # AMS temperature above threshold
  60. # Auto-drying gave up on a unit (#2770). Defaults True: it reports that
  61. # Bambuddy has stopped acting, which nothing else in the UI would say.
  62. on_ams_drying_suspended = Column(Boolean, default=True)
  63. # Event triggers - AMS-HT environmental alarms (single slot heated AMS)
  64. on_ams_ht_humidity_high = Column(Boolean, default=False) # AMS-HT humidity above threshold
  65. on_ams_ht_temperature_high = Column(Boolean, default=False) # AMS-HT temperature above threshold
  66. # Event triggers - Home Assistant sensors bound to a printer (#1148)
  67. on_ha_sensor_alert = Column(Boolean, default=False) # Bound HA sensor entered its alert state
  68. # Event triggers - Home Assistant sensors bound to a storage location (#2824)
  69. # Its own column rather than reusing on_ha_sensor_alert above: that one can
  70. # be scoped to a single printer, and a location alert has no printer to
  71. # scope by, so sharing it would leak drybox alerts to a provider narrowed
  72. # to one printer's sensors.
  73. on_location_ha_sensor_alert = Column(Boolean, default=False)
  74. # Event triggers - Build plate detection
  75. on_plate_not_empty = Column(Boolean, default=True) # Objects detected on plate before print
  76. # Off by default: fires after every print, alongside the print-complete alert (#2525)
  77. on_plate_clear_required = Column(Boolean, default=False) # Print ended, queue gated until plate is confirmed clear
  78. # Event triggers - Bed cooled after print
  79. on_bed_cooled = Column(Boolean, default=False) # Bed cooled below threshold after print
  80. on_first_layer_complete = Column(Boolean, default=False) # First layer finished printing
  81. # Event triggers - Inventory stock alerts
  82. on_stock_reorder_alert = Column(Boolean, default=False) # SKU hits reorder point
  83. on_stock_break_alert = Column(Boolean, default=False) # Stock will run out before replenishment
  84. # Event triggers - Print queue
  85. on_queue_job_added = Column(Boolean, default=False) # Job added to queue
  86. on_queue_job_assigned = Column(Boolean, default=False) # Model-based job assigned to printer
  87. on_queue_job_started = Column(Boolean, default=False) # Queue job started printing
  88. on_queue_job_waiting = Column(Boolean, default=True) # Job waiting for filament or printer
  89. on_queue_job_skipped = Column(Boolean, default=True) # Job skipped (previous print failed)
  90. on_queue_job_failed = Column(Boolean, default=True) # Job failed to start
  91. on_queue_completed = Column(Boolean, default=False) # All pending jobs finished
  92. # Quiet hours (do not disturb)
  93. quiet_hours_enabled = Column(Boolean, default=False)
  94. quiet_hours_start = Column(String(5), nullable=True) # HH:MM format, e.g., "22:00"
  95. quiet_hours_end = Column(String(5), nullable=True) # HH:MM format, e.g., "07:00"
  96. # Daily digest (batch notifications into a single daily summary)
  97. daily_digest_enabled = Column(Boolean, default=False)
  98. daily_digest_time = Column(String(5), nullable=True) # HH:MM format, e.g., "08:00"
  99. # Optional: Link to specific printer (NULL = all printers)
  100. printer_id = Column(Integer, ForeignKey("printers.id", ondelete="SET NULL"), nullable=True)
  101. # Status tracking
  102. last_success = Column(DateTime, nullable=True)
  103. last_error = Column(Text, nullable=True)
  104. last_error_at = Column(DateTime, nullable=True)
  105. # Timestamps
  106. created_at = Column(DateTime, default=datetime.utcnow)
  107. updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
  108. # Relationships
  109. printer = relationship("Printer", back_populates="notification_providers")
  110. logs = relationship("NotificationLog", back_populates="provider", cascade="all, delete-orphan")
  111. digest_queue = relationship("NotificationDigestQueue", back_populates="provider", cascade="all, delete-orphan")