Browse Source

Added notification module for AMS humidity and temperature

maziggy 5 months ago
parent
commit
e392597192

+ 6 - 0
backend/app/api/routes/notifications.py

@@ -45,6 +45,9 @@ def _provider_to_dict(provider: NotificationProvider) -> dict:
         "on_printer_error": provider.on_printer_error,
         "on_printer_error": provider.on_printer_error,
         "on_filament_low": provider.on_filament_low,
         "on_filament_low": provider.on_filament_low,
         "on_maintenance_due": provider.on_maintenance_due,
         "on_maintenance_due": provider.on_maintenance_due,
+        # AMS environmental alarms
+        "on_ams_humidity_high": provider.on_ams_humidity_high,
+        "on_ams_temperature_high": provider.on_ams_temperature_high,
         # Quiet hours
         # Quiet hours
         "quiet_hours_enabled": provider.quiet_hours_enabled,
         "quiet_hours_enabled": provider.quiet_hours_enabled,
         "quiet_hours_start": provider.quiet_hours_start,
         "quiet_hours_start": provider.quiet_hours_start,
@@ -101,6 +104,9 @@ async def create_notification_provider(
         on_printer_error=provider_data.on_printer_error,
         on_printer_error=provider_data.on_printer_error,
         on_filament_low=provider_data.on_filament_low,
         on_filament_low=provider_data.on_filament_low,
         on_maintenance_due=provider_data.on_maintenance_due,
         on_maintenance_due=provider_data.on_maintenance_due,
+        # AMS environmental alarms
+        on_ams_humidity_high=provider_data.on_ams_humidity_high,
+        on_ams_temperature_high=provider_data.on_ams_temperature_high,
         # Quiet hours
         # Quiet hours
         quiet_hours_enabled=provider_data.quiet_hours_enabled,
         quiet_hours_enabled=provider_data.quiet_hours_enabled,
         quiet_hours_start=provider_data.quiet_hours_start,
         quiet_hours_start=provider_data.quiet_hours_start,

+ 57 - 0
backend/app/main.py

@@ -954,6 +954,8 @@ _ams_history_task: asyncio.Task | None = None
 AMS_HISTORY_INTERVAL = 300  # Record every 5 minutes
 AMS_HISTORY_INTERVAL = 300  # Record every 5 minutes
 AMS_HISTORY_RETENTION_DAYS = 30  # Keep data for 30 days
 AMS_HISTORY_RETENTION_DAYS = 30  # Keep data for 30 days
 _ams_cleanup_counter = 0  # Track recordings to trigger periodic cleanup
 _ams_cleanup_counter = 0  # Track recordings to trigger periodic cleanup
+_ams_alarm_cooldown: dict[str, datetime] = {}  # Track alarm cooldowns (printer_id:ams_id:type -> last_alarm_time)
+AMS_ALARM_COOLDOWN_MINUTES = 60  # Don't send same alarm more than once per hour
 
 
 
 
 async def record_ams_history():
 async def record_ams_history():
@@ -968,6 +970,7 @@ async def record_ams_history():
         try:
         try:
             from backend.app.models.ams_history import AMSSensorHistory
             from backend.app.models.ams_history import AMSSensorHistory
             from backend.app.models.printer import Printer
             from backend.app.models.printer import Printer
+            from backend.app.models.settings import Settings
 
 
             async with async_session() as db:
             async with async_session() as db:
                 # Get all active printers
                 # Get all active printers
@@ -976,6 +979,24 @@ async def record_ams_history():
                 )
                 )
                 printers = result.scalars().all()
                 printers = result.scalars().all()
 
 
+                # Get alarm thresholds from settings
+                humidity_threshold = 60.0  # Default: fair threshold
+                temp_threshold = 35.0  # Default: fair threshold
+                result = await db.execute(select(Settings).where(Settings.key == "ams_humidity_fair"))
+                setting = result.scalar_one_or_none()
+                if setting:
+                    try:
+                        humidity_threshold = float(setting.value)
+                    except (ValueError, TypeError):
+                        pass
+                result = await db.execute(select(Settings).where(Settings.key == "ams_temp_fair"))
+                setting = result.scalar_one_or_none()
+                if setting:
+                    try:
+                        temp_threshold = float(setting.value)
+                    except (ValueError, TypeError):
+                        pass
+
                 recorded_count = 0
                 recorded_count = 0
                 for printer in printers:
                 for printer in printers:
                     # Get current state from printer manager
                     # Get current state from printer manager
@@ -1030,6 +1051,42 @@ async def record_ams_history():
                         db.add(history)
                         db.add(history)
                         recorded_count += 1
                         recorded_count += 1
 
 
+                        # Generate AMS label (A, B, C, D or HT-A for AMS-Lite/Hub)
+                        if ams_id >= 128:
+                            ams_label = f"HT-{chr(65 + (ams_id - 128))}"
+                        else:
+                            ams_label = f"AMS-{chr(65 + ams_id)}"
+
+                        # Check humidity alarm (only if above threshold)
+                        if humidity is not None and humidity > humidity_threshold:
+                            cooldown_key = f"{printer.id}:{ams_id}:humidity"
+                            last_alarm = _ams_alarm_cooldown.get(cooldown_key)
+                            now = datetime.now()
+                            if last_alarm is None or (now - last_alarm).total_seconds() >= AMS_ALARM_COOLDOWN_MINUTES * 60:
+                                _ams_alarm_cooldown[cooldown_key] = now
+                                logger.info(f"Sending humidity alarm for {printer.name} {ams_label}: {humidity}% > {humidity_threshold}%")
+                                try:
+                                    await notification_service.on_ams_humidity_high(
+                                        printer.id, printer.name, ams_label, humidity, humidity_threshold, db
+                                    )
+                                except Exception as e:
+                                    logger.warning(f"Failed to send humidity alarm: {e}")
+
+                        # Check temperature alarm (only if above threshold)
+                        if temperature is not None and temperature > temp_threshold:
+                            cooldown_key = f"{printer.id}:{ams_id}:temperature"
+                            last_alarm = _ams_alarm_cooldown.get(cooldown_key)
+                            now = datetime.now()
+                            if last_alarm is None or (now - last_alarm).total_seconds() >= AMS_ALARM_COOLDOWN_MINUTES * 60:
+                                _ams_alarm_cooldown[cooldown_key] = now
+                                logger.info(f"Sending temperature alarm for {printer.name} {ams_label}: {temperature}°C > {temp_threshold}°C")
+                                try:
+                                    await notification_service.on_ams_temperature_high(
+                                        printer.id, printer.name, ams_label, temperature, temp_threshold, db
+                                    )
+                                except Exception as e:
+                                    logger.warning(f"Failed to send temperature alarm: {e}")
+
                 await db.commit()
                 await db.commit()
                 if recorded_count > 0:
                 if recorded_count > 0:
                     logger.info(f"Recorded {recorded_count} AMS sensor history entries")
                     logger.info(f"Recorded {recorded_count} AMS sensor history entries")

+ 4 - 0
backend/app/models/notification.py

@@ -72,6 +72,10 @@ class NotificationProvider(Base):
     on_filament_low = Column(Boolean, default=False)
     on_filament_low = Column(Boolean, default=False)
     on_maintenance_due = Column(Boolean, default=False)  # Maintenance reminder
     on_maintenance_due = Column(Boolean, default=False)  # Maintenance reminder
 
 
+    # Event triggers - AMS environmental alarms
+    on_ams_humidity_high = Column(Boolean, default=False)  # Humidity above threshold
+    on_ams_temperature_high = Column(Boolean, default=False)  # Temperature above threshold
+
     # Quiet hours (do not disturb)
     # Quiet hours (do not disturb)
     quiet_hours_enabled = Column(Boolean, default=False)
     quiet_hours_enabled = Column(Boolean, default=False)
     quiet_hours_start = Column(String(5), nullable=True)  # HH:MM format, e.g., "22:00"
     quiet_hours_start = Column(String(5), nullable=True)  # HH:MM format, e.g., "22:00"

+ 12 - 0
backend/app/models/notification_template.py

@@ -81,6 +81,18 @@ DEFAULT_TEMPLATES = [
         "title_template": "Maintenance Due",
         "title_template": "Maintenance Due",
         "body_template": "{printer}:\n{items}",
         "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": "test",
         "event_type": "test",
         "name": "Test Notification",
         "name": "Test Notification",

+ 8 - 0
backend/app/schemas/notification.py

@@ -38,6 +38,10 @@ class NotificationProviderBase(BaseModel):
     on_filament_low: bool = Field(default=False, description="Notify when filament is running low")
     on_filament_low: bool = Field(default=False, description="Notify when filament is running low")
     on_maintenance_due: bool = Field(default=False, description="Notify when maintenance is due")
     on_maintenance_due: bool = Field(default=False, description="Notify when maintenance is due")
 
 
+    # Event triggers - AMS environmental alarms
+    on_ams_humidity_high: bool = Field(default=False, description="Notify when AMS humidity exceeds threshold")
+    on_ams_temperature_high: bool = Field(default=False, description="Notify when AMS temperature exceeds threshold")
+
     # Quiet hours
     # Quiet hours
     quiet_hours_enabled: bool = Field(default=False, description="Enable quiet hours")
     quiet_hours_enabled: bool = Field(default=False, description="Enable quiet hours")
     quiet_hours_start: str | None = Field(default=None, description="Start time in HH:MM format")
     quiet_hours_start: str | None = Field(default=None, description="Start time in HH:MM format")
@@ -94,6 +98,10 @@ class NotificationProviderUpdate(BaseModel):
     on_filament_low: bool | None = None
     on_filament_low: bool | None = None
     on_maintenance_due: bool | None = None
     on_maintenance_due: bool | None = None
 
 
+    # Event triggers - AMS environmental alarms
+    on_ams_humidity_high: bool | None = None
+    on_ams_temperature_high: bool | None = None
+
     # Quiet hours
     # Quiet hours
     quiet_hours_enabled: bool | None = None
     quiet_hours_enabled: bool | None = None
     quiet_hours_start: str | None = None
     quiet_hours_start: str | None = None

+ 20 - 0
backend/app/schemas/notification_template.py

@@ -18,6 +18,8 @@ class EventType(str, Enum):
     PRINTER_ERROR = "printer_error"
     PRINTER_ERROR = "printer_error"
     FILAMENT_LOW = "filament_low"
     FILAMENT_LOW = "filament_low"
     MAINTENANCE_DUE = "maintenance_due"
     MAINTENANCE_DUE = "maintenance_due"
+    AMS_HUMIDITY_HIGH = "ams_humidity_high"
+    AMS_TEMPERATURE_HIGH = "ams_temperature_high"
     TEST = "test"
     TEST = "test"
 
 
 
 
@@ -32,6 +34,8 @@ EVENT_VARIABLES: dict[str, list[str]] = {
     "printer_error": ["printer", "error_type", "error_detail", "timestamp", "app_name"],
     "printer_error": ["printer", "error_type", "error_detail", "timestamp", "app_name"],
     "filament_low": ["printer", "slot", "remaining_percent", "color", "timestamp", "app_name"],
     "filament_low": ["printer", "slot", "remaining_percent", "color", "timestamp", "app_name"],
     "maintenance_due": ["printer", "items", "timestamp", "app_name"],
     "maintenance_due": ["printer", "items", "timestamp", "app_name"],
+    "ams_humidity_high": ["printer", "ams_label", "humidity", "threshold", "timestamp", "app_name"],
+    "ams_temperature_high": ["printer", "ams_label", "temperature", "threshold", "timestamp", "app_name"],
     "test": ["app_name", "timestamp"],
     "test": ["app_name", "timestamp"],
 }
 }
 
 
@@ -101,6 +105,22 @@ SAMPLE_DATA: dict[str, dict[str, str]] = {
         "timestamp": "2024-01-15 14:30",
         "timestamp": "2024-01-15 14:30",
         "app_name": "Bambuddy",
         "app_name": "Bambuddy",
     },
     },
+    "ams_humidity_high": {
+        "printer": "Bambu X1C",
+        "ams_label": "AMS-A",
+        "humidity": "75",
+        "threshold": "60",
+        "timestamp": "2024-01-15 14:30",
+        "app_name": "Bambuddy",
+    },
+    "ams_temperature_high": {
+        "printer": "Bambu X1C",
+        "ams_label": "AMS-A",
+        "temperature": "42",
+        "threshold": "35",
+        "timestamp": "2024-01-15 14:30",
+        "app_name": "Bambuddy",
+    },
     "test": {
     "test": {
         "app_name": "Bambuddy",
         "app_name": "Bambuddy",
         "timestamp": "2024-01-15 14:30",
         "timestamp": "2024-01-15 14:30",

+ 58 - 3
backend/app/services/notification_service.py

@@ -487,12 +487,17 @@ class NotificationService:
         event_type: str = "unknown",
         event_type: str = "unknown",
         printer_id: int | None = None,
         printer_id: int | None = None,
         printer_name: str | None = None,
         printer_name: str | None = None,
+        force_immediate: bool = False,
     ):
     ):
-        """Send notification to multiple providers and log the results."""
+        """Send notification to multiple providers and log the results.
+
+        Args:
+            force_immediate: If True, bypass digest mode and send immediately (for alarms)
+        """
         for provider in providers:
         for provider in providers:
             try:
             try:
-                # Check if provider wants digest mode
-                if provider.daily_digest_enabled and provider.daily_digest_time:
+                # Check if provider wants digest mode (unless force_immediate is set)
+                if not force_immediate and provider.daily_digest_enabled and provider.daily_digest_time:
                     await self._queue_for_digest(
                     await self._queue_for_digest(
                         provider=provider,
                         provider=provider,
                         event_type=event_type,
                         event_type=event_type,
@@ -729,6 +734,56 @@ class NotificationService:
         title, message = await self._build_message_from_template(db, "maintenance_due", variables)
         title, message = await self._build_message_from_template(db, "maintenance_due", variables)
         await self._send_to_providers(providers, title, message, db, "maintenance_due", printer_id, printer_name)
         await self._send_to_providers(providers, title, message, db, "maintenance_due", printer_id, printer_name)
 
 
+    async def on_ams_humidity_high(
+        self,
+        printer_id: int,
+        printer_name: str,
+        ams_label: str,
+        humidity: float,
+        threshold: float,
+        db: AsyncSession,
+    ):
+        """Handle AMS high humidity alarm event. Always sends immediately (bypasses digest)."""
+        providers = await self._get_providers_for_event(db, "on_ams_humidity_high", printer_id)
+        if not providers:
+            return
+
+        variables = {
+            "printer": printer_name,
+            "ams_label": ams_label,
+            "humidity": f"{humidity:.0f}",
+            "threshold": f"{threshold:.0f}",
+        }
+
+        title, message = await self._build_message_from_template(db, "ams_humidity_high", variables)
+        # Alarms always send immediately, bypassing digest mode
+        await self._send_to_providers(providers, title, message, db, "ams_humidity_high", printer_id, printer_name, force_immediate=True)
+
+    async def on_ams_temperature_high(
+        self,
+        printer_id: int,
+        printer_name: str,
+        ams_label: str,
+        temperature: float,
+        threshold: float,
+        db: AsyncSession,
+    ):
+        """Handle AMS high temperature alarm event. Always sends immediately (bypasses digest)."""
+        providers = await self._get_providers_for_event(db, "on_ams_temperature_high", printer_id)
+        if not providers:
+            return
+
+        variables = {
+            "printer": printer_name,
+            "ams_label": ams_label,
+            "temperature": f"{temperature:.1f}",
+            "threshold": f"{threshold:.1f}",
+        }
+
+        title, message = await self._build_message_from_template(db, "ams_temperature_high", variables)
+        # Alarms always send immediately, bypassing digest mode
+        await self._send_to_providers(providers, title, message, db, "ams_temperature_high", printer_id, printer_name, force_immediate=True)
+
     def clear_template_cache(self):
     def clear_template_cache(self):
         """Clear the template cache. Call this when templates are updated."""
         """Clear the template cache. Call this when templates are updated."""
         self._template_cache.clear()
         self._template_cache.clear()

+ 9 - 0
frontend/src/api/client.ts

@@ -783,6 +783,9 @@ export interface NotificationProvider {
   on_printer_error: boolean;
   on_printer_error: boolean;
   on_filament_low: boolean;
   on_filament_low: boolean;
   on_maintenance_due: boolean;
   on_maintenance_due: boolean;
+  // AMS environmental alarms
+  on_ams_humidity_high: boolean;
+  on_ams_temperature_high: boolean;
   // Quiet hours
   // Quiet hours
   quiet_hours_enabled: boolean;
   quiet_hours_enabled: boolean;
   quiet_hours_start: string | null;
   quiet_hours_start: string | null;
@@ -817,6 +820,9 @@ export interface NotificationProviderCreate {
   on_printer_error?: boolean;
   on_printer_error?: boolean;
   on_filament_low?: boolean;
   on_filament_low?: boolean;
   on_maintenance_due?: boolean;
   on_maintenance_due?: boolean;
+  // AMS environmental alarms
+  on_ams_humidity_high?: boolean;
+  on_ams_temperature_high?: boolean;
   // Quiet hours
   // Quiet hours
   quiet_hours_enabled?: boolean;
   quiet_hours_enabled?: boolean;
   quiet_hours_start?: string | null;
   quiet_hours_start?: string | null;
@@ -844,6 +850,9 @@ export interface NotificationProviderUpdate {
   on_printer_error?: boolean;
   on_printer_error?: boolean;
   on_filament_low?: boolean;
   on_filament_low?: boolean;
   on_maintenance_due?: boolean;
   on_maintenance_due?: boolean;
+  // AMS environmental alarms
+  on_ams_humidity_high?: boolean;
+  on_ams_temperature_high?: boolean;
   // Quiet hours
   // Quiet hours
   quiet_hours_enabled?: boolean;
   quiet_hours_enabled?: boolean;
   quiet_hours_start?: string | null;
   quiet_hours_start?: string | null;

+ 33 - 0
frontend/src/components/NotificationProviderCard.tsx

@@ -147,6 +147,12 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
             {provider.on_maintenance_due && (
             {provider.on_maintenance_due && (
               <span className="px-2 py-0.5 bg-purple-500/20 text-purple-400 text-xs rounded">Maintenance</span>
               <span className="px-2 py-0.5 bg-purple-500/20 text-purple-400 text-xs rounded">Maintenance</span>
             )}
             )}
+            {provider.on_ams_humidity_high && (
+              <span className="px-2 py-0.5 bg-blue-600/20 text-blue-300 text-xs rounded">Humidity</span>
+            )}
+            {provider.on_ams_temperature_high && (
+              <span className="px-2 py-0.5 bg-orange-600/20 text-orange-300 text-xs rounded">Temperature</span>
+            )}
             {provider.quiet_hours_enabled && (
             {provider.quiet_hours_enabled && (
               <span className="px-2 py-0.5 bg-indigo-500/20 text-indigo-400 text-xs rounded flex items-center gap-1">
               <span className="px-2 py-0.5 bg-indigo-500/20 text-indigo-400 text-xs rounded flex items-center gap-1">
                 <Moon className="w-3 h-3" />
                 <Moon className="w-3 h-3" />
@@ -317,6 +323,33 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
                 </div>
                 </div>
               </div>
               </div>
 
 
+              {/* AMS Environmental Alarms */}
+              <div className="space-y-2">
+                <p className="text-xs text-bambu-gray uppercase tracking-wide">AMS Alarms</p>
+
+                <div className="flex items-center justify-between">
+                  <div>
+                    <p className="text-sm text-white">Humidity High</p>
+                    <p className="text-xs text-bambu-gray">Notify when humidity exceeds threshold</p>
+                  </div>
+                  <Toggle
+                    checked={provider.on_ams_humidity_high ?? false}
+                    onChange={(checked) => updateMutation.mutate({ on_ams_humidity_high: checked })}
+                  />
+                </div>
+
+                <div className="flex items-center justify-between">
+                  <div>
+                    <p className="text-sm text-white">Temperature High</p>
+                    <p className="text-xs text-bambu-gray">Notify when temperature exceeds threshold</p>
+                  </div>
+                  <Toggle
+                    checked={provider.on_ams_temperature_high ?? false}
+                    onChange={(checked) => updateMutation.mutate({ on_ams_temperature_high: checked })}
+                  />
+                </div>
+              </div>
+
               {/* Quiet Hours */}
               {/* Quiet Hours */}
               <div className="space-y-2">
               <div className="space-y-2">
                 <div className="flex items-center justify-between">
                 <div className="flex items-center justify-between">

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-Bz7SiJBw.css


File diff suppressed because it is too large
+ 0 - 0
static/assets/index-CB_jd89Z.css


File diff suppressed because it is too large
+ 0 - 0
static/assets/index-GIyWZiRq.js


+ 2 - 2
static/index.html

@@ -23,8 +23,8 @@
 
 
     <!-- Splash screens for iOS -->
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-CRQNt4-E.js"></script>
-    <link rel="stylesheet" crossorigin href="/assets/index-Bz7SiJBw.css">
+    <script type="module" crossorigin src="/assets/index-GIyWZiRq.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-CB_jd89Z.css">
   </head>
   </head>
   <body>
   <body>
     <div id="root"></div>
     <div id="root"></div>

Some files were not shown because too many files changed in this diff