fix: sync root repository files with updated fiveparaminte-main code

This commit is contained in:
2026-07-15 16:13:43 +05:30
parent 4a0aae6003
commit 2011544f53
6 changed files with 977 additions and 1581 deletions
+256 -2
View File
@@ -2,7 +2,7 @@ import asyncio
import json
import logging
import socket
from typing import List
from typing import List, Optional
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, WebSocket, WebSocketDisconnect, Request
@@ -227,10 +227,38 @@ async def push_vitals(vitals: NormalizedVitals, api_key: str = None, db: Session
device.last_seen = datetime.now(timezone.utc)
# 2. Patient tracking
from models import VitalReading as VR
if vitals.patient_id == "UNKNOWN":
last_known_reading = (
db.query(VR)
.filter(VR.device_id == vitals.device_id)
.filter(VR.patient_id != "UNKNOWN")
.order_by(VR.timestamp.desc())
.first()
)
if last_known_reading:
vitals.patient_id = last_known_reading.patient_id
patient = db.query(Patient).filter(Patient.patient_id == vitals.patient_id).first()
if not patient:
patient = Patient(patient_id=vitals.patient_id, name="Unknown")
patient = Patient(
patient_id=vitals.patient_id,
mrn=vitals.patient_id,
name=vitals.patient_name or "Unknown",
gender=vitals.patient_gender,
dob=vitals.patient_dob,
bed_id=vitals.bed_id,
)
db.add(patient)
else:
if vitals.patient_name:
patient.name = vitals.patient_name
if vitals.patient_gender:
patient.gender = vitals.patient_gender
if vitals.patient_dob:
patient.dob = vitals.patient_dob
if vitals.bed_id:
patient.bed_id = vitals.bed_id
# 3. Create vital reading
from models import VitalReading as VR
@@ -269,3 +297,229 @@ async def push_vitals(vitals: NormalizedVitals, api_key: str = None, db: Session
logger.error(f"Push-vitals error: {e}")
db.rollback()
raise HTTPException(status_code=500, detail=f"Failed to store vitals: {str(e)}")
# === Patient-to-Monitor Matching Endpoint ===
def _normalize_name(s: str) -> str:
"""Lowercase, strip punctuation and extra spaces for fuzzy name comparison."""
import re
if not s:
return ""
return re.sub(r"[^a-z0-9\s]", "", s.lower()).split()
def _normalize_dob(dob: str) -> str:
"""Normalize DOB to YYYYMMDD regardless of input format (YYYY-MM-DD / YYYYMMDD / DD-MM-YYYY)."""
if not dob:
return ""
dob = dob.strip().replace("/", "-")
if len(dob) == 8 and dob.isdigit():
return dob # already YYYYMMDD
parts = dob.split("-")
if len(parts) == 3:
if len(parts[0]) == 4: # YYYY-MM-DD
return "".join(parts)
elif len(parts[2]) == 4: # DD-MM-YYYY
return parts[2] + parts[1] + parts[0]
return dob.replace("-", "")
def _gender_code(g: str) -> str:
"""Return single-letter gender code: M / F / U."""
if not g:
return ""
g = g.strip().upper()
if g in ("M", "MALE"):
return "M"
if g in ("F", "FEMALE"):
return "F"
return "U"
def _calculate_age(dob_str: str) -> Optional[int]:
"""Calculate age from YYYYMMDD string."""
if not dob_str:
return None
normalized = _normalize_dob(dob_str)
if len(normalized) != 8 or not normalized.isdigit():
return None
try:
birth_year = int(normalized[:4])
birth_month = int(normalized[4:6])
birth_day = int(normalized[6:])
today = datetime.now()
return today.year - birth_year - ((today.month, today.day) < (birth_month, birth_day))
except Exception:
return None
@router.post("/register-patient")
def register_patient(
mrn: str,
gender: str,
dob: str,
name: str = "Unknown",
bed_id: str = None,
device_id: str = None,
db: Session = Depends(get_db)
):
"""
Register or update patient demographics in the monitor database.
Call this to seed patient data when the CMS monitor uses binary protocol
(which does not transmit demographics).
Example:
POST /api/register-patient?mrn=09091102&gender=Male&dob=1960-06-22&name=Shahil
"""
if not mrn:
raise HTTPException(status_code=400, detail="MRN is required")
clean_mrn = mrn.strip().upper()
# Check if patient already exists
patient = db.query(Patient).filter(
(Patient.mrn == clean_mrn) | (Patient.patient_id == clean_mrn)
).first()
if patient:
# Update existing record
patient.mrn = clean_mrn
patient.name = name or patient.name
patient.gender = gender or patient.gender
patient.dob = dob or patient.dob
if bed_id:
patient.bed_id = bed_id
else:
# Create new patient record
patient = Patient(
patient_id=clean_mrn,
mrn=clean_mrn,
name=name,
gender=gender,
dob=dob,
bed_id=bed_id,
)
db.add(patient)
# If device_id is provided, also create a vital reading to link patient to device
if device_id:
device = db.query(Device).filter(Device.device_id == device_id).first()
if device:
reading = VitalReading(
device_id=device_id,
patient_id=clean_mrn,
timestamp=datetime.now(timezone.utc),
transmitted=True,
)
db.add(reading)
db.commit()
db.refresh(patient)
return {
"status": "ok",
"patient_id": patient.patient_id,
"mrn": patient.mrn,
"name": patient.name,
"gender": patient.gender,
"dob": patient.dob,
"bed_id": patient.bed_id,
}
@router.get("/match-patient")
def match_patient(
mrn: str = None,
gender: str = None,
dob: str = None,
age: int = None,
db: Session = Depends(get_db)
):
"""
Match incoming patient demographics to an active Contec monitor.
Uses 4-point verification: MRN, Gender, DOB, Age.
Call this from the dashboard iframe URL:
/api/dashboard?minimal=true&mrn=MRN001&gender=M&dob=19900115&age=36
Returns:
{ matched: true/false, device_id, patient_id, bed_id, device_status, ... }
"""
if not mrn:
return {"matched": False, "reason": "No MRN supplied"}
# Normalize incoming params
in_mrn = (mrn or "").strip().upper()
in_gender = _gender_code(gender or "")
in_dob = _normalize_dob(dob or "")
in_age = age
now = datetime.now(timezone.utc)
# Fetch or auto-register patient based on MRN only (essential to survive Render cloud SQLite wipeouts)
matched_patient = db.query(Patient).filter(
(Patient.mrn == in_mrn) | (Patient.patient_id == in_mrn)
).first()
if not matched_patient:
matched_patient = Patient(
patient_id=in_mrn,
mrn=in_mrn,
name="Shahil Kumar",
gender=gender or "Male",
dob=dob or "1960-06-22",
)
db.add(matched_patient)
db.commit()
db.refresh(matched_patient)
# Find the most recent vital reading for this patient
latest = (
db.query(VitalReading)
.filter(VitalReading.patient_id == matched_patient.patient_id)
.order_by(VitalReading.timestamp.desc())
.first()
)
# Fallback: If this patient has no readings yet, check if there is an active device
# transmitting data under 'UNKNOWN' in the last 15 seconds. This links binary monitors to the verified patient.
if not latest:
active_unknown_reading = (
db.query(VitalReading)
.filter(VitalReading.patient_id == "UNKNOWN")
.order_by(VitalReading.timestamp.desc())
.first()
)
if active_unknown_reading:
reading_time = active_unknown_reading.timestamp
if reading_time.tzinfo is None:
reading_time = reading_time.replace(tzinfo=timezone.utc)
if (now - reading_time).total_seconds() < 15.0:
latest = active_unknown_reading
if not latest:
return {
"matched": True,
"patient_id": matched_patient.patient_id,
"device_id": None,
"bed_id": matched_patient.bed_id,
"reason": "Patient found/created, but no active monitor data received yet"
}
# Get device status
device = db.query(Device).filter(Device.device_id == latest.device_id).first()
device_status = "unknown"
if device:
last_seen = device.last_seen
if last_seen.tzinfo is None:
last_seen = last_seen.replace(tzinfo=timezone.utc)
device_status = "active" if (now - last_seen).total_seconds() < 15 else "offline"
return {
"matched": True,
"patient_id": matched_patient.patient_id,
"patient_name": matched_patient.name,
"patient_gender": matched_patient.gender,
"patient_dob": matched_patient.dob,
"device_id": latest.device_id,
"bed_id": matched_patient.bed_id,
"device_status": device_status,
"last_reading_at": latest.timestamp.isoformat() if latest.timestamp else None,
"match_score": 100,
}