Spaces:
Sleeping
Sleeping
| from flask import Flask, render_template, request, jsonify | |
| import joblib | |
| import pandas as pd | |
| import requests | |
| import json | |
| import matplotlib | |
| matplotlib.use('Agg') | |
| import matplotlib.pyplot as plt | |
| import io | |
| import base64 | |
| from twilio.rest import Client | |
| from twilio.twiml.messaging_response import MessagingResponse | |
| import threading | |
| import os | |
| import time | |
| import secrets | |
| import math | |
| from matplotlib.patches import Wedge, Circle | |
| app = Flask(__name__) | |
| app.secret_key = os.getenv('FLASK_SECRET_KEY', secrets.token_hex(24)) | |
| # ------------------------- | |
| # 🔑 Persistent irrigation state (file-based) | |
| # ------------------------- | |
| STATE_FILE = 'irrigation_state.json' | |
| def load_irrigation_state(): | |
| """Load irrigation state from JSON file""" | |
| if os.path.exists(STATE_FILE): | |
| try: | |
| with open(STATE_FILE, 'r') as f: | |
| return json.load(f) | |
| except Exception as e: | |
| print(f"Error loading state: {e}") | |
| return {} | |
| return {} | |
| def save_irrigation_state(state): | |
| """Save irrigation state to JSON file""" | |
| try: | |
| with open(STATE_FILE, 'w') as f: | |
| json.dump(state, f, indent=2) | |
| except Exception as e: | |
| print(f"Error saving state: {e}") | |
| irrigation_state = load_irrigation_state() | |
| # ------------------------- | |
| # Load ML Model | |
| # ------------------------- | |
| svm_poly_model = None | |
| try: | |
| svm_poly_model = joblib.load('svm_poly_model.pkl') | |
| print("SVM model loaded successfully.") | |
| except FileNotFoundError: | |
| print("Error: svm_poly_model.pkl not found. Make sure the model file is in the correct directory.") | |
| except Exception as e: | |
| print(f"Error loading SVM model: {e}") | |
| # ------------------------- | |
| # Mappings | |
| # ------------------------- | |
| crop_type_mapping = { | |
| 'BANANA': 0, 'BEAN': 1, 'CABBAGE': 2, 'CITRUS': 3, 'COTTON': 4, 'MAIZE': 5, | |
| 'MELON': 6, 'MUSTARD': 7, 'ONION': 8, 'OTHER': 9, 'POTATO': 10, 'RICE': 11, | |
| 'SOYABEAN': 12, 'SUGARCANE': 13, 'TOMATO': 14, 'WHEAT': 15 | |
| } | |
| soil_type_mapping = {'DRY': 0, 'HUMID': 1, 'WET': 2} | |
| weather_condition_mapping = {'NORMAL': 0, 'RAINY': 1, 'SUNNY': 2, 'WINDY': 3} | |
| # ------------------------- | |
| # API & Twilio Config | |
| # ------------------------- | |
| WEATHER_API_KEY = os.getenv('WEATHER_API', 'ee75ffd59875aa5ca6c207e594336b30') | |
| TWILIO_ACCOUNT_SID = os.getenv('TWILIO_ACCOUNT_SID', 'ACe45f7038c5338a153d1126ca6d547c84') | |
| TWILIO_AUTH_TOKEN = os.getenv('TWILIO_AUTH_TOKEN', '48b9eea898885ef395d48edc74924340') | |
| TWILIO_PHONE_NUMBER = 'whatsapp:+14155238886' | |
| USER_PHONE_NUMBER = os.getenv('FARMER_PHONE_NUMBER', 'whatsapp:+919763059811') | |
| twilio_client = Client(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN) | |
| # ------------------------- | |
| # Helper Functions | |
| # ------------------------- | |
| def get_weather(city: str): | |
| url = f"https://api.openweathermap.org/data/2.5/weather?q={city}&appid={WEATHER_API_KEY}&units=metric" | |
| try: | |
| response = requests.get(url) | |
| response.raise_for_status() | |
| data = response.json() | |
| if data.get('cod') == 200: | |
| return (data['main']['temp'], data['main']['humidity'], | |
| data['weather'][0]['description'], data['main']['pressure']) | |
| else: | |
| print(f"Weather API error: {data.get('message', 'Unknown error')}") | |
| except Exception as e: | |
| print(f"Error fetching weather data for {city}: {e}") | |
| return None, None, None, None | |
| def send_whatsapp_message(to_number, body_text): | |
| try: | |
| message = twilio_client.messages.create( | |
| from_=TWILIO_PHONE_NUMBER, | |
| body=body_text, | |
| to=to_number | |
| ) | |
| print(f"Twilio Message SID: {message.sid}") | |
| return message.sid | |
| except Exception as e: | |
| print(f"Error sending WhatsApp message to {to_number}: {e}") | |
| return None | |
| def trigger_irrigation_complete(crop_type, city, estimated_time_duration, time_unit, user_phone): | |
| message_text = ( | |
| f"✅ Irrigation Complete!\n\n" | |
| f"Crop: {crop_type}\n" | |
| f"Location: {city}\n" | |
| f"Duration: {estimated_time_duration:.2f} {time_unit}\n\n" | |
| "The motor has been turned off automatically." | |
| ) | |
| send_whatsapp_message(user_phone, message_text) | |
| print(f"Irrigation complete message sent to {user_phone} after {estimated_time_duration:.2f} {time_unit}.") | |
| irrigation_state.pop('current', None) # clear state | |
| save_irrigation_state(irrigation_state) | |
| def fig_to_datauri(fig, fmt='png', dpi=150): | |
| buf = io.BytesIO() | |
| fig.savefig(buf, format=fmt, bbox_inches='tight', dpi=dpi) | |
| buf.seek(0) | |
| data = base64.b64encode(buf.read()).decode('ascii') | |
| plt.close(fig) | |
| return f"data:image/{fmt};base64,{data}" | |
| def draw_gauge(value, max_value, title="", unit=""): | |
| val = max(0.0, min(value, max_value)) | |
| ratio = val / float(max_value) if max_value > 0 else 0.0 | |
| start_angle, end_angle = 180, 0 | |
| angle = start_angle + (end_angle - start_angle) * ratio | |
| fig, ax = plt.subplots(figsize=(5, 3)) | |
| ax.set_aspect('equal') | |
| ax.axis('off') | |
| bg_wedge = Wedge((0, 0), 1.0, 180, 0, facecolor='#4CAF50', edgecolor='none', width=0.3) | |
| ax.add_patch(bg_wedge) | |
| fg_wedge = Wedge((0, 0), 1.0, 180, angle, facecolor='#FFFFFF', edgecolor='none', width=0.3) | |
| ax.add_patch(fg_wedge) | |
| num_ticks = 6 | |
| for i in range(num_ticks + 1): | |
| t_ratio = i / float(num_ticks) | |
| t_angle = math.radians(180 - (180 * t_ratio)) | |
| inner_r, outer_r = 0.7, 0.9 | |
| xi_inner, yi_inner = inner_r * math.cos(t_angle), inner_r * math.sin(t_angle) | |
| xi_outer, yi_outer = outer_r * math.cos(t_angle), outer_r * math.sin(t_angle) | |
| ax.plot([xi_inner, xi_outer], [yi_inner, yi_outer], color='#333', lw=1) | |
| lbl_r = 0.55 | |
| xl, yl = lbl_r * math.cos(t_angle), lbl_r * math.sin(t_angle) | |
| lbl_val = int(round(max_value * t_ratio)) | |
| ax.text(xl, yl, str(lbl_val), ha='center', va='center', fontsize=8, color='#333') | |
| needle_length = 0.85 | |
| needle_angle = math.radians(angle) | |
| xn, yn = needle_length * math.cos(needle_angle), needle_length * math.sin(needle_angle) | |
| ax.plot([0, xn], [0, yn], color='red', lw=2) | |
| ax.add_patch(Circle((0, 0), 0.05, color='black')) | |
| ax.text(0, -0.15, f"{title}", ha='center', va='center', fontsize=10, weight='bold') | |
| ax.text(0, -0.32, f"{val:.2f} {unit}", ha='center', va='center', fontsize=10, color='#216e39') | |
| ax.set_xlim(-1.05, 1.05) | |
| ax.set_ylim(-0.35, 1.05) | |
| return fig | |
| # ------------------------- | |
| # Routes | |
| # ------------------------- | |
| def index(): | |
| return render_template('index.html') | |
| def fetch_weather_route(): | |
| city = request.args.get('city') | |
| if city: | |
| temperature, humidity, description, pressure = get_weather(city) | |
| if temperature is not None: | |
| return jsonify({ | |
| 'description': description.capitalize(), | |
| 'temperature': temperature, | |
| 'humidity': humidity, | |
| 'pressure': pressure | |
| }) | |
| else: | |
| return jsonify({'error': 'Could not fetch weather data.'}), 404 | |
| return jsonify({'error': 'City parameter missing.'}), 400 | |
| def predict(): | |
| if svm_poly_model is None: | |
| return "Model not loaded, cannot perform prediction.", 500 | |
| crop_type = request.form['crop_type'] | |
| soil_type = request.form['soil_type'] | |
| city = request.form['city'] | |
| try: | |
| motor_capacity = float(request.form['motor_capacity']) | |
| if motor_capacity <= 0: | |
| return "Motor capacity must be a positive number.", 400 | |
| except ValueError: | |
| return "Invalid motor capacity.", 400 | |
| temperature, humidity, description, pressure = get_weather(city) | |
| if temperature is None: | |
| temperature, humidity, description, pressure = 32.0, 60, "Not Available", 1012 | |
| weather_condition = 'NORMAL' | |
| else: | |
| desc_lower = description.lower() | |
| weather_condition = ('SUNNY' if 'clear' in desc_lower or 'sun' in desc_lower else | |
| 'RAINY' if 'rain' in desc_lower or 'drizzle' in desc_lower else | |
| 'WINDY' if 'wind' in desc_lower else 'NORMAL') | |
| if crop_type not in crop_type_mapping or soil_type not in soil_type_mapping: | |
| return "Invalid Crop Type or Soil Type.", 400 | |
| user_data = pd.DataFrame({ | |
| 'CROP TYPE': [crop_type_mapping[crop_type]], | |
| 'SOIL TYPE': [soil_type_mapping[soil_type]], | |
| 'TEMPERATURE': [temperature], | |
| 'WEATHER CONDITION': [weather_condition_mapping[weather_condition]] | |
| }) | |
| water_requirement = svm_poly_model.predict(user_data)[0] | |
| estimated_time_seconds = water_requirement / motor_capacity if motor_capacity > 0 else 0 | |
| if estimated_time_seconds < 120: | |
| time_unit, display_time = "seconds", estimated_time_seconds | |
| else: | |
| time_unit, display_time = "minutes", estimated_time_seconds / 60 | |
| irrigation_state['current'] = { | |
| "estimated_time_seconds": estimated_time_seconds, | |
| "estimated_time_duration": display_time, | |
| "time_unit": time_unit, | |
| "crop_type": crop_type, | |
| "city": city, | |
| "monitoring_active": False, | |
| "timer_start_time": 0 | |
| } | |
| save_irrigation_state(irrigation_state) | |
| message_to_farmer = ( | |
| f"💧 Irrigation Prediction Ready 💧\n\n" | |
| f"Crop: *{crop_type}*\n" | |
| f"Location: *{city}*\n" | |
| f"Weather: {description.capitalize()}, {temperature}°C\n" | |
| f"Water Needed: *{water_requirement:.2f} m³/sq.m*\n" | |
| f"Est. Motor Time: *{display_time:.2f} {time_unit}*\n\n" | |
| "Reply *1* to START the motor and monitoring.\n" | |
| "Reply *0* to CANCEL." | |
| ) | |
| send_whatsapp_message(USER_PHONE_NUMBER, message_to_farmer) | |
| water_img = None | |
| time_img = None | |
| try: | |
| max_water_axis = max(100, water_requirement * 1.5) | |
| fig_w = draw_gauge(water_requirement, max_water_axis, title="Water Req (m³/sq.m)") | |
| water_img = fig_to_datauri(fig_w) | |
| except Exception as e: | |
| print(f"Could not generate water gauge: {e}") | |
| try: | |
| max_time_axis = 60 if time_unit == 'seconds' else max(120, display_time * 1.5) | |
| fig_t = draw_gauge(display_time, max_time_axis, title=f"Motor Time ({time_unit})", unit=time_unit) | |
| time_img = fig_to_datauri(fig_t) | |
| except Exception as e: | |
| print(f"Could not generate time gauge: {e}") | |
| return render_template('predict.html', | |
| water_requirement=round(water_requirement, 2), | |
| estimated_time_duration=round(display_time, 2), | |
| time_unit=time_unit, | |
| water_img=water_img, | |
| time_img=time_img) | |
| def twilio_reply(): | |
| # Reload state from file to get latest data | |
| global irrigation_state | |
| irrigation_state = load_irrigation_state() | |
| message_body = request.values.get('Body', '').strip() | |
| print(f"[DEBUG] Received WhatsApp message: '{message_body}'") | |
| print(f"[DEBUG] Current irrigation_state: {irrigation_state}") | |
| resp = MessagingResponse() | |
| irrigation_params = irrigation_state.get('current') | |
| print(f"[DEBUG] irrigation_params: {irrigation_params}") | |
| if message_body == "1": | |
| print(f"[DEBUG] User replied '1' to start motor") | |
| if irrigation_params and 'estimated_time_seconds' in irrigation_params: | |
| print(f"[DEBUG] Valid irrigation params found!") | |
| duration_sec = irrigation_params['estimated_time_seconds'] | |
| irrigation_params['monitoring_active'] = True | |
| irrigation_params['timer_start_time'] = time.time() | |
| irrigation_state['current'] = irrigation_params | |
| save_irrigation_state(irrigation_state) | |
| print(f"[DEBUG] State saved, starting timer for {duration_sec} seconds") | |
| timer = threading.Timer(duration_sec, trigger_irrigation_complete, args=[ | |
| irrigation_params['crop_type'], | |
| irrigation_params['city'], | |
| irrigation_params['estimated_time_duration'], | |
| irrigation_params['time_unit'], | |
| USER_PHONE_NUMBER | |
| ]) | |
| timer.daemon = True | |
| timer.start() | |
| resp.message( | |
| f"✅ Motor started! Irrigation will run for {irrigation_params['estimated_time_duration']:.2f} {irrigation_params['time_unit']}." | |
| ) | |
| print(f"[DEBUG] Success message sent") | |
| else: | |
| print(f"[DEBUG] ERROR: No valid irrigation params found!") | |
| print(f"[DEBUG] irrigation_params is None: {irrigation_params is None}") | |
| if irrigation_params: | |
| print(f"[DEBUG] 'estimated_time_seconds' in params: {'estimated_time_seconds' in irrigation_params}") | |
| resp.message("❌ Error: No pending irrigation task found.") | |
| elif message_body == "0": | |
| print(f"[DEBUG] User replied '0' to cancel") | |
| resp.message("👍 Motor start has been canceled.") | |
| irrigation_state.pop('current', None) | |
| save_irrigation_state(irrigation_state) | |
| else: | |
| print(f"[DEBUG] Invalid reply received: '{message_body}'") | |
| resp.message("Invalid reply. Please reply '1' to start or '0' to cancel.") | |
| return str(resp) | |
| def monitoring_status(): | |
| # Reload state from file to get latest data | |
| global irrigation_state | |
| irrigation_state = load_irrigation_state() | |
| irrigation_params = irrigation_state.get('current') | |
| if not irrigation_params: | |
| return jsonify({"monitoring": False}) | |
| is_active = irrigation_params.get("monitoring_active", False) | |
| time_remaining = None | |
| if is_active: | |
| start_time = irrigation_params.get("timer_start_time", 0) | |
| total_duration = irrigation_params.get("estimated_time_seconds", 0) | |
| elapsed_time = time.time() - start_time | |
| time_remaining = max(0, total_duration - elapsed_time) | |
| if time_remaining <= 0: | |
| is_active = False | |
| irrigation_state.pop('current', None) | |
| save_irrigation_state(irrigation_state) | |
| print("Monitoring finished, state cleared.") | |
| return jsonify({ | |
| "monitoring": is_active, | |
| "time_remaining": time_remaining, | |
| "total_duration": irrigation_params.get("estimated_time_seconds", 0) | |
| }) | |
| if __name__ == "__main__": | |
| app.run(host="0.0.0.0", port=7860, debug=True) | |