mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-08 15:11:21 +02:00
Print costs covered filament and energy but not the printer. Each printer can now carry an optional wear cost per printing hour. Every run's wear is its logged duration at that rate, stored on the print log entry; the archive keeps the first run's wear, like energy (#1378). Failed and cancelled runs count, reconciled runs (unknown duration) don't, and a rate applies to new prints only. Wear is shown on the archive card, as a hidden sortable Print Log column, as a Quick Stats tile, in the most expensive record, in project totals and in batch order cost, and is kept by GitHub backup and restore. Billing still charges filament only. New nullable columns: printers.wear_cost_per_hour, print_log_entries.wear_cost and print_archives.wear_cost.
81 lines
5.0 KiB
Python
81 lines
5.0 KiB
Python
from datetime import datetime
|
|
|
|
from sqlalchemy import Boolean, DateTime, Float, String, func
|
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
|
|
from backend.app.core.database import Base
|
|
|
|
|
|
class Printer(Base):
|
|
__tablename__ = "printers"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
name: Mapped[str] = mapped_column(String(100))
|
|
serial_number: Mapped[str] = mapped_column(String(50), unique=True)
|
|
ip_address: Mapped[str] = mapped_column(String(253))
|
|
access_code: Mapped[str] = mapped_column(String(20))
|
|
model: Mapped[str | None] = mapped_column(String(50))
|
|
location: Mapped[str | None] = mapped_column(String(100)) # Group/location name
|
|
nozzle_count: Mapped[int] = mapped_column(default=1) # 1 or 2, auto-detected from MQTT
|
|
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
|
|
# What an hour of printing costs in wear on this machine (#694). Empty or 0
|
|
# leaves wear cost off for the printer.
|
|
wear_cost_per_hour: Mapped[float | None] = mapped_column(Float, nullable=True)
|
|
runtime_seconds: Mapped[int] = mapped_column(
|
|
default=0
|
|
) # Accumulated active runtime (RUNNING state only — see #1521)
|
|
last_runtime_update: Mapped[datetime | None] = mapped_column(
|
|
DateTime, nullable=True
|
|
) # Last time runtime was updated
|
|
# External camera configuration
|
|
external_camera_url: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
|
external_camera_type: Mapped[str | None] = mapped_column(String(20), nullable=True) # mjpeg, rtsp, snapshot
|
|
external_camera_enabled: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
# Optional single-frame snapshot URL — when set, used for snapshot / finish-photo
|
|
# / timelapse / plate-detect captures instead of opening the live stream and
|
|
# skipping a warm-up frame. Bypasses MJPEG warm-up issues on sources that
|
|
# expose a dedicated frame endpoint (e.g. go2rtc's /api/frame.jpeg). #1177.
|
|
external_camera_snapshot_url: Mapped[str | None] = mapped_column(String(500), nullable=True)
|
|
camera_rotation: Mapped[int] = mapped_column(default=0) # 0, 90, 180, 270 degrees
|
|
# Picked for the chamber light while the camera is in use (#1655). Only
|
|
# counts when the app setting camera_light_mode is "selected".
|
|
camera_light_auto: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
# Plate detection - check if build plate is empty before starting print
|
|
plate_detection_enabled: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
# ROI for plate detection (percentages: 0.0-1.0)
|
|
plate_detection_roi_x: Mapped[float | None] = mapped_column(Float, nullable=True) # X start %
|
|
plate_detection_roi_y: Mapped[float | None] = mapped_column(Float, nullable=True) # Y start %
|
|
plate_detection_roi_w: Mapped[float | None] = mapped_column(Float, nullable=True) # Width %
|
|
plate_detection_roi_h: Mapped[float | None] = mapped_column(Float, nullable=True) # Height %
|
|
# Queue: True after a print finishes/fails, until user acknowledges the plate is cleared.
|
|
# Persisted so the gate survives crashes and power cycles (issue #961).
|
|
awaiting_plate_clear: Mapped[bool] = mapped_column(Boolean, default=False)
|
|
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_plugs: Mapped[list["SmartPlug"]] = relationship(back_populates="printer")
|
|
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")
|
|
sensor_history: Mapped[list["PrinterSensorHistory"]] = relationship(
|
|
back_populates="printer", cascade="all, delete-orphan"
|
|
)
|
|
ha_sensors: Mapped[list["PrinterHASensor"]] = relationship(back_populates="printer", cascade="all, delete-orphan")
|
|
|
|
|
|
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.maintenance import PrinterMaintenance # noqa: E402
|
|
from backend.app.models.notification import NotificationProvider # noqa: E402
|
|
from backend.app.models.printer_ha_sensor import PrinterHASensor # noqa: E402
|
|
from backend.app.models.printer_sensor_history import PrinterSensorHistory # noqa: E402
|
|
from backend.app.models.smart_plug import SmartPlug # noqa: E402
|