import os import asyncio import base64 import uuid from datetime import datetime, timedelta, timezone import sys import json from flask import Flask, request, jsonify, current_app from pymongo import MongoClient from APIs import generate, EdamamAPI from APIs.mqtt import send_command from APIs.webex import WebexManager from microwaveCookPlanner import MicrowaveCookPlanner import safety_checker import pymongo from APIs.twilio import send_alert_sms sys.path.insert(0, '..') try: from shared import config except ImportError: from ..shared import config app = Flask(__name__) # --------------------------------------------------------- # Configuration & Setup # --------------------------------------------------------- # Configure MongoDB connection MONGO_URI = os.getenv("MONGO_URI", "mongodb://localhost:27017/") client = MongoClient(MONGO_URI) db = client["microwave_network_db"] client_collection = db["client_data"] cooking_collection = db["cooking_parameters"] telemetry_collection = db["telemetry_data"] alert_collection = db["alert_data"] webex_tokens_collection = db["webex_tokens"] # Collection for Webex OAuth tokens # Webex Credentials & Configuration from Environment Variables WEBEX_CLIENT_ID = os.getenv("WEBEX_CLIENT_ID", "YOUR_WEBEX_CLIENT_ID") WEBEX_CLIENT_SECRET = os.getenv("WEBEX_CLIENT_SECRET", "YOUR_WEBEX_CLIENT_SECRET") WEBEX_REDIRECT_URI = os.getenv("WEBEX_REDIRECT_URI", "https://smartwave.matthiasg.dev/oauth/callback") WEBEX_TEAM_ID = os.getenv("WEBEX_TEAM_ID", "YOUR_WEBEX_TEAM_ID") WEBEX_NINLUC_ID = os.getenv("WEBEX_NINLUC_ID", "YOUR_WEBEX_NINLUC_ID") SAVEUP_TWILIO_API_TOKEN = os.getenv("SAVEUP_TWILIO_API_TOKEN", "false").lower() == "true" # Ensure the camera image storage directory exists when the app starts CAMERA_IMAGE_DIR = "storage/dishPhotos" os.makedirs(CAMERA_IMAGE_DIR, exist_ok=True) # --------------------------------------------------------- # Classes # --------------------------------------------------------- microwave_cook_planner = MicrowaveCookPlanner() # Instantiate Webex Manager webex_manager = WebexManager( db_collection=webex_tokens_collection, client_id=WEBEX_CLIENT_ID, client_secret=WEBEX_CLIENT_SECRET, redirect_uri=WEBEX_REDIRECT_URI, team_id=WEBEX_TEAM_ID, user_id=WEBEX_NINLUC_ID ) # --------------------------------------------------------- # OAuth Routes # --------------------------------------------------------- @app.route("/oauth/callback") def oauth_callback(): """OAuth redirect endpoint that receives the authorization code.""" code = request.args.get("code") if not code: return jsonify({"error": "Missing code parameter"}), 400 try: webex_manager.exchange_code(code) return jsonify({"status": "success", "message": "Webex tokens stored successfully in MongoDB!"}), 200 except Exception as e: current_app.logger.exception("Failed to exchange OAuth code") return jsonify({"error": f"OAuth exchange failed: {str(e)}"}), 500 # --------------------------------------------------------- # Application Routes # --------------------------------------------------------- @app.route("/") def hello_world(): gen = generate(prompt="Say Hello, to the user !") print(gen) return f"
{gen}
" @app.route("/cooking-params", methods=["POST"]) async def cooking_params(): try: data = request.get_json() if not data: return jsonify({"error": "Invalid or missing JSON payload"}), 400 # Extract user or device parameters height_cm = float(data.get("dish_height", 4.0)) initial_temp_c = float(data.get("ir_initial_temp", 20.0)) microwave_wattage = int(data.get("microwave_wattage", 900)) defrost_mode = bool(data.get("defrost_mode", False)) # 1. Save Camera Image camera_image_b64 = data.get("camera_image") if not camera_image_b64: return jsonify({"error": "Missing required field 'camera_image'"}), 400 filename = f"dish_{uuid.uuid4().hex}.jpg" filepath = os.path.join(CAMERA_IMAGE_DIR, filename) try: with open(filepath, "wb") as f: f.write(base64.b64decode(camera_image_b64)) data["camera_image"] = filepath except Exception as e: current_app.logger.exception("Failed to save camera image") return jsonify({"error": f"Failed to save camera image: {str(e)}"}), 500 # 2. Run Vision Safety Check & Cook Planner Concurrently try: if getattr(config, "DEBUG_DANGEROUS_AREA", False): print(f"[Debug] Running safety check on predefined image") safety_task = asyncio.to_thread(safety_checker.check_dish_safety, os.path.join(CAMERA_IMAGE_DIR, "dish_0de0ee1dab8949fc8e78796947e24ed3.jpg")) elif getattr(config, "DEBUG", False): safety_task = asyncio.to_thread(safety_checker.check_dish_safety, os.path.join("microwaveDish.jpg")) else: safety_task = asyncio.to_thread(safety_checker.check_dish_safety, filepath) planner_task = asyncio.to_thread( microwave_cook_planner.generate_plan, image_path=filepath, height_cm=height_cm, initial_temp_c=initial_temp_c, microwave_wattage=microwave_wattage, defrost_mode=defrost_mode ) # Execute both concurrently and await results safety_result, cook_plan = await asyncio.gather(safety_task, planner_task) except Exception as e: current_app.logger.exception("Exception during safety check or cook planning") return jsonify({"error": f"Task execution failed: {str(e)}"}), 500 # 4. Attach Cooking Plan & safety check and Save to MongoDB data["safety_check"] = safety_result data["analysis_results"] = cook_plan try: cooking_collection.insert_one(data) data.pop("_id", None) except Exception as e: return jsonify({"error": f"Database error: {str(e)}"}), 500 # 3. Evaluate Safety Result if not safety_result.get("is_safe", True): current_app.logger.warning(f"Unsafe dish detected: {safety_result}") return jsonify({ "error": "Safety hazard detected in microwave area", "is_safe": False, "warning_message": safety_result.get("warning_message", "Unsafe materials detected."), "detected_hazards": safety_result.get("detected_hazards", []) }), 200 return jsonify(cook_plan), 201 except Exception as e: current_app.logger.exception("Exception in /cooking-params") @app.route("/alert", methods=["POST"]) def alert(): data = request.get_json() or {} # 1. Inject UTC timestamp data["created_at"] = datetime.now(timezone.utc) saved_alert = alert_collection.insert_one(data) orchestrator_id = data.get("orchestrator_id") alert_info = data.get("alert", {}) alert_type = alert_info.get("type", "Unknown") alert_message = alert_info.get("message", "No message provided") # --- GET CLIENT METADATA --- client_name = f"Unknown Client (Orchestrator {orchestrator_id})" if orchestrator_id else "Unknown Client" client_email = None client_phone = None if orchestrator_id: client_doc = client_collection.find_one({ "devices": { "$elemMatch": { "device_type": "orchestrator", "device_id": str(orchestrator_id) } } }) if client_doc: client_name = client_doc.get("client_name", client_name) contact_info = client_doc.get("contact_info", {}) client_email = contact_info.get("email") client_phone = contact_info.get("phone") # Extract phone number for Twilio # --- WEBEX ROOM LOGIC --- webex_room_id = None room_title = f"Support - {client_name}" room_link = None webex_status = "skipped" # --- TWILIO SMS LOGIC --- sms_sent = False three_minutes_ago = datetime.now(timezone.utc) - timedelta(minutes=3) recent_alert = alert_collection.find_one({ "orchestrator_id": orchestrator_id, "webex_room_id": {"$exists": True, "$ne": None}, "created_at": {"$gte": three_minutes_ago}, "_id": {"$ne": saved_alert.inserted_id} }, sort=[("created_at", pymongo.DESCENDING)]) if recent_alert: webex_room_id = recent_alert["webex_room_id"] room_title = recent_alert.get("webex_room_title", room_title) try: webex_manager.send_alert_info_message( room_id=webex_room_id, alert_type=alert_type, alert_message=alert_message, alert_id=str(saved_alert.inserted_id) ) webex_status = "reused_room" room_link = f"https://web.webex.com/spaces/{webex_room_id}" except Exception as e: current_app.logger.exception("Failed to send follow-up message to existing Webex room") webex_status = f"message_failed: {str(e)}" else: try: room_details = webex_manager.create_support_room( client_name, client_email, alert_type, alert_message, str(saved_alert.inserted_id) ) if isinstance(room_details, dict): webex_room_id = room_details.get("id") room_title = room_details.get("title", room_title) # Set room_link if your webex_manager returns meetingLink or custom URL room_link = room_details.get("meetingLink") or f"https://web.webex.com/spaces/{webex_room_id}" else: webex_room_id = room_details room_link = f"https://web.webex.com/spaces/{webex_room_id}" webex_status = "created_new_room" except Exception as e: current_app.logger.exception("Failed to create Webex support room") webex_status = f"creation_failed: {str(e)}" # --- SEND SMS --- if webex_room_id and client_phone and not SAVEUP_TWILIO_API_TOKEN: sms_sent = send_alert_sms( to_phone=client_phone, room_title=room_title, room_link=room_link, client_email=client_email ) # Update document with room and sms details if webex_room_id: alert_collection.update_one( {"_id": saved_alert.inserted_id}, {"$set": {"webex_room_id": webex_room_id, "webex_room_title": room_title, "webex_room_link": room_link, "sms_sent": sms_sent}} ) else: # Just save the sms alert_collection.update_one( {"_id": saved_alert.inserted_id}, {"$set": {"sms_sent": sms_sent}} ) return jsonify({ "status": "success", "message": "Alert saved", "webex_room_id": webex_room_id, "webex_room_url": room_link, "webex_status": webex_status, "sms_sent": sms_sent }), 200 @app.route("/telemetry", methods=["POST"]) def telemetry(): data = request.get_json() if data is None: return jsonify({"error": "Invalid or missing JSON payload"}), 400 # If the payload was double-encoded as a string, deserialize it if isinstance(data, str): try: data = json.loads(data) except (json.JSONDecodeError, TypeError): return jsonify({"error": "String payload could not be parsed as JSON"}), 400 if not isinstance(data, dict): return jsonify({"error": "Expected a JSON object/dictionary"}), 400 # Stamp UTC timestamp for Node-RED queries data["received_at"] = datetime.now(datetime.timezone.utc).isoformat() try: telemetry_collection.insert_one(data) return jsonify({"status": "success", "message": "Telemetry saved"}), 200 except Exception as e: return jsonify({"error": f"Database error: {str(e)}"}), 500 @app.route("/debug/telemetryrequest", methods=["GET"]) def debug_telemetryrequest(): # Construct external HTTP endpoint dynamically based on incoming request host telemetry_url = f"{request.host_url.rstrip('/').replace('http://', 'https://')}/telemetry" cmd_payload = { "action": "request_telemetry", "endpoint": telemetry_url } try: send_command(topic="cmd/all", payload=cmd_payload) return jsonify({ "status": "Telemetry command sent to cmd/all", "published_payload": cmd_payload }), 200 except Exception as e: return jsonify({"error": f"Failed to publish MQTT command: {str(e)}"}), 500 @app.route("/debug", methods=["GET"]) def debug(): return safety_checker.check_dish_safety(os.path.join(CAMERA_IMAGE_DIR, "dish_0de0ee1dab8949fc8e78796947e24ed3.jpg")) if __name__ == "__main__": app.run(debug=getattr(config, "DEBUG", True))