Files
Smartwave/cloud/app.py
T
2026-08-17 17:28:41 +02:00

355 lines
13 KiB
Python

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"<p>{gen}</p>"
@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),
added_alert_info=True
)
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))