RishiXD commited on
Commit
25fba8b
·
verified ·
1 Parent(s): 060bba1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +344 -336
app.py CHANGED
@@ -1,336 +1,344 @@
1
- from fastapi import FastAPI, HTTPException
2
- from pydantic import BaseModel, validator
3
- import requests
4
- import uvicorn
5
- import firebase_admin
6
- from firebase_admin import credentials, firestore
7
- import logging
8
- from datetime import datetime
9
- import pytz
10
- import os
11
- from fastapi import APIRouter, HTTPException
12
- import requests
13
-
14
- app = APIRouter()
15
- # Set up logging
16
- logging.basicConfig(level=logging.INFO)
17
- logger = logging.getLogger(__name__)
18
-
19
- app = FastAPI()
20
-
21
- # Initialize Firebase Admin SDK
22
- try:
23
- cred = credentials.Certificate("serviceAccountKey.json")
24
- firebase_admin.initialize_app(cred)
25
- db = firestore.client()
26
- except Exception as e:
27
- logger.error(f"Failed to initialize Firebase Admin SDK: {str(e)}")
28
- raise
29
- class UpdateStatusRequest(BaseModel):
30
- userId: str
31
- medicineName: str
32
- medicineTime: str # Expected format: "HH:MM"
33
-
34
- class Credentials(BaseModel):
35
- esp32_ip: str
36
- user: str # Changed from email to user
37
-
38
- @validator("esp32_ip")
39
- def validate_esp32_ip(cls, value):
40
- # Remove http:// or https:// if present
41
- value = value.replace("http://", "").replace("https://", "")
42
- # Check for invalid IP like 0.0.0.0
43
- if "0.0.0.0" in value:
44
- raise ValueError("Invalid ESP32 IP address: 0.0.0.0 is not a valid destination address")
45
- return value
46
-
47
- def convert_to_serializable(obj):
48
- """Convert Firestore timestamps and other non-serializable objects to JSON-serializable format."""
49
- if isinstance(obj, firebase_admin.firestore.firestore.DocumentReference):
50
- return str(obj.path)
51
- if hasattr(obj, 'isoformat'): # Handles DatetimeWithNanoseconds
52
- return obj.isoformat()
53
- if isinstance(obj, dict):
54
- return {k: convert_to_serializable(v) for k, v in obj.items()}
55
- if isinstance(obj, list):
56
- return [convert_to_serializable(item) for item in obj]
57
- return obj
58
-
59
- def convert_times_to_ist(medicine_times):
60
- utc = pytz.utc
61
- ist = pytz.timezone("Asia/Kolkata")
62
- converted_times = []
63
-
64
- for t in medicine_times:
65
- try:
66
- if isinstance(t, datetime):
67
- utc_time = t.astimezone(utc)
68
- elif isinstance(t, str):
69
- if "Z" in t:
70
- utc_time = datetime.fromisoformat(t.replace("Z", "+00:00")).astimezone(utc)
71
- else:
72
- utc_time = datetime.fromisoformat(t).astimezone(utc)
73
- else:
74
- logger.warning(f"[convert_times_to_ist] Unsupported type for medicineTime: {type(t)}")
75
- continue
76
-
77
- ist_time = utc_time.astimezone(ist)
78
- converted_times.append(ist_time.strftime("%H:%M")) # HH:MM format
79
- except Exception as e:
80
- logger.warning(f"[convert_times_to_ist] Error converting time '{t}': {e}")
81
- return converted_times
82
-
83
-
84
- @app.post("/send_medicineData")
85
- async def send_medicineData(data: Credentials):
86
- try:
87
- logger.info(f"Received request: {data}")
88
-
89
- # Step 1: Retrieve the medicines array from Firestore using user ID
90
- logger.info(f"Fetching medicines data for user: {data.user}")
91
- doc_ref = db.collection("users").document(data.user)
92
- doc = doc_ref.get()
93
-
94
- if not doc.exists:
95
- logger.error("User data not found")
96
- raise HTTPException(status_code=404, detail={"error": "User data not found"})
97
-
98
- user_data = doc.to_dict()
99
- medicines_data = user_data.get("medicines", []) # Fetch medicines list safely
100
-
101
- if not medicines_data:
102
- logger.error("Medicines data not found for this user")
103
- raise HTTPException(status_code=404, detail={"error": "Medicines data not found for this user"})
104
-
105
- # Step 2: Extract and filter medicine details based on endDate
106
- current_date = datetime.now(pytz.UTC) # Current date in UTC
107
- logger.info(f"Current date (UTC): {current_date}")
108
-
109
- extracted_medicines = []
110
- for medicine in medicines_data:
111
- end_date_raw = medicine.get("endDate")
112
- if not end_date_raw:
113
- logger.warning(f"Medicine missing endDate: {medicine}")
114
- continue
115
-
116
- try:
117
- # Handle endDate (already converted to datetime by Firestore Admin SDK)
118
- if isinstance(end_date_raw, datetime):
119
- end_date = end_date_raw
120
- if end_date.tzinfo is None: # If no timezone info, assume UTC
121
- end_date = end_date.replace(tzinfo=pytz.UTC)
122
- else:
123
- end_date = end_date.astimezone(pytz.UTC) # Convert to UTC for comparison
124
- elif isinstance(end_date_raw, str):
125
- # Fallback for string format (e.g., "April 17, 2025 at 12:00:00 AM UTC+5:30")
126
- end_date_str = end_date_raw.replace("at ", "").replace("\u202f", " ")
127
- end_date = datetime.strptime(end_date_str, "%B %d, %Y %I:%M:%S %p UTC%z")
128
- end_date = end_date.astimezone(pytz.UTC) # Convert to UTC for comparison
129
- else:
130
- logger.warning(f"Unsupported endDate type: {type(end_date_raw)}, value: {end_date_raw}")
131
- continue
132
-
133
- # Only include medicines with endDate in the future
134
- if end_date > current_date:
135
- extracted_medicines.append({
136
- "doseFrequency": medicine.get("doseFrequency"),
137
- "endDate": medicine.get("endDate"),
138
- "medicineNames": medicine.get("medicineNames", []),
139
- "medicineTimes": convert_times_to_ist(medicine.get("medicineTimes", {})),
140
- "selectedDays": medicine.get("selectedDays", [])
141
- })
142
-
143
- else:
144
- logger.info(f"Skipping medicine with expired endDate: {end_date_raw}")
145
-
146
- except ValueError as e:
147
- logger.warning(f"Failed to parse endDate: {end_date_raw}, error: {str(e)}")
148
- continue
149
-
150
- # Step 3: Check if there are any valid medicines to send
151
- if not extracted_medicines:
152
- logger.info("No medicines with endDate in the future found.")
153
- return {
154
- "message": "No medicines with endDate in the future found.",
155
- "medicines": []
156
- }
157
-
158
- # Convert any non-serializable objects (e.g., timestamps)
159
- extracted_medicines = convert_to_serializable(extracted_medicines)
160
-
161
- # Step 4: Send the medicines data to ESP32
162
- esp32_ip = data.esp32_ip.replace("http://", "").replace("https://", "")
163
- esp32_url = f"http://{esp32_ip}/credentials"
164
- logger.info(f"Sending medicines data to ESP32 at {esp32_url}")
165
- esp32_response = requests.post(esp32_url, json={"medicines": extracted_medicines}, timeout=20)
166
- esp32_response.raise_for_status()
167
-
168
- logger.info("Medicines data sent successfully")
169
- return {
170
- "message": "Medicines data sent successfully",
171
- "medicines": [
172
- {
173
- "medicineNames": med.get("medicineNames", []),
174
- "medicineTimes": med.get("medicineTimes", [])
175
- }
176
- for med in extracted_medicines
177
- ],
178
- "esp32_response": esp32_response.text
179
- }
180
-
181
- except requests.exceptions.RequestException as e:
182
- logger.error(f"Request error: {str(e)}")
183
- raise HTTPException(status_code=500, detail={"error": f"Failed to connect to ESP32: {str(e)}"})
184
- except HTTPException as e:
185
- raise e
186
- except Exception as e:
187
- logger.error(f"Unexpected error: {str(e)}", exc_info=True)
188
- raise HTTPException(status_code=500, detail={"error": f"Unexpected error: {str(e)}"})
189
-
190
- def send_command(data: Credentials, command: str):
191
- try:
192
- esp32_ip = data.esp32_ip.replace("http://", "").replace("https://", "")
193
- url = f"http://{esp32_ip}/command/{command}"
194
- logger.info(f"Sending command: {command} to {url}")
195
- response = requests.get(url, timeout=10)
196
- response.raise_for_status()
197
- return {"message": f"{command.capitalize()} command sent successfully", "esp32_response": response.text}
198
- except requests.exceptions.RequestException as e:
199
- raise HTTPException(status_code=500, detail={"error": f"Failed to send {command} command: {str(e)}"})
200
-
201
-
202
-
203
- def ping_device(ip: str) -> bool:
204
- try:
205
- # Run the ping command and capture the output
206
- response = os.popen(f'ping {ip}').read()
207
-
208
- # Check if "Request timed out" or "Destination host unreachable" appears in the response
209
- if "Request timed out" in response or "Destination host unreachable" in response:
210
- return False
211
- else:
212
- return True
213
- except Exception as e:
214
- print(f"Error while pinging: {e}")
215
- return False
216
-
217
- # Endpoint to check the microcontroller's availability
218
- @app.post("/command/microcontroller")
219
- def check_microcontroller(data: Credentials):
220
- # First, check if the ESP32 is reachable using the ping_device function
221
- if not ping_device(data.esp32_ip):
222
- return {"status": "error", "message": f"Microcontroller at {data.esp32_ip} is not reachable."}
223
-
224
- # If reachable, return a success message without sending any command to the ESP32
225
- return {"status": "success", "message": f"Microcontroller at {data.esp32_ip} is reachable."}
226
-
227
-
228
- @app.post("/command/reset")
229
- def reset_device(data: Credentials):
230
- return send_command(data, "reset")
231
-
232
- @app.post("/command/camera")
233
- def check_camera(data: Credentials):
234
- return send_command(data, "camera")
235
-
236
- @app.post("/command/lcd")
237
- def check_lcd(data: Credentials):
238
- return send_command(data, "lcd")
239
-
240
- @app.post("/command/motor")
241
- def check_motor(data: Credentials):
242
- return send_command(data, "motor")
243
-
244
- @app.post("/command/battery")
245
- def check_battery(data: Credentials):
246
- return send_command(data, "battery")
247
-
248
- @app.post("/update_medicine_status")
249
- def update_medicine_status(req: UpdateStatusRequest):
250
- try:
251
- current_date = datetime.now().strftime('%d-%m-%Y')
252
- print(f"[INFO] Processing for user={req.userId}, medicine={req.medicineName}, time={req.medicineTime}")
253
-
254
- user_ref = db.collection("users").document(req.userId)
255
- user_doc = user_ref.get()
256
-
257
- if not user_doc.exists:
258
- raise HTTPException(status_code=404, detail="User not found")
259
-
260
- user_data = user_doc.to_dict()
261
- medicines = user_data.get("medicines", [])
262
-
263
- print(f"[DEBUG] Total medicine entries: {len(medicines)}")
264
-
265
- # First, find all matches by name
266
- matched_indices = [
267
- idx for idx, med in enumerate(medicines)
268
- if req.medicineName in med.get("medicineNames", [])
269
- ]
270
-
271
- print(f"[DEBUG] Found {len(matched_indices)} medicine(s) with name match")
272
-
273
- target_index = None
274
-
275
- if len(matched_indices) == 1:
276
- target_index = matched_indices[0]
277
- else:
278
- # Multiple matches, match by time
279
- for idx in matched_indices:
280
- for ts in medicines[idx].get("medicineTimes", []):
281
- try:
282
- if hasattr(ts, "strftime"):
283
- time_str = ts.strftime("%H:%M")
284
- print(f"[TRACE] Comparing time: {time_str} == {req.medicineTime}")
285
- if time_str == req.medicineTime:
286
- target_index = idx
287
- break
288
- except Exception as e:
289
- print(f"[ERROR] Timestamp parsing failed: {e}")
290
- if target_index is not None:
291
- break
292
-
293
- if target_index is None:
294
- raise HTTPException(status_code=404, detail="Matching medicine name and time not found.")
295
-
296
- # Update the status
297
- print(f"[INFO] Updating index {target_index}")
298
- if "status" not in medicines[target_index]:
299
- medicines[target_index]["status"] = {}
300
- medicines[target_index]["status"][current_date] = "taken"
301
-
302
- # Push the full list back
303
- user_ref.update({"medicines": medicines})
304
- print(f"[SUCCESS] Updated medicine status at index {target_index}")
305
-
306
- return {"message": f"Medicine status updated at index {target_index}"}
307
-
308
- except Exception as e:
309
- print(f"[ERROR] {e}")
310
- raise HTTPException(status_code=500, detail=str(e))
311
-
312
- class BuzzerRequest(BaseModel):
313
- esp32_ip: str
314
- sound_type: str # Options: relaxing, soft_melody, gentle_chime, calm_tune, beep_beep
315
-
316
- @app.post("/buzzer/play")
317
- def play_buzzer(data: BuzzerRequest):
318
- valid_sounds = ["relaxing", "soft_melody", "gentle_chime", "calm_tune", "beep_beep"]
319
-
320
- if data.sound_type not in valid_sounds:
321
- raise HTTPException(status_code=400, detail="Invalid sound type selected.")
322
-
323
- try:
324
- url = f"http://{data.esp32_ip}/play_buzzer?sound={data.sound_type}"
325
- response = requests.get(url, timeout=5)
326
- response.raise_for_status()
327
- return {
328
- "status": "success",
329
- "message": f"Playing {data.sound_type} on ESP32",
330
- "esp32_response": response.text
331
- }
332
- except requests.RequestException as e:
333
- raise HTTPException(status_code=500, detail=f"Failed to send buzzer command: {e}")
334
-
335
- if __name__ == "__main__":
336
- uvicorn.run(app, host="0.0.0.0", port=5000)
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException
2
+ from pydantic import BaseModel, validator
3
+ import requests
4
+ import uvicorn
5
+ import firebase_admin
6
+ from firebase_admin import credentials, firestore
7
+ import logging
8
+ from datetime import datetime
9
+ import pytz
10
+ import os
11
+ from fastapi import APIRouter, HTTPException
12
+ import requests
13
+
14
+ router = APIRouter()
15
+ # Set up logging
16
+ logging.basicConfig(level=logging.INFO)
17
+ logger = logging.getLogger(__name__)
18
+
19
+ app = FastAPI()
20
+
21
+ @app.get("/", include_in_schema=False)
22
+ async def _health_check():
23
+ return {"status": "healthy"}
24
+
25
+ app.include_router(router)
26
+
27
+ # Initialize Firebase Admin SDK
28
+ try:
29
+ cred = credentials.Certificate("serviceAccountKey.json")
30
+ firebase_admin.initialize_app(cred)
31
+ db = firestore.client()
32
+ except Exception as e:
33
+ logger.error(f"Failed to initialize Firebase Admin SDK: {str(e)}")
34
+ raise
35
+ class UpdateStatusRequest(BaseModel):
36
+ userId: str
37
+ medicineName: str
38
+ medicineTime: str # Expected format: "HH:MM"
39
+
40
+ class Credentials(BaseModel):
41
+ esp32_ip: str
42
+ user: str # Changed from email to user
43
+
44
+ @validator("esp32_ip")
45
+ def validate_esp32_ip(cls, value):
46
+ # Remove http:// or https:// if present
47
+ value = value.replace("http://", "").replace("https://", "")
48
+ # Check for invalid IP like 0.0.0.0
49
+ if "0.0.0.0" in value:
50
+ raise ValueError("Invalid ESP32 IP address: 0.0.0.0 is not a valid destination address")
51
+ return value
52
+
53
+ def convert_to_serializable(obj):
54
+ """Convert Firestore timestamps and other non-serializable objects to JSON-serializable format."""
55
+ if isinstance(obj, firebase_admin.firestore.firestore.DocumentReference):
56
+ return str(obj.path)
57
+ if hasattr(obj, 'isoformat'): # Handles DatetimeWithNanoseconds
58
+ return obj.isoformat()
59
+ if isinstance(obj, dict):
60
+ return {k: convert_to_serializable(v) for k, v in obj.items()}
61
+ if isinstance(obj, list):
62
+ return [convert_to_serializable(item) for item in obj]
63
+ return obj
64
+
65
+ def convert_times_to_ist(medicine_times):
66
+ utc = pytz.utc
67
+ ist = pytz.timezone("Asia/Kolkata")
68
+ converted_times = []
69
+
70
+ for t in medicine_times:
71
+ try:
72
+ if isinstance(t, datetime):
73
+ utc_time = t.astimezone(utc)
74
+ elif isinstance(t, str):
75
+ if "Z" in t:
76
+ utc_time = datetime.fromisoformat(t.replace("Z", "+00:00")).astimezone(utc)
77
+ else:
78
+ utc_time = datetime.fromisoformat(t).astimezone(utc)
79
+ else:
80
+ logger.warning(f"[convert_times_to_ist] Unsupported type for medicineTime: {type(t)}")
81
+ continue
82
+
83
+ ist_time = utc_time.astimezone(ist)
84
+ converted_times.append(ist_time.strftime("%H:%M")) # HH:MM format
85
+ except Exception as e:
86
+ logger.warning(f"[convert_times_to_ist] Error converting time '{t}': {e}")
87
+ return converted_times
88
+
89
+
90
+ @app.post("/send_medicineData")
91
+ async def send_medicineData(data: Credentials):
92
+ try:
93
+ logger.info(f"Received request: {data}")
94
+
95
+ # Step 1: Retrieve the medicines array from Firestore using user ID
96
+ logger.info(f"Fetching medicines data for user: {data.user}")
97
+ doc_ref = db.collection("users").document(data.user)
98
+ doc = doc_ref.get()
99
+
100
+ if not doc.exists:
101
+ logger.error("User data not found")
102
+ raise HTTPException(status_code=404, detail={"error": "User data not found"})
103
+
104
+ user_data = doc.to_dict()
105
+ medicines_data = user_data.get("medicines", []) # Fetch medicines list safely
106
+
107
+ if not medicines_data:
108
+ logger.error("Medicines data not found for this user")
109
+ raise HTTPException(status_code=404, detail={"error": "Medicines data not found for this user"})
110
+
111
+ # Step 2: Extract and filter medicine details based on endDate
112
+ current_date = datetime.now(pytz.UTC) # Current date in UTC
113
+ logger.info(f"Current date (UTC): {current_date}")
114
+
115
+ extracted_medicines = []
116
+ for medicine in medicines_data:
117
+ end_date_raw = medicine.get("endDate")
118
+ if not end_date_raw:
119
+ logger.warning(f"Medicine missing endDate: {medicine}")
120
+ continue
121
+
122
+ try:
123
+ # Handle endDate (already converted to datetime by Firestore Admin SDK)
124
+ if isinstance(end_date_raw, datetime):
125
+ end_date = end_date_raw
126
+ if end_date.tzinfo is None: # If no timezone info, assume UTC
127
+ end_date = end_date.replace(tzinfo=pytz.UTC)
128
+ else:
129
+ end_date = end_date.astimezone(pytz.UTC) # Convert to UTC for comparison
130
+ elif isinstance(end_date_raw, str):
131
+ # Fallback for string format (e.g., "April 17, 2025 at 12:00:00 AM UTC+5:30")
132
+ end_date_str = end_date_raw.replace("at ", "").replace("\u202f", " ")
133
+ end_date = datetime.strptime(end_date_str, "%B %d, %Y %I:%M:%S %p UTC%z")
134
+ end_date = end_date.astimezone(pytz.UTC) # Convert to UTC for comparison
135
+ else:
136
+ logger.warning(f"Unsupported endDate type: {type(end_date_raw)}, value: {end_date_raw}")
137
+ continue
138
+
139
+ # Only include medicines with endDate in the future
140
+ if end_date > current_date:
141
+ extracted_medicines.append({
142
+ "doseFrequency": medicine.get("doseFrequency"),
143
+ "endDate": medicine.get("endDate"),
144
+ "medicineNames": medicine.get("medicineNames", []),
145
+ "medicineTimes": convert_times_to_ist(medicine.get("medicineTimes", {})),
146
+ "selectedDays": medicine.get("selectedDays", [])
147
+ })
148
+
149
+ else:
150
+ logger.info(f"Skipping medicine with expired endDate: {end_date_raw}")
151
+
152
+ except ValueError as e:
153
+ logger.warning(f"Failed to parse endDate: {end_date_raw}, error: {str(e)}")
154
+ continue
155
+
156
+ # Step 3: Check if there are any valid medicines to send
157
+ if not extracted_medicines:
158
+ logger.info("No medicines with endDate in the future found.")
159
+ return {
160
+ "message": "No medicines with endDate in the future found.",
161
+ "medicines": []
162
+ }
163
+
164
+ # Convert any non-serializable objects (e.g., timestamps)
165
+ extracted_medicines = convert_to_serializable(extracted_medicines)
166
+
167
+ # Step 4: Send the medicines data to ESP32
168
+ esp32_ip = data.esp32_ip.replace("http://", "").replace("https://", "")
169
+ esp32_url = f"http://{esp32_ip}/credentials"
170
+ logger.info(f"Sending medicines data to ESP32 at {esp32_url}")
171
+ esp32_response = requests.post(esp32_url, json={"medicines": extracted_medicines}, timeout=20)
172
+ esp32_response.raise_for_status()
173
+
174
+ logger.info("Medicines data sent successfully")
175
+ return {
176
+ "message": "Medicines data sent successfully",
177
+ "medicines": [
178
+ {
179
+ "medicineNames": med.get("medicineNames", []),
180
+ "medicineTimes": med.get("medicineTimes", [])
181
+ }
182
+ for med in extracted_medicines
183
+ ],
184
+ "esp32_response": esp32_response.text
185
+ }
186
+
187
+ except requests.exceptions.RequestException as e:
188
+ logger.error(f"Request error: {str(e)}")
189
+ raise HTTPException(status_code=500, detail={"error": f"Failed to connect to ESP32: {str(e)}"})
190
+ except HTTPException as e:
191
+ raise e
192
+ except Exception as e:
193
+ logger.error(f"Unexpected error: {str(e)}", exc_info=True)
194
+ raise HTTPException(status_code=500, detail={"error": f"Unexpected error: {str(e)}"})
195
+
196
+ def send_command(data: Credentials, command: str):
197
+ try:
198
+ esp32_ip = data.esp32_ip.replace("http://", "").replace("https://", "")
199
+ url = f"http://{esp32_ip}/command/{command}"
200
+ logger.info(f"Sending command: {command} to {url}")
201
+ response = requests.get(url, timeout=10)
202
+ response.raise_for_status()
203
+ return {"message": f"{command.capitalize()} command sent successfully", "esp32_response": response.text}
204
+ except requests.exceptions.RequestException as e:
205
+ raise HTTPException(status_code=500, detail={"error": f"Failed to send {command} command: {str(e)}"})
206
+
207
+
208
+
209
+ def ping_device(ip: str) -> bool:
210
+ try:
211
+ # Run the ping command and capture the output
212
+ response = os.popen(f'ping {ip}').read()
213
+
214
+ # Check if "Request timed out" or "Destination host unreachable" appears in the response
215
+ if "Request timed out" in response or "Destination host unreachable" in response:
216
+ return False
217
+ else:
218
+ return True
219
+ except Exception as e:
220
+ print(f"Error while pinging: {e}")
221
+ return False
222
+
223
+ # Endpoint to check the microcontroller's availability
224
+ @app.post("/command/microcontroller")
225
+ def check_microcontroller(data: Credentials):
226
+ # First, check if the ESP32 is reachable using the ping_device function
227
+ if not ping_device(data.esp32_ip):
228
+ return {"status": "error", "message": f"Microcontroller at {data.esp32_ip} is not reachable."}
229
+
230
+ # If reachable, return a success message without sending any command to the ESP32
231
+ return {"status": "success", "message": f"Microcontroller at {data.esp32_ip} is reachable."}
232
+
233
+
234
+ @app.post("/command/reset")
235
+ def reset_device(data: Credentials):
236
+ return send_command(data, "reset")
237
+
238
+ @app.post("/command/camera")
239
+ def check_camera(data: Credentials):
240
+ return send_command(data, "camera")
241
+
242
+ @app.post("/command/lcd")
243
+ def check_lcd(data: Credentials):
244
+ return send_command(data, "lcd")
245
+
246
+ @app.post("/command/motor")
247
+ def check_motor(data: Credentials):
248
+ return send_command(data, "motor")
249
+
250
+ @app.post("/command/battery")
251
+ def check_battery(data: Credentials):
252
+ return send_command(data, "battery")
253
+
254
+ @app.post("/update_medicine_status")
255
+ def update_medicine_status(req: UpdateStatusRequest):
256
+ try:
257
+ current_date = datetime.now().strftime('%d-%m-%Y')
258
+ print(f"[INFO] Processing for user={req.userId}, medicine={req.medicineName}, time={req.medicineTime}")
259
+
260
+ user_ref = db.collection("users").document(req.userId)
261
+ user_doc = user_ref.get()
262
+
263
+ if not user_doc.exists:
264
+ raise HTTPException(status_code=404, detail="User not found")
265
+
266
+ user_data = user_doc.to_dict()
267
+ medicines = user_data.get("medicines", [])
268
+
269
+ print(f"[DEBUG] Total medicine entries: {len(medicines)}")
270
+
271
+ # First, find all matches by name
272
+ matched_indices = [
273
+ idx for idx, med in enumerate(medicines)
274
+ if req.medicineName in med.get("medicineNames", [])
275
+ ]
276
+
277
+ print(f"[DEBUG] Found {len(matched_indices)} medicine(s) with name match")
278
+
279
+ target_index = None
280
+
281
+ if len(matched_indices) == 1:
282
+ target_index = matched_indices[0]
283
+ else:
284
+ # Multiple matches, match by time
285
+ for idx in matched_indices:
286
+ for ts in medicines[idx].get("medicineTimes", []):
287
+ try:
288
+ if hasattr(ts, "strftime"):
289
+ time_str = ts.strftime("%H:%M")
290
+ print(f"[TRACE] Comparing time: {time_str} == {req.medicineTime}")
291
+ if time_str == req.medicineTime:
292
+ target_index = idx
293
+ break
294
+ except Exception as e:
295
+ print(f"[ERROR] Timestamp parsing failed: {e}")
296
+ if target_index is not None:
297
+ break
298
+
299
+ if target_index is None:
300
+ raise HTTPException(status_code=404, detail="Matching medicine name and time not found.")
301
+
302
+ # Update the status
303
+ print(f"[INFO] Updating index {target_index}")
304
+ if "status" not in medicines[target_index]:
305
+ medicines[target_index]["status"] = {}
306
+ medicines[target_index]["status"][current_date] = "taken"
307
+
308
+ # Push the full list back
309
+ user_ref.update({"medicines": medicines})
310
+ print(f"[SUCCESS] Updated medicine status at index {target_index}")
311
+
312
+ return {"message": f"Medicine status updated at index {target_index}"}
313
+
314
+ except Exception as e:
315
+ print(f"[ERROR] {e}")
316
+ raise HTTPException(status_code=500, detail=str(e))
317
+
318
+ class BuzzerRequest(BaseModel):
319
+ esp32_ip: str
320
+ sound_type: str # Options: relaxing, soft_melody, gentle_chime, calm_tune, beep_beep
321
+
322
+ @app.post("/buzzer/play")
323
+ def play_buzzer(data: BuzzerRequest):
324
+ valid_sounds = ["relaxing", "soft_melody", "gentle_chime", "calm_tune", "beep_beep"]
325
+
326
+ if data.sound_type not in valid_sounds:
327
+ raise HTTPException(status_code=400, detail="Invalid sound type selected.")
328
+
329
+ try:
330
+ url = f"http://{data.esp32_ip}/play_buzzer?sound={data.sound_type}"
331
+ response = requests.get(url, timeout=5)
332
+ response.raise_for_status()
333
+ return {
334
+ "status": "success",
335
+ "message": f"Playing {data.sound_type} on ESP32",
336
+ "esp32_response": response.text
337
+ }
338
+ except requests.RequestException as e:
339
+ raise HTTPException(status_code=500, detail=f"Failed to send buzzer command: {e}")
340
+
341
+ if __name__ == "__main__":
342
+ import os
343
+ port = int(os.environ.get("PORT", 5000))
344
+ uvicorn.run("app:app", host="0.0.0.0", port=port, reload=True)