Files
bambuddy/backend/app/models/notification.py
maziggy cd004df817 Show Home Assistant sensors on the printer card (#1148, #448)
Binds binary_sensor and reading-carrying sensor entities to a printer and
renders their state on its card, worded by Home Assistant's device_class.
Optional per-sensor alert condition drives a notification on the transition
into the alert state and an opt-in interlock that holds queued prints while
alerting -- a hold with a readable waiting_reason, never a failure, and only
ever on a sensor that was read successfully.

Sensors get their own table rather than a wider entity pattern on SmartPlug:
get_smart_plug_by_printer would otherwise hand the card's power button a door
contact to switch.

The hold is passed to the model matcher directly rather than merged into
busy_printers: _check_auto_drying reads that set as "is currently printing"
and would put an idle-but-held printer down the mid-print drying path.

The notification_providers migration spells its default FALSE, not 0 --
Postgres rejects an integer default for a boolean and _safe_execute swallows
the error.
2026-08-05 14:26:38 +02:00

135 lines
6.8 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)
# 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
# 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 - 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
# 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
# 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")