Files
Smartwave/cloud/app.py
T
Ninluc 44b4c8034f
Build, push image, and notify Watchtower / build-image (push) Successful in 42s
Build, push image, and notify Watchtower / notify (push) Successful in 12s
better Webex
2026-08-17 14:46:35 +02:00

329 lines
12 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
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")
# 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 a UTC timestamp so we can easily query the 3-minute window
data["created_at"] = datetime.now(timezone.utc)
saved_alert = alert_collection.insert_one(data)
# Extract orchestrator_id from the incoming alert
orchestrator_id = data.get("orchestrator_id")
# Safely extract alert info (prevents errors if "alert" is missing)
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
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)
client_email = client_doc.get("contact_info", {}).get("email")
# --- WEBEX ROOM LOGIC ---
webex_room_id = None
webex_status = "skipped"
# 2. Check if a room was created for this orchestrator in the last 3 minutes
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} # Exclude the alert we just inserted
}, sort=[("created_at", pymongo.DESCENDING)])
if recent_alert:
# A room was created recently -> Reuse it and just send a message
webex_room_id = recent_alert["webex_room_id"]
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"
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:
# No recent room -> Create a new one
try:
# Assuming your modified create_support_room returns a dict with {"id": ...}
room_details = webex_manager.create_support_room(
client_name,
client_email,
alert_type,
alert_message,
str(saved_alert.inserted_id) # Cast ObjectId to string for JSON serialization
)
# Extract ID based on whether your function returns a dict or just the ID string
webex_room_id = room_details["id"] if isinstance(room_details, dict) else room_details
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)}"
# 3. Update the alert document with the Webex Room ID (whether new or reused)
if webex_room_id:
alert_collection.update_one(
{"_id": saved_alert.inserted_id},
{"$set": {"webex_room_id": webex_room_id}}
)
# --- SEND SMS WITH DETAILS ---
# TODO: Add SMS logic here
return jsonify({
"status": "success",
"message": "Alert saved",
"webex_room_id": webex_room_id,
"webex_status": webex_status
}), 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))