aaredb/backend/app/routers/logistics.py
2024-11-28 10:32:00 +01:00

247 lines
10 KiB
Python

from fastapi import APIRouter, HTTPException, Depends
from pydantic import ValidationError
from sqlalchemy.orm import Session, joinedload
from typing import List, Optional
from ..models import Dewar as DewarModel, Slot as SlotModel, LogisticsEvent as LogisticsEventModel
from ..schemas import LogisticsEventCreate, SlotSchema, Dewar as DewarSchema
from ..database import get_db
import logging
from datetime import datetime, timedelta
router = APIRouter()
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def calculate_time_until_refill(last_refill: Optional[datetime], refill_interval_hours: int = 1) -> int:
refill_interval = timedelta(hours=refill_interval_hours)
now = datetime.now()
if not last_refill:
return -1 # Sentinel value indicating no refill event recorded
time_until_next_refill = last_refill + refill_interval - now
return max(0, int(time_until_next_refill.total_seconds()))
@router.post("/dewars/return", response_model=DewarSchema)
async def return_to_storage(data: LogisticsEventCreate, db: Session = Depends(get_db)):
logger.info(f"Returning dewar to storage: {data.dewar_qr_code} at location {data.location_qr_code}")
try:
# Log the incoming payload
logger.info("Received payload: %s", data.json())
dewar = db.query(DewarModel).filter(DewarModel.unique_id == data.dewar_qr_code).first()
if not dewar:
logger.error(f"Dewar not found for unique ID: {data.dewar_qr_code}")
raise HTTPException(status_code=404, detail="Dewar not found")
original_slot = db.query(SlotModel).filter(SlotModel.dewar_unique_id == data.dewar_qr_code).first()
if original_slot and original_slot.qr_code != data.location_qr_code:
logger.error(f"Dewar {data.dewar_qr_code} is associated with slot {original_slot.qr_code}")
raise HTTPException(status_code=400, detail=f"Dewar {data.dewar_qr_code} is associated with a different slot {original_slot.qr_code}.")
slot = db.query(SlotModel).filter(SlotModel.qr_code == data.location_qr_code).first()
if not slot:
logger.error(f"Slot not found for QR code: {data.location_qr_code}")
raise HTTPException(status_code=404, detail="Slot not found")
if slot.occupied and slot.dewar_unique_id != data.dewar_qr_code:
logger.error(f"Slot {data.location_qr_code} is already occupied by another dewar")
raise HTTPException(status_code=400, detail="Selected slot is already occupied by another dewar")
# Update slot with dewar information
slot.dewar_unique_id = dewar.unique_id
slot.occupied = True
dewar.last_retrieved_timestamp = None
# Log the event
log_event(db, dewar.id, slot.id, "returned")
db.commit()
logger.info(f"Dewar {data.dewar_qr_code} successfully returned to storage slot {slot.qr_code}.")
db.refresh(dewar)
return dewar
except ValidationError as e:
logger.error(f"Validation error: {e.json()}")
raise HTTPException(status_code=400, detail="Invalid payload")
except Exception as e:
logger.error(f"Unexpected error: {str(e)}")
raise HTTPException(status_code=500, detail="Internal server error")
@router.post("/dewar/scan", response_model=dict)
async def scan_dewar(event_data: LogisticsEventCreate, db: Session = Depends(get_db)):
logger.info(f"Received event data: {event_data}")
dewar_qr_code = event_data.dewar_qr_code
location_qr_code = event_data.location_qr_code
transaction_type = event_data.transaction_type
if not dewar_qr_code or not dewar_qr_code.strip():
logger.error("Dewar QR Code is null or empty")
raise HTTPException(status_code=422, detail="Dewar QR Code cannot be null or empty")
dewar = db.query(DewarModel).filter(DewarModel.unique_id == dewar_qr_code).first()
if not dewar:
raise HTTPException(status_code=404, detail="Dewar not found")
slot = db.query(SlotModel).filter(SlotModel.qr_code == location_qr_code).first()
if transaction_type == 'incoming':
if not slot or slot.occupied:
logger.error(f"Slot not found or already occupied: {slot}")
raise HTTPException(status_code=400, detail="Slot not found or already occupied")
slot.dewar_unique_id = dewar.unique_id
slot.occupied = True
elif transaction_type == 'outgoing':
if not slot or not slot.occupied or slot.dewar_unique_id != dewar.unique_id:
logger.error(f"Slot not found or dewar not associated with slot: {slot}")
raise HTTPException(status_code=400, detail="Slot not found or dewar not associated with slot")
slot.dewar_unique_id = None
slot.occupied = False
elif transaction_type == 'beamline':
if not slot:
logger.error(f"Beamline location not found: {location_qr_code}")
raise HTTPException(status_code=400, detail="Beamline location not found")
dewar.beamline_location = location_qr_code
logger.info(f"Dewar {dewar_qr_code} assigned to beamline {location_qr_code}")
log_event(db, dewar.id, slot.id if slot else None, transaction_type)
db.commit()
return {"message": "Status updated successfully"}
@router.get("/slots", response_model=List[SlotSchema])
async def get_all_slots(db: Session = Depends(get_db)):
slots = db.query(SlotModel).options(joinedload(SlotModel.dewar)).all()
slots_with_refill_time = []
for slot in slots:
time_until_refill = None
retrievedTimestamp = None
beamlineLocation = None
at_beamline = False
retrieved = False
if slot.dewar_unique_id:
# Calculate time until refill
last_refill_event = db.query(LogisticsEventModel) \
.join(DewarModel, DewarModel.id == LogisticsEventModel.dewar_id) \
.filter(
DewarModel.unique_id == slot.dewar.unique_id,
LogisticsEventModel.event_type == "refill"
) \
.order_by(LogisticsEventModel.timestamp.desc()) \
.first()
if last_refill_event:
last_refill = last_refill_event.timestamp
time_until_refill = calculate_time_until_refill(last_refill)
else:
time_until_refill = -1
# Get last retrieved timestamp
last_retrieved_event = db.query(LogisticsEventModel) \
.join(DewarModel, DewarModel.id == LogisticsEventModel.dewar_id) \
.filter(
DewarModel.unique_id == slot.dewar.unique_id,
LogisticsEventModel.event_type == "retrieved"
) \
.order_by(LogisticsEventModel.timestamp.desc()) \
.first()
if last_retrieved_event:
retrievedTimestamp = last_retrieved_event.timestamp.isoformat()
retrieved = True
# Determine the last event excluding refills
last_event = db.query(LogisticsEventModel) \
.join(DewarModel, DewarModel.id == LogisticsEventModel.dewar_id) \
.filter(
DewarModel.unique_id == slot.dewar.unique_id,
LogisticsEventModel.event_type != "refill"
) \
.order_by(LogisticsEventModel.timestamp.desc()) \
.first()
if last_event:
associated_slot = db.query(SlotModel).filter(SlotModel.id == last_event.slot_id).first()
beamlineLocation = associated_slot.label if associated_slot else None
at_beamline = last_event.event_type == "beamline"
slot_data = SlotSchema(
id=slot.id,
qr_code=slot.qr_code,
label=slot.label,
qr_base=slot.qr_base,
occupied=slot.occupied,
needs_refill=slot.needs_refill,
dewar_unique_id=slot.dewar_unique_id,
dewar_name=slot.dewar.dewar_name if slot.dewar else None,
time_until_refill=time_until_refill,
at_beamline=at_beamline,
retrieved=retrieved,
retrievedTimestamp=retrievedTimestamp,
beamlineLocation=beamlineLocation,
)
logger.info(f"Dewar retrieved: {retrieved}")
logger.info(f"Dewar at: {beamlineLocation}")
slots_with_refill_time.append(slot_data)
return slots_with_refill_time
@router.post("/dewar/refill", response_model=dict)
async def refill_dewar(qr_code: str, db: Session = Depends(get_db)):
logger.info(f"Refilling dewar with QR code: {qr_code}")
dewar = db.query(DewarModel).filter(DewarModel.unique_id == qr_code.strip()).first()
if not dewar:
logger.error("Dewar not found")
raise HTTPException(status_code=404, detail="Dewar not found")
now = datetime.now()
new_event = LogisticsEventModel(
dewar_id=dewar.id,
slot_id=None,
event_type="refill",
timestamp=now,
)
db.add(new_event)
db.commit()
time_until_refill_seconds = calculate_time_until_refill(now)
logger.info(f"Dewar refilled successfully with time_until_refill: {time_until_refill_seconds}")
return {"message": "Dewar refilled successfully", "time_until_refill": time_until_refill_seconds}
@router.get("/dewars", response_model=List[DewarSchema])
async def get_all_dewars(db: Session = Depends(get_db)):
dewars = db.query(DewarModel).all()
return dewars
@router.get("/dewar/{unique_id}", response_model=DewarSchema)
async def get_dewar_by_unique_id(unique_id: str, db: Session = Depends(get_db)):
logger.info(f"Received request for dewar with unique_id: {unique_id}")
dewar = db.query(DewarModel).filter(DewarModel.unique_id == unique_id.strip()).first()
if not dewar:
logger.warning(f"Dewar with unique_id '{unique_id}' not found.")
raise HTTPException(status_code=404, detail="Dewar not found")
logger.info(f"Returning dewar: {dewar}")
return dewar
def log_event(db: Session, dewar_id: int, slot_id: Optional[int], event_type: str):
new_event = LogisticsEventModel(
dewar_id=dewar_id,
slot_id=slot_id,
event_type=event_type,
timestamp=datetime.now()
)
db.add(new_event)
db.commit()
logger.info(f"Logged event: {event_type} for dewar: {dewar_id} in slot: {slot_id if slot_id else 'N/A'}")