notification.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. """Pydantic schemas for notification providers."""
  2. from datetime import datetime
  3. from typing import Any
  4. from pydantic import BaseModel, Field, field_validator
  5. from backend.app.core.compat import StrEnum
  6. class ProviderType(StrEnum):
  7. """Supported notification provider types."""
  8. CALLMEBOT = "callmebot"
  9. NTFY = "ntfy"
  10. PUSHOVER = "pushover"
  11. TELEGRAM = "telegram"
  12. EMAIL = "email"
  13. DISCORD = "discord"
  14. WEBHOOK = "webhook"
  15. HOMEASSISTANT = "homeassistant"
  16. class NotificationProviderBase(BaseModel):
  17. """Base schema for notification providers."""
  18. name: str = Field(..., min_length=1, max_length=100, description="User-defined name")
  19. provider_type: ProviderType = Field(..., description="Type of notification provider")
  20. enabled: bool = Field(default=True, description="Whether notifications are enabled")
  21. config: dict[str, Any] = Field(..., description="Provider-specific configuration")
  22. # Event triggers - print lifecycle
  23. on_print_start: bool = Field(default=False, description="Notify on print start")
  24. on_print_complete: bool = Field(default=True, description="Notify on print complete")
  25. on_print_failed: bool = Field(default=True, description="Notify on print failed")
  26. on_print_stopped: bool = Field(default=True, description="Notify when print is stopped/cancelled")
  27. on_print_progress: bool = Field(default=False, description="Notify at 25%, 50%, 75% progress")
  28. on_print_missing_spool_assignment: bool = Field(
  29. default=False,
  30. description="Notify when a print starts with required trays missing spool assignments",
  31. )
  32. # Event triggers - printer status
  33. on_printer_offline: bool = Field(default=False, description="Notify when printer goes offline")
  34. on_printer_error: bool = Field(default=False, description="Notify on printer errors (AMS, etc.)")
  35. on_ai_failure_detection: bool = Field(
  36. default=False,
  37. description="Notify when Obico AI detects a possible print failure (spaghetti)",
  38. )
  39. on_filament_low: bool = Field(default=False, description="Notify when filament is running low")
  40. on_maintenance_due: bool = Field(default=False, description="Notify when maintenance is due")
  41. # Event triggers - AMS environmental alarms (regular AMS)
  42. on_ams_humidity_high: bool = Field(default=False, description="Notify when AMS humidity exceeds threshold")
  43. on_ams_temperature_high: bool = Field(default=False, description="Notify when AMS temperature exceeds threshold")
  44. # Event triggers - AMS-HT environmental alarms
  45. on_ams_ht_humidity_high: bool = Field(default=False, description="Notify when AMS-HT humidity exceeds threshold")
  46. on_ams_ht_temperature_high: bool = Field(
  47. default=False, description="Notify when AMS-HT temperature exceeds threshold"
  48. )
  49. # Event triggers - Build plate detection
  50. on_plate_not_empty: bool = Field(default=True, description="Notify when objects detected on plate before print")
  51. # Event triggers - Bed cooled
  52. on_bed_cooled: bool = Field(default=False, description="Notify when bed cools after print")
  53. # Event triggers - First layer complete
  54. on_first_layer_complete: bool = Field(default=False, description="Notify when first layer completes")
  55. # Event triggers - Print queue
  56. on_queue_job_added: bool = Field(default=False, description="Notify when job is added to queue")
  57. on_queue_job_assigned: bool = Field(default=False, description="Notify when model-based job is assigned to printer")
  58. on_queue_job_started: bool = Field(default=False, description="Notify when queue job starts printing")
  59. on_queue_job_waiting: bool = Field(default=True, description="Notify when job is waiting for filament or printer")
  60. on_queue_job_skipped: bool = Field(default=True, description="Notify when job is skipped")
  61. on_queue_job_failed: bool = Field(default=True, description="Notify when job fails to start")
  62. on_queue_completed: bool = Field(default=False, description="Notify when all queue jobs finish")
  63. # Quiet hours
  64. quiet_hours_enabled: bool = Field(default=False, description="Enable quiet hours")
  65. quiet_hours_start: str | None = Field(default=None, description="Start time in HH:MM format")
  66. quiet_hours_end: str | None = Field(default=None, description="End time in HH:MM format")
  67. # Daily digest
  68. daily_digest_enabled: bool = Field(default=False, description="Batch notifications into daily digest")
  69. daily_digest_time: str | None = Field(default=None, description="Time to send digest in HH:MM format")
  70. # Printer filter
  71. printer_id: int | None = Field(default=None, description="Specific printer ID or null for all")
  72. @field_validator("quiet_hours_start", "quiet_hours_end", "daily_digest_time")
  73. @classmethod
  74. def validate_time_format(cls, v: str | None) -> str | None:
  75. if v is None:
  76. return v
  77. try:
  78. parts = v.split(":")
  79. if len(parts) != 2:
  80. raise ValueError("Invalid time format")
  81. hour, minute = int(parts[0]), int(parts[1])
  82. if not (0 <= hour <= 23 and 0 <= minute <= 59):
  83. raise ValueError("Invalid time range")
  84. return f"{hour:02d}:{minute:02d}"
  85. except (ValueError, TypeError):
  86. raise ValueError("Time must be in HH:MM format (e.g., 22:00)")
  87. class NotificationProviderCreate(NotificationProviderBase):
  88. """Schema for creating a notification provider."""
  89. pass
  90. class NotificationProviderUpdate(BaseModel):
  91. """Schema for updating a notification provider (all fields optional)."""
  92. name: str | None = Field(default=None, min_length=1, max_length=100)
  93. provider_type: ProviderType | None = None
  94. enabled: bool | None = None
  95. config: dict[str, Any] | None = None
  96. # Event triggers - print lifecycle
  97. on_print_start: bool | None = None
  98. on_print_complete: bool | None = None
  99. on_print_failed: bool | None = None
  100. on_print_stopped: bool | None = None
  101. on_print_progress: bool | None = None
  102. on_print_missing_spool_assignment: bool | None = None
  103. # Event triggers - printer status
  104. on_printer_offline: bool | None = None
  105. on_printer_error: bool | None = None
  106. on_ai_failure_detection: bool | None = None
  107. on_filament_low: bool | None = None
  108. on_maintenance_due: bool | None = None
  109. # Event triggers - AMS environmental alarms (regular AMS)
  110. on_ams_humidity_high: bool | None = None
  111. on_ams_temperature_high: bool | None = None
  112. # Event triggers - AMS-HT environmental alarms
  113. on_ams_ht_humidity_high: bool | None = None
  114. on_ams_ht_temperature_high: bool | None = None
  115. # Event triggers - Build plate detection
  116. on_plate_not_empty: bool | None = None
  117. # Event triggers - Bed cooled
  118. on_bed_cooled: bool | None = None
  119. # Event triggers - First layer complete
  120. on_first_layer_complete: bool | None = None
  121. # Event triggers - Print queue
  122. on_queue_job_added: bool | None = None
  123. on_queue_job_assigned: bool | None = None
  124. on_queue_job_started: bool | None = None
  125. on_queue_job_waiting: bool | None = None
  126. on_queue_job_skipped: bool | None = None
  127. on_queue_job_failed: bool | None = None
  128. on_queue_completed: bool | None = None
  129. # Quiet hours
  130. quiet_hours_enabled: bool | None = None
  131. quiet_hours_start: str | None = None
  132. quiet_hours_end: str | None = None
  133. # Daily digest
  134. daily_digest_enabled: bool | None = None
  135. daily_digest_time: str | None = None
  136. # Printer filter
  137. printer_id: int | None = None
  138. class NotificationProviderResponse(NotificationProviderBase):
  139. """Schema for notification provider API responses."""
  140. id: int
  141. last_success: datetime | None = None
  142. last_error: str | None = None
  143. last_error_at: datetime | None = None
  144. created_at: datetime
  145. updated_at: datetime
  146. class Config:
  147. from_attributes = True
  148. class NotificationTestRequest(BaseModel):
  149. """Schema for testing notification configuration."""
  150. provider_type: ProviderType
  151. config: dict[str, Any]
  152. class NotificationTestResponse(BaseModel):
  153. """Schema for test notification response."""
  154. success: bool
  155. message: str
  156. # Provider-specific config schemas for documentation/validation reference
  157. class CallMeBotConfig(BaseModel):
  158. """CallMeBot/WhatsApp configuration."""
  159. phone: str = Field(..., description="Phone number with country code (e.g., +1234567890)")
  160. apikey: str = Field(..., description="API key from CallMeBot")
  161. class NtfyConfig(BaseModel):
  162. """ntfy configuration."""
  163. server: str = Field(default="https://ntfy.sh", description="ntfy server URL")
  164. topic: str = Field(..., description="Topic name to publish to")
  165. auth_token: str | None = Field(default=None, description="Optional authentication token")
  166. event_priorities: dict[str, int] | None = Field(
  167. default=None,
  168. description=(
  169. "Per-event priority override. Keys are event names (e.g. 'on_print_failed'); "
  170. "values are ntfy priorities 1-5 (1=min, 2=low, 3=default, 4=high, 5=urgent). "
  171. "Events without an entry use ntfy's server-side default."
  172. ),
  173. )
  174. class PushoverConfig(BaseModel):
  175. """Pushover configuration."""
  176. user_key: str = Field(..., description="Your Pushover user key")
  177. app_token: str = Field(..., description="Your Pushover application token")
  178. priority: int = Field(default=0, ge=-2, le=2, description="Message priority (-2 to 2)")
  179. # Emergency priority (2) only: how often to re-alert and when to stop.
  180. # Pushover requires retry >= 30s and expire <= 10800s (3h).
  181. retry: int = Field(default=60, ge=30, le=10800, description="Emergency re-alert interval in seconds (priority 2)")
  182. expire: int = Field(default=3600, ge=30, le=10800, description="Emergency alert expiry in seconds (priority 2)")
  183. class TelegramConfig(BaseModel):
  184. """Telegram bot configuration."""
  185. bot_token: str = Field(..., description="Bot token from @BotFather")
  186. chat_id: str = Field(..., description="Chat ID to send messages to")
  187. class EmailConfig(BaseModel):
  188. """Email/SMTP configuration."""
  189. smtp_server: str = Field(..., description="SMTP server hostname")
  190. smtp_port: int = Field(default=587, description="SMTP port (587 for TLS, 465 for SSL)")
  191. username: str = Field(..., description="SMTP username/email")
  192. password: str = Field(..., description="SMTP password or app password")
  193. from_email: str = Field(..., description="From email address")
  194. to_email: str = Field(..., description="Recipient email address")
  195. use_tls: bool = Field(default=True, description="Use TLS encryption")
  196. # Notification Log schemas
  197. class NotificationLogResponse(BaseModel):
  198. """Schema for notification log API responses."""
  199. id: int
  200. provider_id: int
  201. provider_name: str | None = None
  202. provider_type: str | None = None
  203. event_type: str
  204. title: str
  205. message: str
  206. success: bool
  207. error_message: str | None = None
  208. printer_id: int | None = None
  209. printer_name: str | None = None
  210. created_at: datetime
  211. class Config:
  212. from_attributes = True
  213. class NotificationLogStats(BaseModel):
  214. """Statistics for notification logs."""
  215. total: int
  216. success_count: int
  217. failure_count: int
  218. by_event_type: dict[str, int]
  219. by_provider: dict[str, int]