from fastapi import FastAPI, Request, HTTPException import requests import json import uuid from datetime import datetime, timezone import os # Import the 'os' module app = FastAPI() # --- CONFIGURATION --- # Use Hugging Face Secrets for sensitive data CLIENT_ID = "amzn1.application-oa2-client.bc4475f5b85148fa8c1329e4bb6da5e9" # Your *NEW* ALEXA Client ID (from Custom Skill) CLIENT_SECRET = os.environ.get("CLIENT_SECRET") # Get *ALEXA* Client Secret from environment variable ALEXA_EVENT_GATEWAY = "https://api.amazonalexa.com/v3/events" # --- DEVICE MAPPING --- TRIGGERS = { "Trigger1": {"endpointId": "device-001", "description": "Webhook Trigger 1", "state": "NOT_DETECTED"}, "Trigger2": {"endpointId": "device-002", "description": "Webhook Trigger 2", "state": "NOT_DETECTED"}, "Trigger3": {"endpointId": "device-003", "description": "Webhook Trigger 3", "state": "NOT_DETECTED"}, "Trigger4": {"endpointId": "device-004", "description": "Webhook Trigger 4", "state": "NOT_DETECTED"}, "Trigger5": {"endpointId": "device-005", "description": "Webhook Trigger 5", "state": "NOT_DETECTED"}, "Trigger6": {"endpointId": "device-006", "description": "Webhook Trigger 6", "state": "NOT_DETECTED"}, "Trigger7": {"endpointId": "device-007", "description": "Webhook Trigger 7", "state": "NOT_DETECTED"}, "Trigger8": {"endpointId": "device-008", "description": "Webhook Trigger 8", "state": "NOT_DETECTED"}, "Trigger9": {"endpointId": "device-009", "description": "Webhook Trigger 9", "state": "NOT_DETECTED"}, "Trigger10": {"endpointId": "device-010", "description": "Webhook Trigger 10", "state": "NOT_DETECTED"}, } # --- TOKEN MANAGEMENT (Simple - In-Memory for Now) --- TOKEN_DATA = { "access_token": None, "expires_at": None, # Timestamp when the token expires } # --- HELPER FUNCTION: Get LWA Access Token --- async def get_lwa_access_token(): """Fetches or refreshes the LWA access token.""" global TOKEN_DATA # Check if we have a token and if it's still valid if TOKEN_DATA["access_token"] and TOKEN_DATA["expires_at"] and datetime.now(timezone.utc).timestamp() < TOKEN_DATA["expires_at"]: return TOKEN_DATA["access_token"] # Get a new token token_url = "https://api.amazon.com/auth/o2/token" data = { "grant_type": "client_credentials", "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET, "scope": "alexa::proactive_events", } # --- DEBUGGING LOGS --- print("----- LWA Token Request Data -----") print(data) print("---------------------------------") print(f"----- CLIENT_SECRET Value: {CLIENT_SECRET} -----") # --- END OF DEBUGGING LOGS --- try: response = requests.post(token_url, data=data) response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx) token_response = response.json() TOKEN_DATA["access_token"] = token_response["access_token"] # Calculate expiry time (subtract a small buffer to be safe) TOKEN_DATA["expires_at"] = datetime.now(timezone.utc).timestamp() + token_response["expires_in"] - 60 return TOKEN_DATA["access_token"] except requests.exceptions.RequestException as e: print(f"Error getting LWA token: {e}") raise HTTPException(status_code=500, detail="Failed to get LWA access token") # --- HELPER FUNCTION: Send ChangeReport to Alexa --- async def send_change_report(endpoint_id, property_name, property_value): """Sends a ChangeReport event to Alexa.""" try: access_token = await get_lwa_access_token() except HTTPException: # Re-raise exception from get_lwa_access_token raise headers = { "Authorization": f"Bearer {access_token}", "Content-Type": "application/json", } change_report = { "event": { "header": { "namespace": "Alexa", "name": "ChangeReport", "messageId": str(uuid.uuid4()), "payloadVersion": "3", }, "endpoint": { "endpointId": endpoint_id, }, "payload": { "change": { "cause": {"type": "PHYSICAL_INTERACTION"}, "properties": [ { "namespace": "Alexa.ContactSensor", "name": property_name, "value": property_value, "timeOfSample": datetime.now(timezone.utc).isoformat(), "uncertaintyInMilliseconds": 0, } ], } }, } } try: response = requests.post( ALEXA_EVENT_GATEWAY, headers=headers, data=json.dumps(change_report) ) response.raise_for_status() print(f"ChangeReport sent for {endpoint_id}. Response: {response.status_code} - {response.text}") # Log full response except requests.exceptions.RequestException as e: print(f"Error sending ChangeReport: {e}") raise HTTPException(status_code=500, detail=f"Failed to send ChangeReport: {e}") # --- WEBHOOK ENDPOINT --- @app.post("/{trigger_name}") async def alexa_webhook(trigger_name: str, request: Request): data = await request.json() print(f"Received request for {trigger_name}: {data}") if trigger_name in TRIGGERS: endpoint_id = TRIGGERS[trigger_name]["endpointId"] # Toggle the state current_state = TRIGGERS[trigger_name]["state"] new_state = "NOT_DETECTED" if current_state == "DETECTED" else "DETECTED" TRIGGERS[trigger_name]["state"] = new_state try: await send_change_report(endpoint_id, "detectionState", new_state) except HTTPException as e: return e # Return the HTTPException directly return { "event": { "header": { "namespace": "Alexa", "name": "Response", "payloadVersion": "3", "messageId": str(uuid.uuid4()) }, "payload": {"message": f"{trigger_name} activated! New state: {new_state}"} } } else: raise HTTPException(status_code=404, detail=f"Unknown trigger: {trigger_name}")