Files
plast-track/backend/app/services/dashboard.py
2026-06-15 13:35:22 +01:00

119 lines
4.6 KiB
Python

from __future__ import annotations
from datetime import datetime, time, timedelta, timezone
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from ..models import Cycle, Downtime, Machine, ProductionOrder, Scrap
from .events import get_open_downtime, get_running_order
from .oee import compute_machine_oee
UTC = timezone.utc
def _today_bounds() -> tuple[datetime, datetime]:
start = datetime.combine(datetime.now(UTC).date(), time.min, tzinfo=UTC)
return start, start + timedelta(days=1)
def serialize_machine_config(machine: Machine) -> dict:
return {
"name": machine.name,
"brand": machine.brand,
"model": machine.model,
"is_active": machine.is_active,
"cycle_signal_edge": machine.cycle_signal_edge,
"debounce_ms": machine.debounce_ms,
"min_cycle_time_sec": machine.min_cycle_time_sec,
"max_cycle_time_sec": machine.max_cycle_time_sec,
"stop_detection_delay_sec": machine.stop_detection_delay_sec,
}
def serialize_machine_card(session: Session, machine: Machine) -> dict:
running_order = get_running_order(session, machine.id)
open_downtime = get_open_downtime(session, machine.id)
today_start, today_end = _today_bounds()
recent_cycles = session.execute(
select(Cycle.cycle_time_sec).where(Cycle.machine_id == machine.id).order_by(Cycle.timestamp.desc()).limit(20)
).scalars().all()
avg_cycle = sum(recent_cycles) / len(recent_cycles) if recent_cycles else 0
order_produced_qty = 0
order_scrap_qty = 0
progress = 0
if running_order is not None:
order_produced_qty = session.execute(
select(func.coalesce(func.sum(Cycle.produced_qty), 0)).where(
Cycle.production_order_id == running_order.id,
Cycle.is_valid.is_(True),
)
).scalar_one()
order_scrap_qty = session.execute(
select(func.coalesce(func.sum(Scrap.quantity), 0)).where(Scrap.production_order_id == running_order.id)
).scalar_one()
progress = min(order_produced_qty / running_order.planned_qty, 1) if running_order.planned_qty else 0
daily_oee = compute_machine_oee(session, machine.id, today_start, today_end)
latest_downtime = session.execute(
select(Downtime).where(Downtime.machine_id == machine.id).order_by(Downtime.start_time.desc()).limit(1)
).scalar_one_or_none()
return {
"id": machine.id,
"code": machine.code,
"name": machine.name,
"brand": machine.brand,
"model": machine.model,
"status": machine.status,
"status_since": machine.status_since.isoformat() if machine.status_since else None,
"powered_on": machine.powered_on,
"config": serialize_machine_config(machine),
"active_order": None
if running_order is None
else {
"id": running_order.id,
"order_number": running_order.order_number,
"article_ref": running_order.article_ref,
"article_name": running_order.article_name,
"mold_ref": running_order.mold_ref,
"material_ref": running_order.material_ref,
"planned_qty": running_order.planned_qty,
"cavities": running_order.cavities,
"theoretical_cycle_time_sec": running_order.theoretical_cycle_time_sec,
"status": running_order.status,
},
"cycle_real_avg_sec": round(avg_cycle, 2),
"produced_qty": int(order_produced_qty),
"scrap_qty": int(order_scrap_qty),
"order_progress": round(progress, 4),
"today_oee": daily_oee,
"open_downtime": None
if open_downtime is None
else {
"id": open_downtime.id,
"reason_code": open_downtime.reason_code,
"start_time": open_downtime.start_time.isoformat(),
"comment": open_downtime.comment,
},
"last_downtime": None
if latest_downtime is None
else {
"id": latest_downtime.id,
"reason_code": latest_downtime.reason_code,
"start_time": latest_downtime.start_time.isoformat(),
"end_time": latest_downtime.end_time.isoformat() if latest_downtime.end_time else None,
"duration_sec": latest_downtime.duration_sec,
},
}
def build_dashboard_snapshot(session: Session) -> dict:
machines = session.execute(select(Machine).where(Machine.is_active.is_(True)).order_by(Machine.code)).scalars().all()
return {
"timestamp": datetime.now(UTC).isoformat(),
"machines": [serialize_machine_card(session, machine) for machine in machines],
}