mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-19 15:54:27 +02:00
A group can now be limited to a set of printers. Its members see and control only those printers. Every other printer answers 404, as if it didn't exist. - Groups gain restrict_printers and a group_printers table (migration for SQLite and Postgres). A user's printers are the union of their limited groups. Groups without the flag don't limit anything, a user in no limited group keeps every printer, and admins see all of them. - core/printer_scope.py holds the scope. RequestPrinterScope and RequirePrinterPermissionIfAuthEnabled apply it to routes: printer routes, camera, queue and batches, archives, projects, stats, print log, pipeline runs, inventory and Spoolman assignments, maintenance, smart plugs, scheduled drying, firmware and Obico status. - API keys, camera stream, Cam Wall, overlay and WebSocket tokens carry the printers of whoever created them. WebSocket broadcasts are filtered per connection, and the filtering fails closed. - Scheduler: "Any <model>" jobs stay on their owner's printers. A job pinned to a printer its owner lost waits with a reason. Callers with no user identity and limited printers must queue to a specific printer. - Group editor: new Printer access section, translated into all 15 locales. Saving a system group no longer resends unchanged permissions, which the backend refused.
138 lines
4.6 KiB
Python
138 lines
4.6 KiB
Python
"""API routes for AMS sensor history."""
|
|
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
from fastapi import APIRouter, Depends, Query
|
|
from pydantic import BaseModel
|
|
from sqlalchemy import and_, func, select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from backend.app.core.auth import RequirePrinterPermissionIfAuthEnabled
|
|
from backend.app.core.database import get_db
|
|
from backend.app.core.permissions import Permission
|
|
from backend.app.models.ams_history import AMSSensorHistory
|
|
from backend.app.models.user import User
|
|
|
|
router = APIRouter(prefix="/ams-history", tags=["ams-history"])
|
|
|
|
|
|
class AMSHistoryPoint(BaseModel):
|
|
recorded_at: datetime
|
|
humidity: float | None
|
|
humidity_raw: float | None
|
|
temperature: float | None
|
|
|
|
|
|
class AMSHistoryResponse(BaseModel):
|
|
printer_id: int
|
|
ams_id: int
|
|
data: list[AMSHistoryPoint]
|
|
min_humidity: float | None
|
|
max_humidity: float | None
|
|
avg_humidity: float | None
|
|
min_temperature: float | None
|
|
max_temperature: float | None
|
|
avg_temperature: float | None
|
|
|
|
|
|
@router.get("/{printer_id}/{ams_id}", response_model=AMSHistoryResponse)
|
|
async def get_ams_history(
|
|
printer_id: int,
|
|
ams_id: int,
|
|
hours: int = Query(default=24, ge=1, le=168, description="Hours of history (1-168)"),
|
|
db: AsyncSession = Depends(get_db),
|
|
_: User | None = RequirePrinterPermissionIfAuthEnabled(Permission.AMS_HISTORY_READ),
|
|
):
|
|
"""Get AMS sensor history for a specific printer and AMS unit."""
|
|
since = datetime.now(timezone.utc) - timedelta(hours=hours)
|
|
|
|
# Get data points
|
|
result = await db.execute(
|
|
select(AMSSensorHistory)
|
|
.where(
|
|
and_(
|
|
AMSSensorHistory.printer_id == printer_id,
|
|
AMSSensorHistory.ams_id == ams_id,
|
|
AMSSensorHistory.recorded_at >= since,
|
|
)
|
|
)
|
|
.order_by(AMSSensorHistory.recorded_at)
|
|
)
|
|
records = result.scalars().all()
|
|
|
|
# Calculate stats
|
|
stats_result = await db.execute(
|
|
select(
|
|
func.min(AMSSensorHistory.humidity).label("min_humidity"),
|
|
func.max(AMSSensorHistory.humidity).label("max_humidity"),
|
|
func.avg(AMSSensorHistory.humidity).label("avg_humidity"),
|
|
func.min(AMSSensorHistory.temperature).label("min_temp"),
|
|
func.max(AMSSensorHistory.temperature).label("max_temp"),
|
|
func.avg(AMSSensorHistory.temperature).label("avg_temp"),
|
|
).where(
|
|
and_(
|
|
AMSSensorHistory.printer_id == printer_id,
|
|
AMSSensorHistory.ams_id == ams_id,
|
|
AMSSensorHistory.recorded_at >= since,
|
|
)
|
|
)
|
|
)
|
|
stats = stats_result.one()
|
|
|
|
return AMSHistoryResponse(
|
|
printer_id=printer_id,
|
|
ams_id=ams_id,
|
|
data=[
|
|
AMSHistoryPoint(
|
|
recorded_at=r.recorded_at,
|
|
humidity=r.humidity,
|
|
humidity_raw=r.humidity_raw,
|
|
temperature=r.temperature,
|
|
)
|
|
for r in records
|
|
],
|
|
min_humidity=stats.min_humidity,
|
|
max_humidity=stats.max_humidity,
|
|
# ``is not None``, not truthiness: an average of exactly 0 is a
|
|
# reading, and the min/max beside it would report it while the average
|
|
# showed an em dash. AVG over an empty or all-NULL window is the only
|
|
# case that has no answer (#3140).
|
|
avg_humidity=round(stats.avg_humidity, 1) if stats.avg_humidity is not None else None,
|
|
min_temperature=stats.min_temp,
|
|
max_temperature=stats.max_temp,
|
|
avg_temperature=round(stats.avg_temp, 1) if stats.avg_temp is not None else None,
|
|
)
|
|
|
|
|
|
@router.delete("/{printer_id}")
|
|
async def delete_old_history(
|
|
printer_id: int,
|
|
days: int = Query(default=30, ge=1, le=365, description="Delete data older than X days"),
|
|
db: AsyncSession = Depends(get_db),
|
|
_: User | None = RequirePrinterPermissionIfAuthEnabled(Permission.AMS_HISTORY_READ),
|
|
):
|
|
"""Delete old AMS history data for a printer."""
|
|
cutoff = datetime.now(timezone.utc) - timedelta(days=days)
|
|
|
|
result = await db.execute(
|
|
select(func.count(AMSSensorHistory.id)).where(
|
|
and_(
|
|
AMSSensorHistory.printer_id == printer_id,
|
|
AMSSensorHistory.recorded_at < cutoff,
|
|
)
|
|
)
|
|
)
|
|
count = result.scalar()
|
|
|
|
await db.execute(
|
|
AMSSensorHistory.__table__.delete().where(
|
|
and_(
|
|
AMSSensorHistory.printer_id == printer_id,
|
|
AMSSensorHistory.recorded_at < cutoff,
|
|
)
|
|
)
|
|
)
|
|
await db.commit()
|
|
|
|
return {"deleted": count, "message": f"Deleted {count} records older than {days} days"}
|