mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 11:12:35 +02:00
195 lines
10 KiB
Python
195 lines
10 KiB
Python
"""Notification provider and log models for push notifications."""
|
|
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, Text
|
|
from sqlalchemy.orm import relationship
|
|
|
|
from backend.app.core.database import Base
|
|
|
|
|
|
class NotificationDigestQueue(Base):
|
|
"""Model for queuing notifications to be sent in daily digest."""
|
|
|
|
__tablename__ = "notification_digest_queue"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
provider_id = Column(Integer, ForeignKey("notification_providers.id", ondelete="CASCADE"), nullable=False)
|
|
event_type = Column(String(50), nullable=False) # print_start, print_complete, etc.
|
|
title = Column(String(255), nullable=False)
|
|
message = Column(Text, nullable=False)
|
|
printer_id = Column(Integer, ForeignKey("printers.id", ondelete="SET NULL"), nullable=True)
|
|
printer_name = Column(String(100), nullable=True)
|
|
created_at = Column(DateTime, default=datetime.utcnow, index=True)
|
|
|
|
# Relationships
|
|
provider = relationship("NotificationProvider", back_populates="digest_queue")
|
|
|
|
|
|
class NotificationLog(Base):
|
|
"""Model for logging sent notifications."""
|
|
|
|
__tablename__ = "notification_logs"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
provider_id = Column(Integer, ForeignKey("notification_providers.id", ondelete="CASCADE"), nullable=False)
|
|
event_type = Column(String(50), nullable=False) # print_start, print_complete, etc.
|
|
title = Column(String(255), nullable=False)
|
|
message = Column(Text, nullable=False)
|
|
success = Column(Boolean, default=True)
|
|
error_message = Column(Text, nullable=True)
|
|
printer_id = Column(Integer, ForeignKey("printers.id", ondelete="SET NULL"), nullable=True)
|
|
printer_name = Column(String(100), nullable=True) # Store name in case printer is deleted
|
|
created_at = Column(DateTime, default=datetime.utcnow, index=True)
|
|
|
|
# Relationships
|
|
provider = relationship("NotificationProvider", back_populates="logs")
|
|
|
|
|
|
class NotificationProvider(Base):
|
|
"""Model for notification providers (WhatsApp, ntfy, Pushover, etc.)."""
|
|
|
|
__tablename__ = "notification_providers"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
name = Column(String(100), nullable=False) # User-defined name
|
|
provider_type = Column(String(50), nullable=False) # callmebot, ntfy, pushover, telegram, email
|
|
enabled = Column(Boolean, default=True)
|
|
|
|
# Provider-specific configuration stored as JSON string
|
|
config = Column(Text, nullable=False)
|
|
|
|
# Event triggers - print lifecycle
|
|
on_print_start = Column(Boolean, default=False)
|
|
on_print_complete = Column(Boolean, default=True)
|
|
on_print_failed = Column(Boolean, default=True)
|
|
on_print_stopped = Column(Boolean, default=True) # User cancelled/stopped print
|
|
on_print_progress = Column(Boolean, default=False) # 25%, 50%, 75% milestones
|
|
on_print_missing_spool_assignment = Column(Boolean, default=False) # Print started with unassigned required tray(s)
|
|
on_billing_charge_failed = Column(Boolean, default=True) # A completed/stopped print could not be charged
|
|
|
|
# Event triggers - printer status
|
|
on_printer_offline = Column(Boolean, default=False)
|
|
on_printer_error = Column(Boolean, default=False) # AMS issues, etc.
|
|
on_ai_failure_detection = Column(Boolean, default=False) # Obico spaghetti / failure detection (#1794)
|
|
on_filament_low = Column(Boolean, default=False)
|
|
on_maintenance_due = Column(Boolean, default=False) # Maintenance reminder
|
|
|
|
# Event triggers - AMS environmental alarms (regular AMS with 4 slots)
|
|
on_ams_humidity_high = Column(Boolean, default=False) # AMS humidity above threshold
|
|
on_ams_temperature_high = Column(Boolean, default=False) # AMS temperature above threshold
|
|
# Auto-drying gave up on a unit (#2770). Defaults True: it reports that
|
|
# Bambuddy has stopped acting, which nothing else in the UI would say.
|
|
on_ams_drying_suspended = Column(Boolean, default=True)
|
|
|
|
# Event triggers - AMS-HT environmental alarms (single slot heated AMS)
|
|
on_ams_ht_humidity_high = Column(Boolean, default=False) # AMS-HT humidity above threshold
|
|
on_ams_ht_temperature_high = Column(Boolean, default=False) # AMS-HT temperature above threshold
|
|
|
|
# Event triggers - Home Assistant sensors bound to a printer (#1148)
|
|
on_ha_sensor_alert = Column(Boolean, default=False) # Bound HA sensor entered its alert state
|
|
|
|
# Event triggers - Home Assistant sensors bound to a storage location (#2824)
|
|
# Its own column rather than reusing on_ha_sensor_alert above: that one can
|
|
# be scoped to a single printer, and a location alert has no printer to
|
|
# scope by, so sharing it would leak drybox alerts to a provider narrowed
|
|
# to one printer's sensors.
|
|
on_location_ha_sensor_alert = Column(Boolean, default=False)
|
|
|
|
# Event triggers - Build plate detection
|
|
on_plate_not_empty = Column(Boolean, default=True) # Objects detected on plate before print
|
|
# Off by default: fires after every print, alongside the print-complete alert (#2525)
|
|
on_plate_clear_required = Column(Boolean, default=False) # Print ended, queue gated until plate is confirmed clear
|
|
|
|
# Print asked for an outcome verdict (#1898). Defaults ON: it only ever
|
|
# fires for prints where the user opted in per-job, so the provider-level
|
|
# toggle exists to silence a channel, not to enable the feature.
|
|
on_print_confirm_request = Column(Boolean, default=True)
|
|
# How a Telegram provider collects that verdict (#3046): inline URL
|
|
# buttons ("buttons", the original behaviour), a thumbs-up/down reaction
|
|
# on the message ("reactions"), or both. Reactions need no inbound
|
|
# connectivity — the poller fetches them — so they work without an
|
|
# external_url. Ignored for every other provider type.
|
|
telegram_verdict_mode = Column(String(16), default="buttons")
|
|
|
|
# Event triggers - Bed cooled after print
|
|
on_bed_cooled = Column(Boolean, default=False) # Bed cooled below threshold after print
|
|
on_first_layer_complete = Column(Boolean, default=False) # First layer finished printing
|
|
# Messages another application sends through Bambuddy (POST /notifications/app-message),
|
|
# e.g. Bambuddy Orders' "an order needs you". Off by default: nothing new arrives on upgrade.
|
|
on_app_message = Column(Boolean, default=False)
|
|
|
|
# Event triggers - Inventory stock alerts
|
|
on_stock_reorder_alert = Column(Boolean, default=False) # SKU hits reorder point
|
|
on_stock_break_alert = Column(Boolean, default=False) # Stock will run out before replenishment
|
|
|
|
# Event triggers - Print queue
|
|
on_queue_job_added = Column(Boolean, default=False) # Job added to queue
|
|
on_queue_job_assigned = Column(Boolean, default=False) # Model-based job assigned to printer
|
|
on_queue_job_started = Column(Boolean, default=False) # Queue job started printing
|
|
on_queue_job_waiting = Column(Boolean, default=True) # Job waiting for filament or printer
|
|
on_queue_job_skipped = Column(Boolean, default=True) # Job skipped (previous print failed)
|
|
on_queue_job_failed = Column(Boolean, default=True) # Job failed to start
|
|
on_queue_completed = Column(Boolean, default=False) # All pending jobs finished
|
|
|
|
# Quiet hours (do not disturb)
|
|
quiet_hours_enabled = Column(Boolean, default=False)
|
|
quiet_hours_start = Column(String(5), nullable=True) # HH:MM format, e.g., "22:00"
|
|
quiet_hours_end = Column(String(5), nullable=True) # HH:MM format, e.g., "07:00"
|
|
|
|
# Daily digest (batch notifications into a single daily summary)
|
|
daily_digest_enabled = Column(Boolean, default=False)
|
|
daily_digest_time = Column(String(5), nullable=True) # HH:MM format, e.g., "08:00"
|
|
|
|
# Optional: Link to specific printer (NULL = all printers)
|
|
printer_id = Column(Integer, ForeignKey("printers.id", ondelete="SET NULL"), nullable=True)
|
|
|
|
# Status tracking
|
|
last_success = Column(DateTime, nullable=True)
|
|
last_error = Column(Text, nullable=True)
|
|
last_error_at = Column(DateTime, nullable=True)
|
|
|
|
# Timestamps
|
|
created_at = Column(DateTime, default=datetime.utcnow)
|
|
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
|
|
|
# Relationships
|
|
printer = relationship("Printer", back_populates="notification_providers")
|
|
logs = relationship("NotificationLog", back_populates="provider", cascade="all, delete-orphan")
|
|
digest_queue = relationship("NotificationDigestQueue", back_populates="provider", cascade="all, delete-orphan")
|
|
# ORM-level cascade like the two above: SQLite ships with foreign keys
|
|
# off, so the column's ON DELETE CASCADE alone would leave rows behind.
|
|
pending_verdicts = relationship("TelegramPendingVerdict", back_populates="provider", cascade="all, delete-orphan")
|
|
|
|
|
|
class TelegramPendingVerdict(Base):
|
|
"""A Telegram outcome prompt still waiting for a thumbs-up/down reaction (#3046).
|
|
|
|
One row per delivered ``print_confirm_request`` message on a provider in
|
|
"reactions" or "both" mode. The reaction poller matches incoming
|
|
``message_reaction`` updates against (provider, chat, message) and deletes
|
|
the row once a verdict landed; rows older than a week are pruned.
|
|
"""
|
|
|
|
__tablename__ = "telegram_pending_verdicts"
|
|
|
|
id = Column(Integer, primary_key=True, index=True)
|
|
provider_id = Column(Integer, ForeignKey("notification_providers.id", ondelete="CASCADE"), nullable=False)
|
|
chat_id = Column(String(64), nullable=False)
|
|
message_id = Column(Integer, nullable=False)
|
|
archive_id = Column(Integer, ForeignKey("print_archives.id", ondelete="CASCADE"), nullable=False)
|
|
# The archive's confirm_token when the prompt went out. A reaction counts
|
|
# only while that is still the archive's live token, i.e. exactly as long
|
|
# as the prompt's one-tap links would: a reprint clears the token, so an
|
|
# earlier run's message can no longer answer the new run.
|
|
confirm_token = Column(String(64), nullable=True)
|
|
# sendPhoto messages carry the prompt as a caption, which is edited with a
|
|
# different Bot API method than a plain text message.
|
|
has_caption = Column(Boolean, default=False)
|
|
# The Markdown we sent, so the confirmation edit can append to it — the
|
|
# reaction update does not carry the message body.
|
|
message_text = Column(Text, nullable=True)
|
|
created_at = Column(DateTime, default=datetime.utcnow, index=True)
|
|
|
|
provider = relationship("NotificationProvider", back_populates="pending_verdicts")
|