from flask import Blueprint, jsonify, request import pandas as pd import os from datetime import timedelta from statsmodels.tsa.statespace.sarimax import SARIMAX from models import SensorData, User, session from werkzeug.security import generate_password_hash, check_password_hash from flask_login import login_user, logout_user, login_required, current_user bp = Blueprint('api', __name__) # --- AUTHENTICATION & PASSWORD RECOVERY ROUTES (Unchanged) --- @bp.route('/api/signup', methods=['POST']) def signup(): data = request.get_json() if session.query(User).filter_by(username=data.get('username')).first(): return jsonify({"message": "Username already exists."}), 409 if session.query(User).filter_by(email=data.get('email')).first(): return jsonify({"message": "Email already registered."}), 409 new_user = User( username=data.get('username'), email=data.get('email'), security_question_1=data.get('security_question_1'), security_answer_1_hash=generate_password_hash(data.get('security_answer_1')), security_question_2=data.get('security_question_2'), security_answer_2_hash=generate_password_hash(data.get('security_answer_2')) ) new_user.set_password(data.get('password')) session.add(new_user) session.commit() return jsonify({"message": "Account created successfully."}), 201 @bp.route('/api/login', methods=['POST']) def login(): data = request.get_json() user = session.query(User).filter_by(username=data.get('username')).first() if not user or not user.check_password(data.get('password')): return jsonify({"message": "Invalid username or password."}), 401 login_user(user) return jsonify({"message": "Logged in successfully."}), 200 @bp.route('/api/logout', methods=['POST']) @login_required def logout(): logout_user() return jsonify({"message": "You have been logged out."}), 200 @bp.route('/api/check_session', methods=['GET']) def check_session(): if current_user.is_authenticated: return jsonify({"loggedIn": True, "username": current_user.username}), 200 return jsonify({"loggedIn": False}), 401 @bp.route('/api/get-security-questions', methods=['POST']) def get_security_questions(): data = request.get_json() user = session.query(User).filter_by(username=data.get('username')).first() if not user or not user.security_question_1: return jsonify({"message": "Unable to retrieve security questions for this user."}), 404 return jsonify({"question1": user.security_question_1, "question2": user.security_question_2}), 200 @bp.route('/api/reset-password', methods=['POST']) def reset_password(): data = request.get_json() user = session.query(User).filter_by(username=data.get('username')).first() if not user: return jsonify({"message": "Invalid credentials."}), 401 answer1_correct = check_password_hash(user.security_answer_1_hash, data.get('answer1')) answer2_correct = check_password_hash(user.security_answer_2_hash, data.get('answer2')) if not (answer1_correct and answer2_correct): return jsonify({"message": "One or more security answers are incorrect."}), 401 user.set_password(data.get('newPassword')) session.commit() return jsonify({"message": "Password has been reset successfully. Please log in."}), 200 # --- DATA ROUTES (Unchanged) --- @bp.route('/status', methods=['GET']) def status(): return jsonify({'status': 'API is running'}) @bp.route('/api/upload-mock-data', methods=['POST']) def upload_mock_data(): file_path = "mock_sensor_data.csv" if not os.path.exists(file_path): return jsonify({"error": "Mock file not found"}), 404 df = pd.read_csv(file_path) df["timestamp"] = pd.to_datetime(df["timestamp"]) for _, row in df.iterrows(): entry = SensorData( timestamp=row["timestamp"], sensor_id=row["sensor_id"], rainfall_mm=row["rainfall_mm"], water_level_cm=row["water_level_cm"], flow_rate_lps=row["flow_rate_lps"] ) session.add(entry) session.commit() return jsonify({"message": "Mock data uploaded successfully!"}) @bp.route('/api/get-sensor-data', methods=['GET']) @login_required def get_sensor_data(): try: data = session.query(SensorData).all() result = [ { "timestamp": entry.timestamp.strftime("%Y-%m-%d %H:%M:%S"), "sensor_id": entry.sensor_id, "rainfall_mm": entry.rainfall_mm, "water_level_cm": entry.water_level_cm, "flow_rate_lps": entry.flow_rate_lps } for entry in data ] return jsonify(result) except Exception as e: return jsonify({"error": f"An internal error occurred: {str(e)}"}), 500 # --- UPDATED FORECASTING ROUTE --- @bp.route('/api/forecast', methods=['GET']) @login_required def forecast(): try: query = session.query(SensorData).statement df = pd.read_sql(query, session.bind) if df.empty or len(df) < 24: # Need enough data to forecast return jsonify({"error": "Not enough data to create a forecast."}), 400 df['timestamp'] = pd.to_datetime(df['timestamp']) df = df.set_index('timestamp').sort_index() forecast_results = {} metrics_to_forecast = { "waterLevel": "water_level_cm", "flowRate": "flow_rate_lps", "rainfall": "rainfall_mm" } last_timestamp = df.index[-1] forecast_steps = 24 future_dates = pd.date_range(start=last_timestamp + timedelta(hours=1), periods=forecast_steps, freq='H') for key, column_name in metrics_to_forecast.items(): # Resample series for stability series = df[column_name].resample('H').mean().fillna(method='ffill') # Simple check to avoid trying to forecast flat-line data (common with rainfall) if series.nunique() < 2: predicted_mean = [series.iloc[-1]] * forecast_steps else: # Define and train the SARIMAX model model = SARIMAX(series, order=(1, 1, 1), seasonal_order=(1, 1, 0, 12), enforce_stationarity=False, enforce_invertibility=False) results = model.fit(disp=False) prediction = results.get_forecast(steps=forecast_steps) predicted_mean = prediction.predicted_mean.tolist() # Format the prediction for this metric forecast_results[key] = { "timestamps": [d.strftime("%Y-%m-%d %H:%M:%S") for d in future_dates], "predicted_values": predicted_mean } # Ensure rainfall forecast does not predict negative values if key == 'rainfall': forecast_results[key]['predicted_values'] = [max(0, val) for val in forecast_results[key]['predicted_values']] return jsonify(forecast_results) except Exception as e: return jsonify({"error": f"Failed to generate forecast: {str(e)}"}), 500