Fix maintenance tasks showing wrong rod type for printer models (#351)

H2/A1 series have linear rails, not carbon rods. X1/P1/P2S have carbon
rods, not linear rails. Split rod-specific maintenance into model-aware
pairs: "Lubricate/Clean Carbon Rods" for X1/P1/P2S and "Lubricate/Clean
Linear Rails" for A1/H2. Added rod type classification to printer_models
and startup cleanup for stale/duplicate system maintenance types.
This commit is contained in:
maziggy
2026-02-13 12:23:34 +01:00
parent f9b47282a1
commit cffe7e62bc
11 changed files with 267 additions and 118 deletions
+71 -12
View File
@@ -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 Linear Rails",
"description": "Apply lubricant to linear rails and rods for smooth motion",
"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 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"