mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 03:54:17 +02:00
Added notification module for AMS humidity and temperature
This commit is contained in:
@@ -45,6 +45,9 @@ def _provider_to_dict(provider: NotificationProvider) -> dict:
|
||||
"on_printer_error": provider.on_printer_error,
|
||||
"on_filament_low": provider.on_filament_low,
|
||||
"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_enabled": provider.quiet_hours_enabled,
|
||||
"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_filament_low=provider_data.on_filament_low,
|
||||
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_enabled=provider_data.quiet_hours_enabled,
|
||||
quiet_hours_start=provider_data.quiet_hours_start,
|
||||
|
||||
@@ -954,6 +954,8 @@ _ams_history_task: asyncio.Task | None = None
|
||||
AMS_HISTORY_INTERVAL = 300 # Record every 5 minutes
|
||||
AMS_HISTORY_RETENTION_DAYS = 30 # Keep data for 30 days
|
||||
_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():
|
||||
@@ -968,6 +970,7 @@ async def record_ams_history():
|
||||
try:
|
||||
from backend.app.models.ams_history import AMSSensorHistory
|
||||
from backend.app.models.printer import Printer
|
||||
from backend.app.models.settings import Settings
|
||||
|
||||
async with async_session() as db:
|
||||
# Get all active printers
|
||||
@@ -976,6 +979,24 @@ async def record_ams_history():
|
||||
)
|
||||
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
|
||||
for printer in printers:
|
||||
# Get current state from printer manager
|
||||
@@ -1030,6 +1051,42 @@ async def record_ams_history():
|
||||
db.add(history)
|
||||
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()
|
||||
if recorded_count > 0:
|
||||
logger.info(f"Recorded {recorded_count} AMS sensor history entries")
|
||||
|
||||
@@ -72,6 +72,10 @@ class NotificationProvider(Base):
|
||||
on_filament_low = Column(Boolean, default=False)
|
||||
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_enabled = Column(Boolean, default=False)
|
||||
quiet_hours_start = Column(String(5), nullable=True) # HH:MM format, e.g., "22:00"
|
||||
|
||||
@@ -81,6 +81,18 @@ DEFAULT_TEMPLATES = [
|
||||
"title_template": "Maintenance Due",
|
||||
"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",
|
||||
"name": "Test Notification",
|
||||
|
||||
@@ -38,6 +38,10 @@ class NotificationProviderBase(BaseModel):
|
||||
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")
|
||||
|
||||
# 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_enabled: bool = Field(default=False, description="Enable quiet hours")
|
||||
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_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_enabled: bool | None = None
|
||||
quiet_hours_start: str | None = None
|
||||
|
||||
@@ -18,6 +18,8 @@ class EventType(str, Enum):
|
||||
PRINTER_ERROR = "printer_error"
|
||||
FILAMENT_LOW = "filament_low"
|
||||
MAINTENANCE_DUE = "maintenance_due"
|
||||
AMS_HUMIDITY_HIGH = "ams_humidity_high"
|
||||
AMS_TEMPERATURE_HIGH = "ams_temperature_high"
|
||||
TEST = "test"
|
||||
|
||||
|
||||
@@ -32,6 +34,8 @@ EVENT_VARIABLES: dict[str, list[str]] = {
|
||||
"printer_error": ["printer", "error_type", "error_detail", "timestamp", "app_name"],
|
||||
"filament_low": ["printer", "slot", "remaining_percent", "color", "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"],
|
||||
}
|
||||
|
||||
@@ -101,6 +105,22 @@ SAMPLE_DATA: dict[str, dict[str, str]] = {
|
||||
"timestamp": "2024-01-15 14:30",
|
||||
"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": {
|
||||
"app_name": "Bambuddy",
|
||||
"timestamp": "2024-01-15 14:30",
|
||||
|
||||
@@ -487,12 +487,17 @@ class NotificationService:
|
||||
event_type: str = "unknown",
|
||||
printer_id: int | 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:
|
||||
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(
|
||||
provider=provider,
|
||||
event_type=event_type,
|
||||
@@ -729,6 +734,56 @@ class NotificationService:
|
||||
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)
|
||||
|
||||
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):
|
||||
"""Clear the template cache. Call this when templates are updated."""
|
||||
self._template_cache.clear()
|
||||
|
||||
@@ -783,6 +783,9 @@ export interface NotificationProvider {
|
||||
on_printer_error: boolean;
|
||||
on_filament_low: boolean;
|
||||
on_maintenance_due: boolean;
|
||||
// AMS environmental alarms
|
||||
on_ams_humidity_high: boolean;
|
||||
on_ams_temperature_high: boolean;
|
||||
// Quiet hours
|
||||
quiet_hours_enabled: boolean;
|
||||
quiet_hours_start: string | null;
|
||||
@@ -817,6 +820,9 @@ export interface NotificationProviderCreate {
|
||||
on_printer_error?: boolean;
|
||||
on_filament_low?: boolean;
|
||||
on_maintenance_due?: boolean;
|
||||
// AMS environmental alarms
|
||||
on_ams_humidity_high?: boolean;
|
||||
on_ams_temperature_high?: boolean;
|
||||
// Quiet hours
|
||||
quiet_hours_enabled?: boolean;
|
||||
quiet_hours_start?: string | null;
|
||||
@@ -844,6 +850,9 @@ export interface NotificationProviderUpdate {
|
||||
on_printer_error?: boolean;
|
||||
on_filament_low?: boolean;
|
||||
on_maintenance_due?: boolean;
|
||||
// AMS environmental alarms
|
||||
on_ams_humidity_high?: boolean;
|
||||
on_ams_temperature_high?: boolean;
|
||||
// Quiet hours
|
||||
quiet_hours_enabled?: boolean;
|
||||
quiet_hours_start?: string | null;
|
||||
|
||||
@@ -147,6 +147,12 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
|
||||
{provider.on_maintenance_due && (
|
||||
<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 && (
|
||||
<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" />
|
||||
@@ -317,6 +323,33 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
|
||||
</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 */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
-2
@@ -23,8 +23,8 @@
|
||||
|
||||
<!-- Splash screens for iOS -->
|
||||
<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>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Reference in New Issue
Block a user