Added notification module for AMS humidity and temperature

This commit is contained in:
maziggy
2025-12-10 20:02:22 +01:00
parent 0dc76746b4
commit e392597192
13 changed files with 235 additions and 31 deletions
+6
View File
@@ -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,
+57
View File
@@ -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")
+4
View File
@@ -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",
+8
View File
@@ -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",
+58 -3
View File
@@ -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()
+9
View File
@@ -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
View File
@@ -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>