|
|
@@ -26,6 +26,7 @@ from backend.app.schemas.maintenance import (
|
|
|
PrinterMaintenanceUpdate,
|
|
|
)
|
|
|
from backend.app.services.notification_service import notification_service
|
|
|
+from backend.app.utils.printer_models import get_rod_type
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@@ -33,12 +34,33 @@ router = APIRouter(prefix="/maintenance", tags=["maintenance"])
|
|
|
|
|
|
# Default maintenance types
|
|
|
DEFAULT_MAINTENANCE_TYPES = [
|
|
|
+ # Carbon rod models only (X1/P1/P2S)
|
|
|
+ {
|
|
|
+ "name": "Lubricate Carbon Rods",
|
|
|
+ "description": "Apply lubricant to carbon rods for smooth motion",
|
|
|
+ "default_interval_hours": 50.0,
|
|
|
+ "icon": "Droplet",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ "name": "Clean Carbon Rods",
|
|
|
+ "description": "Wipe carbon rods with a dry cloth",
|
|
|
+ "default_interval_hours": 100.0,
|
|
|
+ "icon": "Sparkles",
|
|
|
+ },
|
|
|
+ # Linear rail models only (A1/H2)
|
|
|
{
|
|
|
"name": "Lubricate Linear Rails",
|
|
|
- "description": "Apply lubricant to linear rails and rods for smooth motion",
|
|
|
+ "description": "Apply lubricant to linear rails for smooth motion",
|
|
|
"default_interval_hours": 50.0,
|
|
|
"icon": "Droplet",
|
|
|
},
|
|
|
+ {
|
|
|
+ "name": "Clean Linear Rails",
|
|
|
+ "description": "Wipe linear rails with a dry cloth to remove dust and debris",
|
|
|
+ "default_interval_hours": 100.0,
|
|
|
+ "icon": "Sparkles",
|
|
|
+ },
|
|
|
+ # Universal (all models)
|
|
|
{
|
|
|
"name": "Clean Nozzle/Hotend",
|
|
|
"description": "Clean nozzle exterior and perform cold pull if needed",
|
|
|
@@ -51,12 +73,6 @@ DEFAULT_MAINTENANCE_TYPES = [
|
|
|
"default_interval_hours": 200.0,
|
|
|
"icon": "Ruler",
|
|
|
},
|
|
|
- {
|
|
|
- "name": "Clean Carbon Rods",
|
|
|
- "description": "Wipe carbon rods with a dry cloth",
|
|
|
- "default_interval_hours": 100.0,
|
|
|
- "icon": "Sparkles",
|
|
|
- },
|
|
|
{
|
|
|
"name": "Clean Build Plate",
|
|
|
"description": "Deep clean build plate with IPA or soap",
|
|
|
@@ -71,6 +87,30 @@ DEFAULT_MAINTENANCE_TYPES = [
|
|
|
},
|
|
|
]
|
|
|
|
|
|
+# System types that only apply to printers with a specific rod/rail type.
|
|
|
+# "carbon" = X1/P1/P2S series (carbon rods), "linear_rail" = A1/H2 series.
|
|
|
+# Types not listed here apply to all printers.
|
|
|
+_ROD_TYPE_REQUIREMENTS: dict[str, str] = {
|
|
|
+ "Lubricate Carbon Rods": "carbon",
|
|
|
+ "Clean Carbon Rods": "carbon",
|
|
|
+ "Lubricate Linear Rails": "linear_rail",
|
|
|
+ "Clean Linear Rails": "linear_rail",
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+def _should_apply_to_printer(type_name: str, printer_model: str | None) -> bool:
|
|
|
+ """Check if a system maintenance type should apply to a given printer model."""
|
|
|
+ rod_requirement = _ROD_TYPE_REQUIREMENTS.get(type_name)
|
|
|
+ if rod_requirement is None:
|
|
|
+ return True # Not model-specific, applies to all
|
|
|
+
|
|
|
+ rod_type = get_rod_type(printer_model)
|
|
|
+ if rod_type is None:
|
|
|
+ # Unknown model — default to carbon rods (legacy behavior)
|
|
|
+ return rod_requirement == "carbon"
|
|
|
+
|
|
|
+ return rod_type == rod_requirement
|
|
|
+
|
|
|
|
|
|
async def get_printer_total_hours(db: AsyncSession, printer_id: int) -> float:
|
|
|
"""Calculate total active hours for a printer from runtime counter plus offset.
|
|
|
@@ -94,13 +134,27 @@ async def get_printer_total_hours(db: AsyncSession, printer_id: int) -> float:
|
|
|
|
|
|
|
|
|
async def ensure_default_types(db: AsyncSession) -> None:
|
|
|
- """Ensure default maintenance types exist."""
|
|
|
- result = await db.execute(select(MaintenanceType).where(MaintenanceType.is_system.is_(True)))
|
|
|
+ """Ensure default maintenance types exist, remove stale/duplicate ones."""
|
|
|
+ result = await db.execute(
|
|
|
+ select(MaintenanceType).where(MaintenanceType.is_system.is_(True)).order_by(MaintenanceType.id)
|
|
|
+ )
|
|
|
existing = result.scalars().all()
|
|
|
- existing_names = {t.name for t in existing}
|
|
|
|
|
|
+ default_names = {t["name"] for t in DEFAULT_MAINTENANCE_TYPES}
|
|
|
+
|
|
|
+ # Remove stale system types no longer in defaults (e.g. renamed types)
|
|
|
+ # and deduplicate: if concurrent requests created the same type twice,
|
|
|
+ # keep only the first (lowest id) and delete the rest.
|
|
|
+ seen_names: set[str] = set()
|
|
|
+ for t in existing:
|
|
|
+ if t.name not in default_names or t.name in seen_names:
|
|
|
+ await db.delete(t)
|
|
|
+ else:
|
|
|
+ seen_names.add(t.name)
|
|
|
+
|
|
|
+ # Create any missing default types
|
|
|
for type_def in DEFAULT_MAINTENANCE_TYPES:
|
|
|
- if type_def["name"] not in existing_names:
|
|
|
+ if type_def["name"] not in seen_names:
|
|
|
new_type = MaintenanceType(
|
|
|
name=type_def["name"],
|
|
|
description=type_def["description"],
|
|
|
@@ -228,6 +282,11 @@ async def _get_printer_maintenance_internal(
|
|
|
now = datetime.utcnow()
|
|
|
|
|
|
for maint_type in all_types:
|
|
|
+ # Skip system types that don't apply to this printer model
|
|
|
+ # (e.g., "Clean Carbon Rods" for H2D which has steel rods)
|
|
|
+ if maint_type.is_system and not _should_apply_to_printer(maint_type.name, printer.model):
|
|
|
+ continue
|
|
|
+
|
|
|
item = existing_items.get(maint_type.id)
|
|
|
default_interval_type = getattr(maint_type, "interval_type", "hours") or "hours"
|
|
|
|