mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-17 19:36:28 +02:00
- Fixed bug in printer's hour counter
This commit is contained in:
@@ -4,12 +4,11 @@ import logging
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from backend.app.core.database import get_db
|
||||
from backend.app.models.archive import PrintArchive
|
||||
from backend.app.models.maintenance import MaintenanceHistory, MaintenanceType, PrinterMaintenance
|
||||
from backend.app.models.printer import Printer
|
||||
from backend.app.schemas.maintenance import (
|
||||
@@ -71,23 +70,24 @@ DEFAULT_MAINTENANCE_TYPES = [
|
||||
|
||||
|
||||
async def get_printer_total_hours(db: AsyncSession, printer_id: int) -> float:
|
||||
"""Calculate total print hours for a printer from archives plus offset.
|
||||
"""Calculate total active hours for a printer from runtime counter plus offset.
|
||||
|
||||
Includes ALL prints (completed, failed, cancelled) since the printer
|
||||
components are being used regardless of print outcome.
|
||||
Uses the runtime_seconds counter which tracks actual machine active time
|
||||
(RUNNING and PAUSE states), including calibration, heating, and printing.
|
||||
"""
|
||||
# Get archive hours (all prints, not just completed)
|
||||
# Get printer runtime and offset
|
||||
result = await db.execute(
|
||||
select(func.sum(PrintArchive.print_time_seconds)).where(PrintArchive.printer_id == printer_id)
|
||||
select(Printer.runtime_seconds, Printer.print_hours_offset).where(Printer.id == printer_id)
|
||||
)
|
||||
total_seconds = result.scalar() or 0
|
||||
archive_hours = total_seconds / 3600.0
|
||||
row = result.one_or_none()
|
||||
if not row:
|
||||
return 0.0
|
||||
|
||||
# Get printer offset
|
||||
result = await db.execute(select(Printer.print_hours_offset).where(Printer.id == printer_id))
|
||||
offset = result.scalar() or 0.0
|
||||
runtime_seconds = row[0] or 0
|
||||
offset = row[1] or 0.0
|
||||
|
||||
return archive_hours + offset
|
||||
runtime_hours = runtime_seconds / 3600.0
|
||||
return runtime_hours + offset
|
||||
|
||||
|
||||
async def ensure_default_types(db: AsyncSession) -> None:
|
||||
@@ -564,23 +564,23 @@ async def set_printer_hours(
|
||||
total_hours: float,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Set the total print hours for a printer (adjusts offset to match)."""
|
||||
"""Set the total print hours for a printer (adjusts offset to match).
|
||||
|
||||
The offset is calculated as: offset = total_hours - runtime_hours
|
||||
Where runtime_hours comes from the runtime_seconds counter that tracks
|
||||
actual machine active time (RUNNING/PAUSE states).
|
||||
"""
|
||||
# Get printer
|
||||
result = await db.execute(select(Printer).where(Printer.id == printer_id))
|
||||
printer = result.scalar_one_or_none()
|
||||
if not printer:
|
||||
raise HTTPException(status_code=404, detail="Printer not found")
|
||||
|
||||
# Get current archive hours (all prints, not just completed)
|
||||
# Must match get_printer_total_hours() which includes all prints
|
||||
result = await db.execute(
|
||||
select(func.sum(PrintArchive.print_time_seconds)).where(PrintArchive.printer_id == printer_id)
|
||||
)
|
||||
total_seconds = result.scalar() or 0
|
||||
archive_hours = total_seconds / 3600.0
|
||||
# Get current runtime hours
|
||||
runtime_hours = (printer.runtime_seconds or 0) / 3600.0
|
||||
|
||||
# Calculate needed offset
|
||||
printer.print_hours_offset = max(0, total_hours - archive_hours)
|
||||
printer.print_hours_offset = max(0, total_hours - runtime_hours)
|
||||
|
||||
await db.commit()
|
||||
|
||||
@@ -611,6 +611,6 @@ async def set_printer_hours(
|
||||
return {
|
||||
"printer_id": printer_id,
|
||||
"total_hours": total_hours,
|
||||
"archive_hours": archive_hours,
|
||||
"runtime_hours": runtime_hours,
|
||||
"offset_hours": printer.print_hours_offset,
|
||||
}
|
||||
|
||||
@@ -365,6 +365,16 @@ async def run_migrations(conn):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Migration: Add runtime tracking columns to printers
|
||||
try:
|
||||
await conn.execute(text("ALTER TABLE printers ADD COLUMN runtime_seconds INTEGER DEFAULT 0"))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
await conn.execute(text("ALTER TABLE printers ADD COLUMN last_runtime_update DATETIME"))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
async def seed_notification_templates():
|
||||
"""Seed default notification templates if they don't exist."""
|
||||
|
||||
@@ -1440,6 +1440,83 @@ def stop_ams_history_recording():
|
||||
logging.getLogger(__name__).info("AMS history recording stopped")
|
||||
|
||||
|
||||
# Printer runtime tracking
|
||||
_runtime_tracking_task: asyncio.Task | None = None
|
||||
RUNTIME_TRACKING_INTERVAL = 30 # Update every 30 seconds
|
||||
|
||||
|
||||
async def track_printer_runtime():
|
||||
"""Background task to track printer active runtime (RUNNING/PAUSE states)."""
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Wait for MQTT connections to establish on startup
|
||||
await asyncio.sleep(15)
|
||||
|
||||
while True:
|
||||
try:
|
||||
from backend.app.models.printer import Printer
|
||||
|
||||
async with async_session() as db:
|
||||
# Get all active printers
|
||||
result = await db.execute(select(Printer).where(Printer.is_active.is_(True)))
|
||||
printers = result.scalars().all()
|
||||
|
||||
now = datetime.now()
|
||||
updated_count = 0
|
||||
|
||||
for printer in printers:
|
||||
# Get current state from printer manager
|
||||
state = printer_manager.get_status(printer.id)
|
||||
if not state or not state.connected:
|
||||
continue
|
||||
|
||||
# Check if printer is in an active state (RUNNING or PAUSE)
|
||||
if state.state in ("RUNNING", "PAUSE"):
|
||||
# Calculate time since last update
|
||||
if printer.last_runtime_update:
|
||||
elapsed = (now - printer.last_runtime_update).total_seconds()
|
||||
# Sanity check: don't add more than 2x the interval (handles server restarts)
|
||||
if elapsed > 0 and elapsed < RUNTIME_TRACKING_INTERVAL * 2:
|
||||
printer.runtime_seconds += int(elapsed)
|
||||
updated_count += 1
|
||||
|
||||
printer.last_runtime_update = now
|
||||
else:
|
||||
# Printer is idle/offline - clear last_runtime_update
|
||||
printer.last_runtime_update = None
|
||||
|
||||
if updated_count > 0:
|
||||
await db.commit()
|
||||
logger.debug(f"Updated runtime for {updated_count} printer(s)")
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.info("Runtime tracking cancelled")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.warning(f"Runtime tracking failed: {e}")
|
||||
|
||||
await asyncio.sleep(RUNTIME_TRACKING_INTERVAL)
|
||||
|
||||
|
||||
def start_runtime_tracking():
|
||||
"""Start the printer runtime tracking background task."""
|
||||
global _runtime_tracking_task
|
||||
if _runtime_tracking_task is None:
|
||||
_runtime_tracking_task = asyncio.create_task(track_printer_runtime())
|
||||
logging.getLogger(__name__).info("Printer runtime tracking started")
|
||||
|
||||
|
||||
def stop_runtime_tracking():
|
||||
"""Stop the printer runtime tracking background task."""
|
||||
global _runtime_tracking_task
|
||||
if _runtime_tracking_task:
|
||||
_runtime_tracking_task.cancel()
|
||||
_runtime_tracking_task = None
|
||||
logging.getLogger(__name__).info("Printer runtime tracking stopped")
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# Startup
|
||||
@@ -1489,6 +1566,9 @@ async def lifespan(app: FastAPI):
|
||||
# Start AMS history recording
|
||||
start_ams_history_recording()
|
||||
|
||||
# Start printer runtime tracking
|
||||
start_runtime_tracking()
|
||||
|
||||
# Start anonymous telemetry (opt-out via settings)
|
||||
asyncio.create_task(start_telemetry_loop(async_session))
|
||||
|
||||
@@ -1524,6 +1604,7 @@ async def lifespan(app: FastAPI):
|
||||
smart_plug_manager.stop_scheduler()
|
||||
notification_service.stop_digest_scheduler()
|
||||
stop_ams_history_recording()
|
||||
stop_runtime_tracking()
|
||||
printer_manager.disconnect_all()
|
||||
await close_spoolman_client()
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from datetime import datetime
|
||||
from sqlalchemy import String, Boolean, DateTime, Float, func
|
||||
|
||||
from sqlalchemy import Boolean, DateTime, Float, String, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from backend.app.core.database import Base
|
||||
@@ -19,37 +20,27 @@ class Printer(Base):
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
auto_archive: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
print_hours_offset: Mapped[float] = mapped_column(Float, default=0.0) # Baseline hours to add
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, server_default=func.now()
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime, server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
runtime_seconds: Mapped[int] = mapped_column(default=0) # Accumulated active runtime (RUNNING/PAUSE states)
|
||||
last_runtime_update: Mapped[datetime | None] = mapped_column(
|
||||
DateTime, nullable=True
|
||||
) # Last time runtime was updated
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
||||
updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||
|
||||
# Relationships
|
||||
archives: Mapped[list["PrintArchive"]] = relationship(
|
||||
back_populates="printer", cascade="all, delete-orphan"
|
||||
)
|
||||
smart_plug: Mapped["SmartPlug | None"] = relationship(
|
||||
back_populates="printer", uselist=False
|
||||
)
|
||||
notification_providers: Mapped[list["NotificationProvider"]] = relationship(
|
||||
back_populates="printer"
|
||||
)
|
||||
archives: Mapped[list["PrintArchive"]] = relationship(back_populates="printer", cascade="all, delete-orphan")
|
||||
smart_plug: Mapped["SmartPlug | None"] = relationship(back_populates="printer", uselist=False)
|
||||
notification_providers: Mapped[list["NotificationProvider"]] = relationship(back_populates="printer")
|
||||
maintenance_items: Mapped[list["PrinterMaintenance"]] = relationship(
|
||||
back_populates="printer", cascade="all, delete-orphan"
|
||||
)
|
||||
kprofile_notes: Mapped[list["KProfileNote"]] = relationship(
|
||||
back_populates="printer", cascade="all, delete-orphan"
|
||||
)
|
||||
ams_history: Mapped[list["AMSSensorHistory"]] = relationship(
|
||||
back_populates="printer", cascade="all, delete-orphan"
|
||||
)
|
||||
kprofile_notes: Mapped[list["KProfileNote"]] = relationship(back_populates="printer", cascade="all, delete-orphan")
|
||||
ams_history: Mapped[list["AMSSensorHistory"]] = relationship(back_populates="printer", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
from backend.app.models.archive import PrintArchive # noqa: E402
|
||||
from backend.app.models.ams_history import AMSSensorHistory # noqa: E402
|
||||
from backend.app.models.archive import PrintArchive # noqa: E402
|
||||
from backend.app.models.kprofile_note import KProfileNote # noqa: E402
|
||||
from backend.app.models.smart_plug import SmartPlug # noqa: E402
|
||||
from backend.app.models.notification import NotificationProvider # noqa: E402
|
||||
from backend.app.models.maintenance import PrinterMaintenance # noqa: E402
|
||||
from backend.app.models.notification import NotificationProvider # noqa: E402
|
||||
from backend.app.models.smart_plug import SmartPlug # noqa: E402
|
||||
|
||||
@@ -340,7 +340,7 @@ function PrinterSection({
|
||||
className="group"
|
||||
>
|
||||
<div className="text-sm font-medium text-white group-hover:text-bambu-green transition-colors flex items-center gap-1">
|
||||
{Math.floor(overview.total_print_hours)} hours
|
||||
{Math.round(overview.total_print_hours)} hours
|
||||
<Edit3 className="w-3 h-3 text-bambu-gray group-hover:text-bambu-green" />
|
||||
</div>
|
||||
<div className="text-xs text-bambu-gray">Total print time</div>
|
||||
|
||||
Reference in New Issue
Block a user