retrieved event working

This commit is contained in:
GotthardG 2024-11-22 22:04:44 +01:00
parent c7e6c0390e
commit fc8bb8d200
6 changed files with 155 additions and 112 deletions

View File

@ -11,8 +11,8 @@ slotQRCodes = [
"C1-X10SA", "C2-X10SA", "C3-X10SA", "C4-X10SA", "C5-X10SA", "C1-X10SA", "C2-X10SA", "C3-X10SA", "C4-X10SA", "C5-X10SA",
"D1-X10SA", "D2-X10SA", "D3-X10SA", "D4-X10SA", "D5-X10SA", "D1-X10SA", "D2-X10SA", "D3-X10SA", "D4-X10SA", "D5-X10SA",
"NB1", "NB2", "NB3", "NB4", "NB5", "NB6", "NB1", "NB2", "NB3", "NB4", "NB5", "NB6",
"X10SA-beamline", "X06SA-beamline", "X06DA-beamline", "X10SA-Beamline", "X06SA-Beamline", "X06DA-Beamline",
"X10SA-outgoing", "X06-outgoing" "Outgoing X10SA", "Outgoing X06SA"
] ]
def timedelta_to_str(td: timedelta) -> str: def timedelta_to_str(td: timedelta) -> str:

View File

@ -143,8 +143,7 @@ class Slot(Base):
qr_base = Column(String, nullable=True) qr_base = Column(String, nullable=True)
occupied = Column(Boolean, default=False) occupied = Column(Boolean, default=False)
needs_refill = Column(Boolean, default=False) needs_refill = Column(Boolean, default=False)
dewar_name = Column(String) # Ensure this field exists dewar_unique_id = Column(String, ForeignKey('dewars.unique_id'), nullable=True)
dewar_unique_id = Column(String, ForeignKey('dewars.unique_id'), nullable=True) # Added field
dewar = relationship("Dewar", back_populates="slot") dewar = relationship("Dewar", back_populates="slot")
events = relationship("LogisticsEvent", back_populates="slot") events = relationship("LogisticsEvent", back_populates="slot")
@ -152,8 +151,8 @@ class LogisticsEvent(Base):
__tablename__ = "logistics_events" __tablename__ = "logistics_events"
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
dewar_id = Column(Integer, ForeignKey('dewars.id')) # corrected table name dewar_id = Column(Integer, ForeignKey('dewars.id'))
slot_id = Column(Integer, ForeignKey('slots.id')) # corrected table name slot_id = Column(Integer, ForeignKey('slots.id'))
event_type = Column(String, index=True) event_type = Column(String, index=True)
timestamp = Column(DateTime, default=datetime.utcnow) timestamp = Column(DateTime, default=datetime.utcnow)
dewar = relationship("Dewar", back_populates="events") dewar = relationship("Dewar", back_populates="events")

View File

@ -1,5 +1,5 @@
from fastapi import APIRouter, HTTPException, Depends from fastapi import APIRouter, HTTPException, Depends
from sqlalchemy.orm import Session from sqlalchemy.orm import Session, joinedload
from typing import List, Optional from typing import List, Optional
from ..models import Dewar as DewarModel, Slot as SlotModel, LogisticsEvent as LogisticsEventModel from ..models import Dewar as DewarModel, Slot as SlotModel, LogisticsEvent as LogisticsEventModel
from ..schemas import LogisticsEventCreate, SlotSchema, Dewar as DewarSchema from ..schemas import LogisticsEventCreate, SlotSchema, Dewar as DewarSchema
@ -13,7 +13,7 @@ logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def calculate_time_until_refill(last_refill: Optional[datetime], refill_interval_hours: int = 24) -> int: def calculate_time_until_refill(last_refill: Optional[datetime], refill_interval_hours: int = 1) -> int:
refill_interval = timedelta(hours=refill_interval_hours) refill_interval = timedelta(hours=refill_interval_hours)
now = datetime.now() now = datetime.now()
@ -24,42 +24,124 @@ def calculate_time_until_refill(last_refill: Optional[datetime], refill_interval
return max(0, int(time_until_next_refill.total_seconds())) return max(0, int(time_until_next_refill.total_seconds()))
@router.post("/dewars/retrieve", response_model=DewarSchema)
async def retrieve_dewar(data: LogisticsEventCreate, db: Session = Depends(get_db)):
logger.info(f"Received data for retrieve_dewar: {data}")
dewar = db.query(DewarModel).filter(DewarModel.unique_id == data.dewar_qr_code).first()
if not dewar:
raise HTTPException(status_code=404, detail="Dewar not found")
new_event = LogisticsEventModel(
dewar_id=dewar.id,
slot_id=None,
event_type="retrieved",
timestamp=datetime.now(),
)
db.add(new_event)
db.commit()
db.refresh(dewar)
# Get the last retrieved event for the dewar
last_retrieved_event = db.query(LogisticsEventModel).filter(
LogisticsEventModel.dewar_id == dewar.id,
LogisticsEventModel.event_type == "retrieved"
).order_by(LogisticsEventModel.timestamp.desc()).first()
if last_retrieved_event:
dewar.last_retrieved_timestamp = last_retrieved_event.timestamp.isoformat()
logger.info(f"Last retrieved event timestamp for dewar {dewar.unique_id}: "
f"{dewar.last_retrieved_timestamp}")
else:
dewar.last_retrieved_timestamp = None
logger.info(f"No retrieved event found for dewar {dewar.unique_id}")
return dewar
@router.post("/dewar/scan", response_model=dict)
async def scan_dewar(event_data: LogisticsEventCreate, db: Session = Depends(get_db)):
dewar_qr_code = event_data.dewar_qr_code
location_qr_code = event_data.location_qr_code
transaction_type = event_data.transaction_type
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:
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:
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:
raise HTTPException(status_code=400, detail="Beamline location not found")
dewar.beamline_location = location_qr_code
# Create a logistics event
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]) @router.get("/slots", response_model=List[SlotSchema])
async def get_all_slots(db: Session = Depends(get_db)): async def get_all_slots(db: Session = Depends(get_db)):
slots = db.query(SlotModel).all() slots = db.query(SlotModel).options(joinedload(SlotModel.dewar)).all()
slots_with_refill_time = [] slots_with_refill_time = []
for slot in slots: for slot in slots:
time_until_refill = None time_until_refill = None
if slot.dewar_unique_id: retrievedTimestamp = None
logger.info(f"Fetching last refill event for dewar: {slot.dewar_unique_id}")
try: if slot.dewar_unique_id:
last_refill_event = db.query(LogisticsEventModel) \ last_refill_event = db.query(LogisticsEventModel) \
.join(DewarModel, DewarModel.id == LogisticsEventModel.dewar_id) \ .join(DewarModel, DewarModel.id == LogisticsEventModel.dewar_id) \
.filter( .filter(
DewarModel.unique_id == slot.dewar_unique_id, DewarModel.unique_id == slot.dewar.unique_id,
LogisticsEventModel.event_type == "refill" LogisticsEventModel.event_type == "refill"
) \ ) \
.order_by(LogisticsEventModel.timestamp.desc()) \ .order_by(LogisticsEventModel.timestamp.desc()) \
.first() .first()
except Exception as e:
logger.error(f"Error querying last refill event: {str(e)}")
last_refill_event = None
if last_refill_event: if last_refill_event:
last_refill = last_refill_event.timestamp last_refill = last_refill_event.timestamp
time_until_refill = calculate_time_until_refill(last_refill) time_until_refill = calculate_time_until_refill(last_refill)
logger.info(f"Slot ID: {slot.id}, Last Refill: {last_refill}, Time Until Refill: {time_until_refill}")
else: else:
logger.warning(f"Slot ID: {slot.id} for dewar id '{slot.dewar_unique_id}' has no refill events.")
time_until_refill = -1 time_until_refill = -1
else:
logger.warning(f"Slot ID: {slot.id} has no dewar associated.")
time_until_refill = -1
# Ensure Dewar.name is optional if it may not exist in Dewar schema. # Get last retrieved timestamp
dewar_name = slot.dewar.name if slot.dewar and hasattr(slot.dewar, 'name') else None 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()
# Determine if the dewar is at the beamline
at_beamline = False
if slot.dewar_unique_id:
at_beamline_event = db.query(LogisticsEventModel) \
.join(DewarModel, DewarModel.id == LogisticsEventModel.dewar_id) \
.filter(
DewarModel.unique_id == slot.dewar.unique_id,
LogisticsEventModel.event_type == "beamline"
) \
.order_by(LogisticsEventModel.timestamp.desc()) \
.first()
at_beamline = bool(at_beamline_event)
slot_data = SlotSchema( slot_data = SlotSchema(
id=slot.id, id=slot.id,
@ -69,29 +151,23 @@ async def get_all_slots(db: Session = Depends(get_db)):
occupied=slot.occupied, occupied=slot.occupied,
needs_refill=slot.needs_refill, needs_refill=slot.needs_refill,
dewar_unique_id=slot.dewar_unique_id, dewar_unique_id=slot.dewar_unique_id,
dewar_name=dewar_name, # Using the optional dewar_name dewar_name=slot.dewar.dewar_name if slot.dewar else None,
time_until_refill=(time_until_refill or -1) time_until_refill=time_until_refill,
at_beamline=at_beamline,
retrievedTimestamp=retrievedTimestamp,
) )
logger.info(f"Dewar retrieved on the: {retrievedTimestamp}")
slots_with_refill_time.append(slot_data) slots_with_refill_time.append(slot_data)
return slots_with_refill_time
@router.get("/dewars", response_model=List[DewarSchema]) return slots_with_refill_time
async def get_all_dewars(db: Session = Depends(get_db)):
dewars = db.query(DewarModel).all()
return dewars
@router.post("/dewar/refill", response_model=dict) @router.post("/dewar/refill", response_model=dict)
async def refill_dewar(qr_code: str, db: Session = Depends(get_db)): async def refill_dewar(qr_code: str, db: Session = Depends(get_db)):
logger.info(f"Refilling dewar with QR code: {qr_code}") logger.info(f"Refilling dewar with QR code: {qr_code}")
if not isinstance(qr_code, str):
raise HTTPException(status_code=400, detail="Invalid QR code format")
dewar = db.query(DewarModel).filter(DewarModel.unique_id == qr_code.strip()).first() dewar = db.query(DewarModel).filter(DewarModel.unique_id == qr_code.strip()).first()
if not dewar: if not dewar:
logger.error("Dewar not found") logger.error("Dewar not found")
raise HTTPException(status_code=404, detail="Dewar not found") raise HTTPException(status_code=404, detail="Dewar not found")
@ -112,6 +188,13 @@ async def refill_dewar(qr_code: str, db: Session = Depends(get_db)):
return {"message": "Dewar refilled successfully", "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) @router.get("/dewar/{unique_id}", response_model=DewarSchema)
async def get_dewar_by_unique_id(unique_id: str, db: Session = Depends(get_db)): 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}") logger.info(f"Received request for dewar with unique_id: {unique_id}")
@ -123,39 +206,13 @@ async def get_dewar_by_unique_id(unique_id: str, db: Session = Depends(get_db)):
return dewar return dewar
@router.post("/dewar/scan", response_model=dict) def log_event(db: Session, dewar_id: int, slot_id: Optional[int], event_type: str):
async def scan_dewar(event_data: LogisticsEventCreate, db: Session = Depends(get_db)): new_event = LogisticsEventModel(
dewar_qr_code = event_data.dewar_qr_code dewar_id=dewar_id,
location_qr_code = event_data.location_qr_code slot_id=slot_id,
transaction_type = event_data.transaction_type event_type=event_type,
timestamp=datetime.now()
logger.info(f"Scanning dewar {dewar_qr_code} for slot {location_qr_code} with transaction type {transaction_type}") )
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:
raise HTTPException(status_code=400, detail="Slot not found or already occupied")
logger.info(f"Associating dewar {dewar.unique_id} with slot {slot.qr_code}")
slot.dewar_unique_id = dewar.unique_id # Properly associate with the unique_id
slot.occupied = True
elif transaction_type == 'outgoing':
if not slot or not slot.occupied or slot.dewar_unique_id != dewar.unique_id:
raise HTTPException(status_code=400, detail="Slot not found or dewar not associated with slot")
logger.info(f"Disassociating dewar {dewar.unique_id} from slot {slot.qr_code}")
slot.dewar_unique_id = None # Remove the association
slot.occupied = False
log_event(db, dewar.id, slot.id if slot else None, transaction_type)
db.commit()
return {"message": "Status updated successfully"}
def log_event(db: Session, dewar_id: int, slot_id: int, event_type: str):
new_event = LogisticsEventModel(dewar_id=dewar_id, slot_id=slot_id, event_type=event_type)
db.add(new_event) db.add(new_event)
db.commit() db.commit()
logger.info(f"Logged event: {event_type} for dewar: {dewar_id} in slot: {slot_id if slot_id else 'N/A'}")

View File

@ -286,7 +286,9 @@ class SlotSchema(BaseModel):
needs_refill: bool needs_refill: bool
dewar_unique_id: Optional[str] dewar_unique_id: Optional[str]
dewar_name: Optional[str] dewar_name: Optional[str]
time_until_refill: Optional[int] # Ensure this field is defined time_until_refill: Optional[int]
at_beamline: Optional[bool]
retrievedTimestamp: Optional[str]
class Config: class Config:
from_attributes = True from_attributes = True

View File

@ -2,8 +2,8 @@ import React, { useEffect } from 'react';
import { Box, Typography, Button, Alert } from '@mui/material'; import { Box, Typography, Button, Alert } from '@mui/material';
import styled from 'styled-components'; import styled from 'styled-components';
import LocalGasStationIcon from '@mui/icons-material/LocalGasStation'; import LocalGasStationIcon from '@mui/icons-material/LocalGasStation';
import LocationOnIcon from '@mui/icons-material/LocationOn'; // New import for location indication
import CountdownTimer from './CountdownTimer'; import CountdownTimer from './CountdownTimer';
import QRCode from 'react-qr-code';
export interface SlotData { export interface SlotData {
id: string; id: string;
@ -15,6 +15,7 @@ export interface SlotData {
dewar_name?: string; dewar_name?: string;
needs_refill?: boolean; needs_refill?: boolean;
time_until_refill?: number; time_until_refill?: number;
at_beamline?: boolean; // New property to indicate the dewar is at the beamline
} }
interface SlotProps { interface SlotProps {
@ -28,6 +29,7 @@ interface SlotProps {
interface StyledSlotProps { interface StyledSlotProps {
isSelected: boolean; isSelected: boolean;
isOccupied: boolean; isOccupied: boolean;
atBeamline: boolean;
} }
const StyledSlot = styled(Box)<StyledSlotProps>` const StyledSlot = styled(Box)<StyledSlotProps>`
@ -35,8 +37,8 @@ const StyledSlot = styled(Box)<StyledSlotProps>`
margin: 8px; margin: 8px;
width: 150px; width: 150px;
height: 260px; height: 260px;
background-color: ${({ isSelected, isOccupied }) => background-color: ${({ isSelected, isOccupied, atBeamline }) =>
isSelected ? '#3f51b5' : isOccupied ? '#f44336' : '#4caf50'}; atBeamline ? '#ff9800' : isSelected ? '#3f51b5' : isOccupied ? '#f44336' : '#4caf50'};
color: white; color: white;
cursor: pointer; cursor: pointer;
display: flex; display: flex;
@ -52,12 +54,6 @@ const StyledSlot = styled(Box)<StyledSlotProps>`
} }
`; `;
const QRCodeContainer = styled.div`
padding: 8px;
background-color: white;
border-radius: 8px;
`;
const BottleIcon: React.FC<{ fillHeight: number }> = ({ fillHeight }) => { const BottleIcon: React.FC<{ fillHeight: number }> = ({ fillHeight }) => {
const pixelHeight = (276.777 * fillHeight) / 100; const pixelHeight = (276.777 * fillHeight) / 100;
const yPosition = 276.777 - pixelHeight; const yPosition = 276.777 - pixelHeight;
@ -80,49 +76,38 @@ const BottleIcon: React.FC<{ fillHeight: number }> = ({ fillHeight }) => {
}; };
const Slot: React.FC<SlotProps> = ({ data, isSelected, onSelect, onRefillDewar, reloadSlots }) => { const Slot: React.FC<SlotProps> = ({ data, isSelected, onSelect, onRefillDewar, reloadSlots }) => {
const { id, qr_code, label, qr_base, occupied, needs_refill, time_until_refill, dewar_unique_id, dewar_name, at_beamline } = data;
const calculateFillHeight = (timeUntilRefill?: number) => { const calculateFillHeight = (timeUntilRefill?: number) => {
if (timeUntilRefill === undefined || timeUntilRefill <= 0) { if (timeUntilRefill === undefined || timeUntilRefill <= 0) {
return 0; return 0;
} }
const maxTime = 86400; const maxTime = 3600;
return Math.min((timeUntilRefill / maxTime) * 100, 100); return Math.min((timeUntilRefill / maxTime) * 100, 100);
}; };
const fillHeight = calculateFillHeight(data.time_until_refill); const fillHeight = calculateFillHeight(time_until_refill);
useEffect(() => { useEffect(() => {
if (data.time_until_refill !== undefined) { if (time_until_refill !== undefined) {
console.log(`Updated time_until_refill: ${data.time_until_refill}`); console.log(`Updated time_until_refill: ${time_until_refill}`);
} }
}, [data.time_until_refill]); }, [time_until_refill]);
const handleRefill = async () => { const handleRefill = async () => {
if (data.dewar_unique_id) { if (dewar_unique_id) {
await onRefillDewar(data.dewar_unique_id); await onRefillDewar(dewar_unique_id);
reloadSlots(); reloadSlots();
} }
}; };
const { id, qr_code, label, qr_base, occupied, needs_refill, time_until_refill, dewar_unique_id, dewar_name, ...rest } = data;
return ( return (
<StyledSlot <StyledSlot isSelected={isSelected} isOccupied={occupied} atBeamline={!!at_beamline} onClick={() => onSelect(data)}>
isSelected={isSelected}
isOccupied={occupied}
onClick={() => onSelect(data)}
{...rest}
>
<Typography variant="h6">{label}</Typography> <Typography variant="h6">{label}</Typography>
{dewar_name && <Typography variant="body2">{`Dewar: ${dewar_name}`}</Typography>} {dewar_name && <Typography variant="body2">{`${dewar_name}`}</Typography>} {/* Ensure correct dewar_name */}
{dewar_unique_id && (
<QRCodeContainer>
<QRCode value={dewar_unique_id} size={64} />
</QRCodeContainer>
)}
{needs_refill && <LocalGasStationIcon />} {needs_refill && <LocalGasStationIcon />}
{dewar_unique_id && ( {dewar_unique_id && <BottleIcon fillHeight={fillHeight} />}
<BottleIcon fillHeight={fillHeight} /> {at_beamline && <Typography style={{fontSize: 12}}><LocationOnIcon /> At Beamline</Typography>} {/* Indicate at beamline */}
)}
{(dewar_unique_id && time_until_refill !== undefined && time_until_refill !== -1) ? ( {(dewar_unique_id && time_until_refill !== undefined && time_until_refill !== -1) ? (
<CountdownTimer key={dewar_unique_id} totalSeconds={time_until_refill} /> <CountdownTimer key={dewar_unique_id} totalSeconds={time_until_refill} />
) : null} ) : null}
@ -136,6 +121,6 @@ const Slot: React.FC<SlotProps> = ({ data, isSelected, onSelect, onRefillDewar,
)} )}
</StyledSlot> </StyledSlot>
); );
}; }
export default Slot; export default Slot;