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], }