pranit144 commited on
Commit
37b0ef7
·
verified ·
1 Parent(s): 49b2da0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +14 -78
app.py CHANGED
@@ -5,7 +5,7 @@ import requests
5
  import json
6
  import matplotlib
7
 
8
- matplotlib.use('Agg') # headless backend for servers
9
  import matplotlib.pyplot as plt
10
  import io
11
  import base64
@@ -14,23 +14,13 @@ from twilio.twiml.messaging_response import MessagingResponse
14
  import threading
15
  import os
16
  import time
17
- import secrets # For generating a secure secret key
18
 
19
  app = Flask(__name__)
20
 
21
- # --- Configuration for Flask Session ---
22
- # Use an environment variable for the secret key in production.
23
- # For development, a random key can be generated, but it will change on each restart,
24
- # invalidating existing sessions.
25
  app.secret_key = os.getenv('FLASK_SECRET_KEY', secrets.token_hex(24))
26
- # Note: For multi-process deployments (like Gunicorn with multiple workers),
27
- # client-side sessions (default Flask sessions) can still work as long as
28
- # the SECRET_KEY is consistent across all workers. For server-side sessions
29
- # (e.g., using Flask-Session with Redis), additional setup is required.
30
 
31
-
32
- # --- Load the pre-trained SVM model ---
33
- svm_poly_model = None # Initialize to None
34
  try:
35
  svm_poly_model = joblib.load('svm_poly_model.pkl')
36
  print("SVM model loaded successfully.")
@@ -39,7 +29,6 @@ except FileNotFoundError:
39
  except Exception as e:
40
  print(f"Error loading SVM model: {e}")
41
 
42
- # --- Data Mappings for the Model ---
43
  crop_type_mapping = {
44
  'BANANA': 0, 'BEAN': 1, 'CABBAGE': 2, 'CITRUS': 3, 'COTTON': 4, 'MAIZE': 5,
45
  'MELON': 6, 'MUSTARD': 7, 'ONION': 8, 'OTHER': 9, 'POTATO': 10, 'RICE': 11,
@@ -48,23 +37,20 @@ crop_type_mapping = {
48
  soil_type_mapping = {'DRY': 0, 'HUMID': 1, 'WET': 2}
49
  weather_condition_mapping = {'NORMAL': 0, 'RAINY': 1, 'SUNNY': 2, 'WINDY': 3}
50
 
51
- # --- API Keys and Credentials ---
52
  WEATHER_API_KEY = os.getenv('WEATHER_API', 'ee75ffd59875aa5ca6c207e594336b30')
53
  TWILIO_ACCOUNT_SID = os.getenv('TWILIO_ACCOUNT_SID', 'ACe45f7038c5338a153d1126ca6d547c84')
54
  TWILIO_AUTH_TOKEN = os.getenv('TWILIO_AUTH_TOKEN', '48b9eea898885ef395d48edc74924340')
55
  TWILIO_PHONE_NUMBER = 'whatsapp:+14155238886'
56
- USER_PHONE_NUMBER = os.getenv('FARMER_PHONE_NUMBER', 'whatsapp:+919763059811') # The farmer's WhatsApp number
57
 
58
- # Initialize Twilio Client
59
  twilio_client = Client(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN)
60
 
61
 
62
  def get_weather(city: str):
63
- """Fetches weather data from OpenWeatherMap API."""
64
  url = f"https://api.openweathermap.org/data/2.5/weather?q={city}&appid={WEATHER_API_KEY}&units=metric"
65
  try:
66
  response = requests.get(url)
67
- response.raise_for_status() # Raise an exception for HTTP errors
68
  data = response.json()
69
  if data.get('cod') == 200:
70
  return (data['main']['temp'], data['main']['humidity'],
@@ -79,7 +65,6 @@ def get_weather(city: str):
79
 
80
 
81
  def send_whatsapp_message(to_number, body_text):
82
- """General function to send WhatsApp messages via Twilio."""
83
  try:
84
  message = twilio_client.messages.create(
85
  from_=TWILIO_PHONE_NUMBER,
@@ -93,9 +78,7 @@ def send_whatsapp_message(to_number, body_text):
93
  return None
94
 
95
 
96
- # Modified to accept parameters directly
97
  def trigger_irrigation_complete(crop_type, city, estimated_time_duration, time_unit, user_phone):
98
- """Function called by the timer when irrigation is finished."""
99
  message_text = (
100
  f"✅ Irrigation Complete!\n\n"
101
  f"Crop: {crop_type}\n"
@@ -105,105 +88,73 @@ def trigger_irrigation_complete(crop_type, city, estimated_time_duration, time_u
105
  )
106
  send_whatsapp_message(user_phone, message_text)
107
  print(f"Irrigation complete message sent to {user_phone} after {estimated_time_duration:.2f} {time_unit}.")
108
- # No need to clear session data here, as this function runs in a separate thread.
109
- # Session data will be cleared on the next user interaction or upon explicit clearing.
110
 
111
 
112
- # --- Matplotlib PNG gauges helper ---
113
  import math
114
  from matplotlib.patches import Wedge, Circle
115
 
116
 
117
  def fig_to_datauri(fig, fmt='png', dpi=150):
118
- """Converts a matplotlib figure to a base64 data URI."""
119
  buf = io.BytesIO()
120
- fig.savefig(buf, format=fmt, bbox_inches='tight', dpi=dpi) #
121
  buf.seek(0)
122
  data = base64.b64encode(buf.read()).decode('ascii')
123
- plt.close(fig) # Essential to prevent memory leaks in a web app
124
  return f"data:image/{fmt};base64,{data}"
125
 
126
 
127
  def draw_gauge(value, max_value, title="", unit=""):
128
- """Draws a semi-circular gauge using matplotlib."""
129
- # Clamp the value between 0 and max_value
130
  val = max(0.0, min(value, max_value))
131
- # Calculate the ratio (0.0 to 1.0)
132
  ratio = val / float(max_value) if max_value > 0 else 0.0
133
- # Angles for semicircle: left (180) -> right (0)
134
  start_angle, end_angle = 180, 0
135
- # Calculate the angle where the filled part stops
136
  angle = start_angle + (end_angle - start_angle) * ratio
137
 
138
  fig, ax = plt.subplots(figsize=(5, 3))
139
  ax.set_aspect('equal')
140
  ax.axis('off')
141
 
142
- # --- COLOR CHANGES HERE ---
143
-
144
- # Background arc (full gauge)
145
- # Changed from '#ececec' (light gray) to '#4CAF50' (Green)
146
  bg_wedge = Wedge((0, 0), 1.0, 180, 0, facecolor='#4CAF50', edgecolor='none', width=0.3)
147
-
148
-
149
-
150
-
151
-
152
-
153
  ax.add_patch(bg_wedge)
154
 
155
- # Foreground arc (value representation)
156
- # Changed from '#4169e1' (Royal Blue) to '#FFFFFF' (White)
157
  fg_wedge = Wedge((0, 0), 1.0, 180, angle, facecolor='#FFFFFF', edgecolor='none', width=0.3)
158
  ax.add_patch(fg_wedge)
159
-
160
- # --------------------------
161
 
162
- # Draw ticks and labels
163
  num_ticks = 6
164
  for i in range(num_ticks + 1):
165
  t_ratio = i / float(num_ticks)
 
166
  inner_r, outer_r = 0.7, 0.9
167
  xi_inner, yi_inner = inner_r * math.cos(t_angle), inner_r * math.sin(t_angle)
168
  xi_outer, yi_outer = outer_r * math.cos(t_angle), outer_r * math.sin(t_angle)
169
- # Ticks might need to be slightly darker or lighter depending on the green you chose
170
  ax.plot([xi_inner, xi_outer], [yi_inner, yi_outer], color='#333', lw=1)
171
 
172
- # Label positioning
173
  lbl_r = 0.55
174
  xl, yl = lbl_r * math.cos(t_angle), lbl_r * math.sin(t_angle)
175
  lbl_val = int(round(max_value * t_ratio))
176
  ax.text(xl, yl, str(lbl_val), horizontalalignment='center', verticalalignment='center', fontsize=8, color='#333')
177
 
178
- # Needle
179
  needle_length = 0.85
180
  needle_angle = math.radians(angle)
181
  xn, yn = needle_length * math.cos(needle_angle), needle_length * math.sin(needle_angle)
182
  ax.plot([0, xn], [0, yn], color='red', lw=2)
183
- # Center circle
184
  center = Circle((0, 0), 0.05, color='black')
185
  ax.add_patch(center)
186
 
187
- # Title and value text
188
  ax.text(0, -0.15, f"{title}", horizontalalignment='center', verticalalignment='center', fontsize=10, weight='bold')
189
- # Kept the value text dark green, which should still look good
190
  ax.text(0, -0.32, f"{val:.2f} {unit}", horizontalalignment='center', verticalalignment='center', fontsize=10, color='#216e39')
191
 
192
- # Adjust limits to fit the semi-circle and text
193
  ax.set_xlim(-1.05, 1.05)
194
  ax.set_ylim(-0.35, 1.05)
195
 
196
  return fig
197
 
198
- # --- Flask Routes ---
199
-
200
  @app.route('/')
201
  def index():
202
  return render_template('index.html')
203
 
204
 
205
  @app.route('/fetch_weather', methods=['GET'])
206
- def fetch_weather_route(): # Renamed to avoid clash with utility function
207
  city = request.args.get('city')
208
  if city:
209
  temperature, humidity, description, pressure = get_weather(city)
@@ -224,7 +175,6 @@ def predict():
224
  if svm_poly_model is None:
225
  return "Model not loaded, cannot perform prediction.", 500
226
 
227
- # Clear previous session data for a new prediction
228
  session.pop('irrigation_params', None)
229
 
230
  crop_type = request.form['crop_type']
@@ -250,7 +200,6 @@ def predict():
250
  'WINDY' if 'wind' in desc_lower else
251
  'NORMAL')
252
 
253
- # Basic input validation for mappings
254
  if crop_type not in crop_type_mapping or soil_type not in soil_type_mapping:
255
  return "Invalid Crop Type or Soil Type.", 400
256
 
@@ -269,7 +218,6 @@ def predict():
269
  else:
270
  time_unit, display_time = "minutes", estimated_time_seconds / 60
271
 
272
- # Store all necessary data in the user session
273
  session['irrigation_params'] = {
274
  "estimated_time_seconds": estimated_time_seconds,
275
  "estimated_time_duration": display_time,
@@ -292,7 +240,6 @@ def predict():
292
  )
293
  send_whatsapp_message(USER_PHONE_NUMBER, message_to_farmer)
294
 
295
- # --- Matplotlib PNG gauges (embedded as data URIs) ---
296
  water_img = None
297
  time_img = None
298
  try:
@@ -323,20 +270,16 @@ def twilio_reply():
323
  message_body = request.values.get('Body', '').strip()
324
  resp = MessagingResponse()
325
 
326
- # Retrieve irrigation parameters from session
327
  irrigation_params = session.get('irrigation_params')
328
 
329
  if message_body == "1":
330
  if irrigation_params and 'estimated_time_seconds' in irrigation_params:
331
  duration_sec = irrigation_params['estimated_time_seconds']
332
 
333
- # Update session state for monitoring
334
  irrigation_params['monitoring_active'] = True
335
  irrigation_params['timer_start_time'] = time.time()
336
- session['irrigation_params'] = irrigation_params # Save updated session
337
 
338
- # Start the background timer to run the completion function
339
- # Pass all necessary details to the timer callback
340
  timer = threading.Timer(duration_sec, trigger_irrigation_complete, args=[
341
  irrigation_params['crop_type'],
342
  irrigation_params['city'],
@@ -344,7 +287,7 @@ def twilio_reply():
344
  irrigation_params['time_unit'],
345
  USER_PHONE_NUMBER
346
  ])
347
- timer.daemon = True # Allow main program to exit even if timer is running
348
  timer.start()
349
 
350
  resp.message(
@@ -353,18 +296,15 @@ def twilio_reply():
353
  resp.message("❌ Error: No pending irrigation task found. Please submit a new prediction first.")
354
  elif message_body == "0":
355
  resp.message("👍 Motor start has been canceled.")
356
- session.pop('irrigation_params', None) # Clear params from session
357
  else:
358
  resp.message("Invalid reply. Please reply '1' to start or '0' to cancel.")
359
 
360
  return str(resp)
361
 
362
 
363
- # --- NEW ENDPOINT FOR UI POLLING ---
364
  @app.route('/monitoring_status')
365
  def monitoring_status():
366
- """Provides the current monitoring status to the front-end."""
367
- # Retrieve irrigation parameters from session
368
  irrigation_params = session.get('irrigation_params')
369
 
370
  if not irrigation_params:
@@ -378,21 +318,17 @@ def monitoring_status():
378
  total_duration = irrigation_params.get("estimated_time_seconds", 0)
379
  elapsed_time = time.time() - start_time
380
  time_remaining = max(0, total_duration - elapsed_time)
381
- # If time_remaining is 0 or less, consider irrigation complete for monitoring UI
382
  if time_remaining <= 0:
383
  is_active = False
384
- session.pop('irrigation_params', None) # Automatically clear session if timer finished
385
  print("Monitoring status detected irrigation completion and cleared session.")
386
 
387
  return jsonify({
388
  "monitoring": is_active,
389
  "time_remaining": time_remaining,
390
- "total_duration": irrigation_params.get("estimated_time_seconds", 0) # Added total duration for progress bar
391
  })
392
 
393
 
394
  if __name__ == "__main__":
395
- # In a production environment, set FLASK_SECRET_KEY as an environment variable
396
- # e.g., export FLASK_SECRET_KEY='your_very_secret_and_long_random_key_here'
397
- # app.run(host="0.0.0.0", port=7860, debug=False) # debug=False for production
398
  app.run(host="0.0.0.0", port=7860, debug=True)
 
5
  import json
6
  import matplotlib
7
 
8
+ matplotlib.use('Agg')
9
  import matplotlib.pyplot as plt
10
  import io
11
  import base64
 
14
  import threading
15
  import os
16
  import time
17
+ import secrets
18
 
19
  app = Flask(__name__)
20
 
 
 
 
 
21
  app.secret_key = os.getenv('FLASK_SECRET_KEY', secrets.token_hex(24))
 
 
 
 
22
 
23
+ svm_poly_model = None
 
 
24
  try:
25
  svm_poly_model = joblib.load('svm_poly_model.pkl')
26
  print("SVM model loaded successfully.")
 
29
  except Exception as e:
30
  print(f"Error loading SVM model: {e}")
31
 
 
32
  crop_type_mapping = {
33
  'BANANA': 0, 'BEAN': 1, 'CABBAGE': 2, 'CITRUS': 3, 'COTTON': 4, 'MAIZE': 5,
34
  'MELON': 6, 'MUSTARD': 7, 'ONION': 8, 'OTHER': 9, 'POTATO': 10, 'RICE': 11,
 
37
  soil_type_mapping = {'DRY': 0, 'HUMID': 1, 'WET': 2}
38
  weather_condition_mapping = {'NORMAL': 0, 'RAINY': 1, 'SUNNY': 2, 'WINDY': 3}
39
 
 
40
  WEATHER_API_KEY = os.getenv('WEATHER_API', 'ee75ffd59875aa5ca6c207e594336b30')
41
  TWILIO_ACCOUNT_SID = os.getenv('TWILIO_ACCOUNT_SID', 'ACe45f7038c5338a153d1126ca6d547c84')
42
  TWILIO_AUTH_TOKEN = os.getenv('TWILIO_AUTH_TOKEN', '48b9eea898885ef395d48edc74924340')
43
  TWILIO_PHONE_NUMBER = 'whatsapp:+14155238886'
44
+ USER_PHONE_NUMBER = os.getenv('FARMER_PHONE_NUMBER', 'whatsapp:+919763059811')
45
 
 
46
  twilio_client = Client(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN)
47
 
48
 
49
  def get_weather(city: str):
 
50
  url = f"https://api.openweathermap.org/data/2.5/weather?q={city}&appid={WEATHER_API_KEY}&units=metric"
51
  try:
52
  response = requests.get(url)
53
+ response.raise_for_status()
54
  data = response.json()
55
  if data.get('cod') == 200:
56
  return (data['main']['temp'], data['main']['humidity'],
 
65
 
66
 
67
  def send_whatsapp_message(to_number, body_text):
 
68
  try:
69
  message = twilio_client.messages.create(
70
  from_=TWILIO_PHONE_NUMBER,
 
78
  return None
79
 
80
 
 
81
  def trigger_irrigation_complete(crop_type, city, estimated_time_duration, time_unit, user_phone):
 
82
  message_text = (
83
  f"✅ Irrigation Complete!\n\n"
84
  f"Crop: {crop_type}\n"
 
88
  )
89
  send_whatsapp_message(user_phone, message_text)
90
  print(f"Irrigation complete message sent to {user_phone} after {estimated_time_duration:.2f} {time_unit}.")
 
 
91
 
92
 
 
93
  import math
94
  from matplotlib.patches import Wedge, Circle
95
 
96
 
97
  def fig_to_datauri(fig, fmt='png', dpi=150):
 
98
  buf = io.BytesIO()
99
+ fig.savefig(buf, format=fmt, bbox_inches='tight', dpi=dpi)
100
  buf.seek(0)
101
  data = base64.b64encode(buf.read()).decode('ascii')
102
+ plt.close(fig)
103
  return f"data:image/{fmt};base64,{data}"
104
 
105
 
106
  def draw_gauge(value, max_value, title="", unit=""):
 
 
107
  val = max(0.0, min(value, max_value))
 
108
  ratio = val / float(max_value) if max_value > 0 else 0.0
 
109
  start_angle, end_angle = 180, 0
 
110
  angle = start_angle + (end_angle - start_angle) * ratio
111
 
112
  fig, ax = plt.subplots(figsize=(5, 3))
113
  ax.set_aspect('equal')
114
  ax.axis('off')
115
 
 
 
 
 
116
  bg_wedge = Wedge((0, 0), 1.0, 180, 0, facecolor='#4CAF50', edgecolor='none', width=0.3)
 
 
 
 
 
 
117
  ax.add_patch(bg_wedge)
118
 
 
 
119
  fg_wedge = Wedge((0, 0), 1.0, 180, angle, facecolor='#FFFFFF', edgecolor='none', width=0.3)
120
  ax.add_patch(fg_wedge)
 
 
121
 
 
122
  num_ticks = 6
123
  for i in range(num_ticks + 1):
124
  t_ratio = i / float(num_ticks)
125
+ t_angle = math.radians(180 - (180 * t_ratio))
126
  inner_r, outer_r = 0.7, 0.9
127
  xi_inner, yi_inner = inner_r * math.cos(t_angle), inner_r * math.sin(t_angle)
128
  xi_outer, yi_outer = outer_r * math.cos(t_angle), outer_r * math.sin(t_angle)
 
129
  ax.plot([xi_inner, xi_outer], [yi_inner, yi_outer], color='#333', lw=1)
130
 
 
131
  lbl_r = 0.55
132
  xl, yl = lbl_r * math.cos(t_angle), lbl_r * math.sin(t_angle)
133
  lbl_val = int(round(max_value * t_ratio))
134
  ax.text(xl, yl, str(lbl_val), horizontalalignment='center', verticalalignment='center', fontsize=8, color='#333')
135
 
 
136
  needle_length = 0.85
137
  needle_angle = math.radians(angle)
138
  xn, yn = needle_length * math.cos(needle_angle), needle_length * math.sin(needle_angle)
139
  ax.plot([0, xn], [0, yn], color='red', lw=2)
 
140
  center = Circle((0, 0), 0.05, color='black')
141
  ax.add_patch(center)
142
 
 
143
  ax.text(0, -0.15, f"{title}", horizontalalignment='center', verticalalignment='center', fontsize=10, weight='bold')
 
144
  ax.text(0, -0.32, f"{val:.2f} {unit}", horizontalalignment='center', verticalalignment='center', fontsize=10, color='#216e39')
145
 
 
146
  ax.set_xlim(-1.05, 1.05)
147
  ax.set_ylim(-0.35, 1.05)
148
 
149
  return fig
150
 
 
 
151
  @app.route('/')
152
  def index():
153
  return render_template('index.html')
154
 
155
 
156
  @app.route('/fetch_weather', methods=['GET'])
157
+ def fetch_weather_route():
158
  city = request.args.get('city')
159
  if city:
160
  temperature, humidity, description, pressure = get_weather(city)
 
175
  if svm_poly_model is None:
176
  return "Model not loaded, cannot perform prediction.", 500
177
 
 
178
  session.pop('irrigation_params', None)
179
 
180
  crop_type = request.form['crop_type']
 
200
  'WINDY' if 'wind' in desc_lower else
201
  'NORMAL')
202
 
 
203
  if crop_type not in crop_type_mapping or soil_type not in soil_type_mapping:
204
  return "Invalid Crop Type or Soil Type.", 400
205
 
 
218
  else:
219
  time_unit, display_time = "minutes", estimated_time_seconds / 60
220
 
 
221
  session['irrigation_params'] = {
222
  "estimated_time_seconds": estimated_time_seconds,
223
  "estimated_time_duration": display_time,
 
240
  )
241
  send_whatsapp_message(USER_PHONE_NUMBER, message_to_farmer)
242
 
 
243
  water_img = None
244
  time_img = None
245
  try:
 
270
  message_body = request.values.get('Body', '').strip()
271
  resp = MessagingResponse()
272
 
 
273
  irrigation_params = session.get('irrigation_params')
274
 
275
  if message_body == "1":
276
  if irrigation_params and 'estimated_time_seconds' in irrigation_params:
277
  duration_sec = irrigation_params['estimated_time_seconds']
278
 
 
279
  irrigation_params['monitoring_active'] = True
280
  irrigation_params['timer_start_time'] = time.time()
281
+ session['irrigation_params'] = irrigation_params
282
 
 
 
283
  timer = threading.Timer(duration_sec, trigger_irrigation_complete, args=[
284
  irrigation_params['crop_type'],
285
  irrigation_params['city'],
 
287
  irrigation_params['time_unit'],
288
  USER_PHONE_NUMBER
289
  ])
290
+ timer.daemon = True
291
  timer.start()
292
 
293
  resp.message(
 
296
  resp.message("❌ Error: No pending irrigation task found. Please submit a new prediction first.")
297
  elif message_body == "0":
298
  resp.message("👍 Motor start has been canceled.")
299
+ session.pop('irrigation_params', None)
300
  else:
301
  resp.message("Invalid reply. Please reply '1' to start or '0' to cancel.")
302
 
303
  return str(resp)
304
 
305
 
 
306
  @app.route('/monitoring_status')
307
  def monitoring_status():
 
 
308
  irrigation_params = session.get('irrigation_params')
309
 
310
  if not irrigation_params:
 
318
  total_duration = irrigation_params.get("estimated_time_seconds", 0)
319
  elapsed_time = time.time() - start_time
320
  time_remaining = max(0, total_duration - elapsed_time)
 
321
  if time_remaining <= 0:
322
  is_active = False
323
+ session.pop('irrigation_params', None)
324
  print("Monitoring status detected irrigation completion and cleared session.")
325
 
326
  return jsonify({
327
  "monitoring": is_active,
328
  "time_remaining": time_remaining,
329
+ "total_duration": irrigation_params.get("estimated_time_seconds", 0)
330
  })
331
 
332
 
333
  if __name__ == "__main__":
 
 
 
334
  app.run(host="0.0.0.0", port=7860, debug=True)