119 lines
4.6 KiB
Python
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],
|
|
}
|