Spaces:
Sleeping
Sleeping
from flask import Flask, render_template, request, jsonify, redirect, url_for, flash, session, send_file | |
from flask_login import LoginManager, login_user, logout_user, login_required, current_user, UserMixin | |
import os | |
from datetime import datetime, date, timedelta | |
import math | |
import json | |
import logging | |
from functools import wraps | |
from apscheduler.schedulers.background import BackgroundScheduler | |
from dotenv import load_dotenv | |
# Load environment variables | |
load_dotenv() | |
# Import our custom modules | |
from models import db, Farmer, Farm, SoilData, FarmingActivity, WeatherData, DailyAdvisory, SMSLog, AdminUser, YearlyPlan, WeatherAlert, MarketPrice, DiseaseDetection, DailyTask, TaskCompletion | |
from gemini_service import GeminiAIService, format_sms_message | |
from telegram_service import TelegramBotService, format_daily_advisory_telegram, format_yearly_plan_summary, format_complete_yearly_plan_telegram, format_enhanced_yearly_plan_telegram | |
from weather_service import WeatherService, AgroWeatherService | |
from weather_alert_service import WeatherAlertService | |
from market_price_service import MarketPriceService | |
from disease_detection_service import DiseaseDetectionService | |
from pdf_generator_service import PDFGeneratorService | |
from daily_task_service import DailyTaskService | |
# Configure logging | |
logging.basicConfig(level=logging.INFO) | |
logger = logging.getLogger(__name__) | |
# Initialize Flask app | |
app = Flask(__name__) | |
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'your-secret-key-here') | |
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///farm_management.db' | |
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False | |
# Initialize extensions | |
db.init_app(app) | |
login_manager = LoginManager() | |
login_manager.init_app(app) | |
login_manager.login_view = 'farmer_login' | |
# Initialize services | |
gemini_service = None | |
telegram_service = None | |
weather_service = None | |
def initialize_services(): | |
"""Initialize AI and API services""" | |
global gemini_service, weather_service, telegram_service, weather_alert_service | |
global market_price_service, disease_detection_service, pdf_generator_service, daily_task_service | |
# Initialize Gemini AI Service | |
gemini_api_key = os.getenv('GEMINI_API_KEY') | |
if gemini_api_key: | |
gemini_service = GeminiAIService(gemini_api_key) | |
logger.info("Gemini AI service initialized") | |
else: | |
gemini_service = None | |
logger.warning("Gemini API key not found") | |
# Initialize Telegram Bot Service | |
telegram_token = os.getenv('TELEGRAM_BOT_TOKEN') | |
if telegram_token: | |
telegram_service = TelegramBotService(telegram_token) | |
# Test bot connection | |
bot_info = telegram_service.get_me() | |
if bot_info: | |
logger.info(f"Telegram Bot initialized: @{bot_info.get('username', 'Unknown')}") | |
else: | |
logger.warning("Telegram Bot token invalid or network error") | |
telegram_service = None | |
else: | |
telegram_service = None | |
logger.warning("Telegram Bot token not found") | |
# Initialize Weather Service (support both WEATHER_API_KEY and OPENWEATHER_API_KEY) | |
weather_api_key = os.getenv('WEATHER_API_KEY') or os.getenv('OPENWEATHER_API_KEY') | |
if weather_api_key: | |
weather_service = AgroWeatherService(weather_api_key) | |
weather_alert_service = WeatherAlertService(weather_api_key) | |
logger.info("Weather services initialized") | |
else: | |
weather_service = None | |
weather_alert_service = None | |
logger.warning("Weather API key not found") | |
# Initialize Market Price Service | |
market_price_service = MarketPriceService() | |
logger.info("Market price service initialized") | |
# Initialize Disease Detection Service | |
if gemini_api_key: | |
disease_detection_service = DiseaseDetectionService(gemini_api_key) | |
logger.info("Disease detection service initialized") | |
else: | |
disease_detection_service = None | |
logger.warning("Disease detection service unavailable (no Gemini API key)") | |
# Initialize PDF Generator Service | |
pdf_generator_service = PDFGeneratorService() | |
logger.info("PDF generator service initialized") | |
# Initialize Daily Task Service | |
daily_task_service = DailyTaskService(gemini_service, weather_service) | |
logger.info("Daily task service initialized") | |
# Optional: preload FARMER_TELEGRAM_CHAT_IDS from environment to DB on startup | |
mapping_raw = os.getenv('FARMER_TELEGRAM_CHAT_IDS') | |
if mapping_raw: | |
try: | |
# Expect format: "<farmer_id>:<chat_id>,<farmer_id2>:<chat_id2>" | |
pairs = [p.strip() for p in mapping_raw.split(',') if p.strip()] | |
with app.app_context(): | |
for pair in pairs: | |
if ':' not in pair: | |
logger.warning(f"Skipping invalid FARMER_TELEGRAM_CHAT_IDS entry: {pair}") | |
continue | |
fid_str, chat_id = pair.split(':', 1) | |
try: | |
fid = int(fid_str) | |
except ValueError: | |
logger.warning(f"Invalid farmer id in FARMER_TELEGRAM_CHAT_IDS: {fid_str}") | |
continue | |
farmer = Farmer.query.get(fid) | |
if not farmer: | |
logger.warning(f"Farmer id {fid} not found, skipping chat id import") | |
continue | |
if farmer.telegram_chat_id and str(farmer.telegram_chat_id) == str(chat_id): | |
logger.info(f"Farmer {fid} already has telegram_chat_id set, skipping") | |
continue | |
farmer.telegram_chat_id = str(chat_id) | |
db.session.add(farmer) | |
logger.info(f"Imported telegram_chat_id for farmer {fid}") | |
db.session.commit() | |
except Exception as e: | |
logger.error(f"Error importing FARMER_TELEGRAM_CHAT_IDS: {str(e)}") | |
def calculate_area_acres(coordinates): | |
"""Approximate polygon area (in acres) from a list of coordinates. | |
Accepts coordinates in either of two forms: | |
- list of [lng, lat] | |
- list of {'lat': lat, 'lng': lng} | |
Uses a simple equirectangular projection around the polygon centroid to | |
convert degrees to meters, applies the shoelace formula, and converts | |
square meters to acres. This is an approximation sufficient for small to | |
medium farm polygons. | |
""" | |
if not coordinates: | |
return 0.0 | |
# Normalize to list of (lat, lng) | |
pts = [] | |
for c in coordinates: | |
if isinstance(c, (list, tuple)) and len(c) >= 2: | |
lng, lat = float(c[0]), float(c[1]) | |
elif isinstance(c, dict): | |
lat = float(c.get('lat')) | |
lng = float(c.get('lng')) | |
else: | |
continue | |
pts.append((lat, lng)) | |
if len(pts) < 3: | |
return 0.0 | |
# Compute average latitude for projection | |
lat_avg = sum(p[0] for p in pts) / len(pts) | |
lat_avg_rad = math.radians(lat_avg) | |
# Meters per degree (approx) | |
meters_per_deg_lat = 111132.92 | |
meters_per_deg_lon = 111320.0 * math.cos(lat_avg_rad) | |
# Convert to planar coordinates (meters) | |
xy = [] | |
for lat, lng in pts: | |
x = lng * meters_per_deg_lon | |
y = lat * meters_per_deg_lat | |
xy.append((x, y)) | |
# Shoelace formula | |
area_m2 = 0.0 | |
for i in range(len(xy)): | |
x1, y1 = xy[i] | |
x2, y2 = xy[(i + 1) % len(xy)] | |
area_m2 += x1 * y2 - x2 * y1 | |
area_m2 = abs(area_m2) / 2.0 | |
# Convert square meters to acres | |
acres = area_m2 / 4046.8564224 | |
return round(acres, 4) | |
def load_user(user_id): | |
"""Load user for Flask-Login""" | |
try: | |
farmer = Farmer.query.get(int(user_id)) | |
except Exception: | |
return None | |
if not farmer: | |
return None | |
# Return the FarmerUser wrapper so views that use current_user.farmer continue to work | |
return FarmerUser(farmer) | |
# User class for login | |
class FarmerUser(UserMixin): | |
def __init__(self, farmer): | |
self.id = farmer.id | |
self.farmer = farmer | |
def get_current_farmer(): | |
"""Return the underlying Farmer instance for the currently-logged-in user. | |
Some codepaths return a FarmerUser wrapper (with a .farmer attribute). In other | |
cases current_user may already be a Farmer instance. This helper normalizes | |
both cases and returns None for anonymous users. | |
""" | |
try: | |
# Prefer wrapper attribute if present | |
if hasattr(current_user, 'farmer') and getattr(current_user, 'farmer'): | |
return current_user.farmer | |
# If current_user itself is a Farmer model instance, return it | |
if isinstance(current_user, Farmer): | |
return current_user | |
except Exception: | |
return None | |
return None | |
def generate_and_send_yearly_plan(farmer_obj): | |
"""Generate or update a comprehensive yearly plan using Gemini AI and send summary via Telegram if available.""" | |
try: | |
year = date.today().year | |
# Prepare farmer data | |
farmer_data = { | |
'name': farmer_obj.name, | |
'contact_number': getattr(farmer_obj, 'contact_number', 'Unknown'), | |
'address': getattr(farmer_obj, 'address', 'Unknown'), | |
'id': farmer_obj.id | |
} | |
comprehensive_plan = { | |
'year': year, | |
'farmer_id': farmer_obj.id, | |
'farms': [], | |
'summary': '', | |
'generated_at': datetime.now().isoformat() | |
} | |
farm_summaries = [] | |
# Generate plan for each farm | |
for farm in farmer_obj.farms: | |
try: | |
# Get farm data | |
farm_data = { | |
'farm_name': farm.farm_name, | |
'farm_size': farm.farm_size, | |
'crop_types': farm.get_crop_types(), | |
'irrigation_type': getattr(farm, 'irrigation_type', 'Unknown'), | |
'coordinates': {'lat': farm.latitude, 'lon': farm.longitude} if farm.latitude and farm.longitude else None | |
} | |
# Get latest soil data | |
soil_data = {} | |
latest_soil = SoilData.query.filter_by(farm_id=farm.id).order_by(SoilData.id.desc()).first() | |
if latest_soil: | |
soil_data = { | |
'soil_type': latest_soil.soil_type, | |
'ph_level': latest_soil.ph_level, | |
'nitrogen_level': latest_soil.nitrogen_level, | |
'phosphorus_level': getattr(latest_soil, 'phosphorus_level', None), | |
'potassium_level': getattr(latest_soil, 'potassium_level', None), | |
'moisture_percentage': getattr(latest_soil, 'moisture_percentage', None) | |
} | |
else: | |
soil_data = { | |
'soil_type': 'Unknown', | |
'ph_level': 'Unknown', | |
'nitrogen_level': 'Unknown', | |
'phosphorus_level': 'Unknown', | |
'potassium_level': 'Unknown', | |
'moisture_percentage': 'Unknown' | |
} | |
# Generate AI-powered yearly plan for this farm | |
farm_plan = None | |
if gemini_service: | |
try: | |
farm_plan = gemini_service.generate_year_plan(farmer_data, farm_data, soil_data) | |
logger.info(f"Generated comprehensive AI yearly plan for farm {farm.farm_name}") | |
except Exception as e: | |
logger.error(f"Failed to generate AI plan for farm {farm.farm_name}: {str(e)}") | |
# Create fallback plan if AI fails | |
if not farm_plan or 'plan' not in farm_plan: | |
crop_details = farm.get_crop_details() | |
fallback_html = f""" | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<style> | |
body {{ font-family: Arial, sans-serif; margin: 20px; background-color: #f5f5f5; }} | |
.header {{ text-align: center; color: #2e7d32; font-size: 28px; font-weight: bold; margin-bottom: 30px; }} | |
.section {{ margin-bottom: 25px; }} | |
.section-title {{ color: #1976d2; font-size: 20px; font-weight: bold; margin-bottom: 15px; }} | |
table {{ width: 100%; border-collapse: collapse; background-color: #e8f5e8; margin-bottom: 20px; }} | |
th {{ background-color: #4caf50; color: white; padding: 12px; text-align: left; }} | |
td {{ padding: 10px; border: 1px solid #ddd; }} | |
.highlight {{ background-color: #ffeb3b; font-weight: bold; }} | |
</style> | |
</head> | |
<body> | |
<div class="header">वार्षिक खेती योजना - {farm.farm_name}</div> | |
<div class="section"> | |
<div class="section-title">फसल विवरण</div> | |
<table> | |
<tr><th>फसल</th><th>बुआई का समय</th><th>कटाई का समय</th></tr> | |
""" | |
for crop in crop_details: | |
crop_name = crop.get('name', 'अज्ञात') | |
sowing_month = crop.get('sowing_month', 'अज्ञात') | |
fallback_html += f"<tr><td>{crop_name}</td><td>{sowing_month}</td><td>मौसम अनुसार</td></tr>" | |
fallback_html += """ | |
</table> | |
</div> | |
<div class="section"> | |
<div class="section-title">सामान्य सुझाव</div> | |
<p>• नियमित मिट्टी की जांच कराएं</p> | |
<p>• <span class="highlight">समय पर</span> सिंचाई करें</p> | |
<p>• उन्नत बीजों का प्रयोग करें</p> | |
</div> | |
</body> | |
</html> | |
""" | |
farm_plan = { | |
'plan': fallback_html, | |
'type': 'fallback_html', | |
'generated_at': datetime.now().isoformat(), | |
'ai_generated': False | |
} | |
# Add farm plan to comprehensive plan | |
farm_plan_data = { | |
'farm_id': farm.id, | |
'farm_name': farm.farm_name, | |
'plan': farm_plan['plan'], | |
'plan_type': farm_plan.get('type', 'html'), | |
'generated_at': farm_plan['generated_at'], | |
'ai_generated': farm_plan.get('ai_generated', False) | |
} | |
comprehensive_plan['farms'].append(farm_plan_data) | |
# Create summary for this farm | |
crop_list = ', '.join([crop.get('name', 'Unknown') for crop in farm.get_crop_details()]) | |
farm_summaries.append(f"{farm.farm_name} ({farm.farm_size} acres): {crop_list}") | |
except Exception as e: | |
logger.error(f"Error generating plan for farm {farm.farm_name}: {str(e)}") | |
# Add basic fallback for this farm | |
comprehensive_plan['farms'].append({ | |
'farm_id': farm.id, | |
'farm_name': farm.farm_name, | |
'plan': json.dumps({'error': 'Failed to generate plan'}), | |
'generated_at': datetime.now().isoformat() | |
}) | |
# Create overall summary | |
comprehensive_plan['summary'] = f"Yearly Plan for {farmer_obj.name} ({len(farmer_obj.farms)} farms)\n" + "\n".join(farm_summaries) | |
# Save to database | |
existing = YearlyPlan.query.filter_by(farmer_id=farmer_obj.id, year=year).first() | |
if existing: | |
existing.plan_json = json.dumps(comprehensive_plan) | |
existing.summary_text = comprehensive_plan['summary'] | |
db.session.add(existing) | |
else: | |
yp = YearlyPlan( | |
farmer_id=farmer_obj.id, | |
year=year, | |
plan_json=json.dumps(comprehensive_plan), | |
summary_text=comprehensive_plan['summary'] | |
) | |
db.session.add(yp) | |
db.session.commit() | |
logger.info(f"Successfully generated comprehensive yearly plan for farmer {farmer_obj.name}") | |
# Generate PDF report | |
pdf_path = None | |
if pdf_generator_service: | |
try: | |
farmer_data = { | |
'name': farmer_obj.name, | |
'contact_number': getattr(farmer_obj, 'contact_number', 'N/A'), | |
'address': getattr(farmer_obj, 'address', 'N/A') | |
} | |
pdf_path = pdf_generator_service.generate_yearly_plan_pdf(farmer_data, comprehensive_plan) | |
if pdf_path: | |
logger.info(f"Generated PDF report: {pdf_path}") | |
except Exception as e: | |
logger.error(f"Failed to generate PDF: {str(e)}") | |
# Send via Telegram if possible | |
if getattr(farmer_obj, 'telegram_chat_id', None) and telegram_service: | |
try: | |
msg = format_yearly_plan_summary(farmer_obj.name, comprehensive_plan['summary']) | |
telegram_service.send_message(farmer_obj.telegram_chat_id, msg) | |
# Send PDF if generated successfully | |
if pdf_path and os.path.exists(pdf_path): | |
try: | |
# Note: This would require telegram_service to have a send_document method | |
# For now, just notify about PDF availability | |
pdf_msg = f"📄 Your detailed yearly plan PDF has been generated and is available for download from your dashboard." | |
telegram_service.send_message(farmer_obj.telegram_chat_id, pdf_msg) | |
logger.info(f"Notified farmer {farmer_obj.name} about PDF availability") | |
except Exception as e: | |
logger.error(f"Failed to send PDF notification: {str(e)}") | |
logger.info(f"Sent yearly plan summary to farmer {farmer_obj.name} via Telegram") | |
except Exception as e: | |
logger.exception(f'Failed to send yearly plan via Telegram: {str(e)}') | |
return True | |
except Exception as e: | |
logger.exception('Failed to generate/send yearly plan') | |
return False | |
# Authentication decorator | |
def admin_required(f): | |
def decorated_function(*args, **kwargs): | |
if 'admin_id' not in session: | |
return redirect(url_for('admin_login')) | |
return f(*args, **kwargs) | |
return decorated_function | |
# =================== AUTHENTICATION ROUTES =================== | |
def farmer_login(): | |
"""Farmer login page""" | |
if request.method == 'POST': | |
aadhaar_id = request.form.get('aadhaar_id') | |
password = request.form.get('password') | |
farmer = Farmer.query.filter_by(aadhaar_id=aadhaar_id).first() | |
if farmer and farmer.check_password(password): | |
user = FarmerUser(farmer) | |
login_user(user) | |
flash('Login successful!', 'success') | |
return redirect(url_for('farmer_dashboard')) | |
else: | |
flash('Invalid Aadhaar ID or password', 'error') | |
return render_template('farmer_login.html') | |
def farmer_register(): | |
"""Farmer registration page""" | |
if request.method == 'POST': | |
# Get farmer details | |
name = request.form.get('name') | |
age = request.form.get('age') | |
gender = request.form.get('gender') | |
aadhaar_id = request.form.get('aadhaar_id') | |
contact_number = request.form.get('contact_number') | |
address = request.form.get('address') | |
password = request.form.get('password') | |
# Validate required fields | |
if not all([name, aadhaar_id, contact_number, address, password]): | |
flash('All required fields must be filled', 'error') | |
return render_template('farmer_register.html') | |
# Check if farmer already exists | |
existing_farmer = Farmer.query.filter_by(aadhaar_id=aadhaar_id).first() | |
if existing_farmer: | |
flash('Farmer with this Aadhaar ID already exists', 'error') | |
return render_template('farmer_register.html') | |
# Create new farmer | |
farmer = Farmer( | |
name=name, | |
age=int(age) if age else None, | |
gender=gender, | |
aadhaar_id=aadhaar_id, | |
contact_number=contact_number, | |
address=address | |
) | |
farmer.set_password(password) | |
db.session.add(farmer) | |
db.session.commit() | |
# Generate a yearly plan for the new farmer and attempt to send | |
try: | |
from models import YearlyPlan | |
from telegram_service import format_yearly_plan_summary | |
def build_simple_yearly_plan(farmer_obj): | |
year = date.today().year | |
farms = [] | |
urgent = [] | |
for farm in farmer_obj.farms: | |
crop_details = farm.get_crop_details() | |
top_tasks = [] | |
for c in crop_details: | |
sow = c.get('sowing_month') or '' | |
name = c.get('name') | |
if sow: | |
top_tasks.append(f"{name}: Sow in {sow}") | |
urgent.append(f"{farm.farm_name}: Prepare for sowing {name} in {sow}") | |
else: | |
top_tasks.append(f"{name}: Check sowing schedule") | |
farms.append({'farm_id': farm.id, 'farm_name': farm.farm_name, 'top_tasks': top_tasks}) | |
plan = { | |
'year': year, | |
'farms': farms, | |
'urgent': urgent | |
} | |
summary_text = '\n'.join([f"{f['farm_name']}: {', '.join(f['top_tasks'][:2])}" for f in farms]) | |
return year, plan, summary_text | |
year, plan_json, summary_text = build_simple_yearly_plan(farmer) | |
yp = YearlyPlan(farmer_id=farmer.id, year=year, plan_json=json.dumps(plan_json), summary_text=summary_text) | |
db.session.add(yp) | |
db.session.commit() | |
# Attempt delivery via Telegram | |
if farmer.telegram_chat_id and telegram_service: | |
try: | |
msg = format_yearly_plan_summary(farmer.name, plan_json) | |
telegram_service.send_message(farmer.telegram_chat_id, msg) | |
except Exception: | |
pass | |
except Exception: | |
# Don't block registration if plan generation fails | |
pass | |
flash('Registration successful! Please login.', 'success') | |
return redirect(url_for('farmer_login')) | |
return render_template('farmer_register.html') | |
def farmer_logout(): | |
"""Farmer logout""" | |
logout_user() | |
flash('Logged out successfully', 'info') | |
return redirect(url_for('farmer_login')) | |
def admin_login(): | |
"""Admin login page""" | |
if request.method == 'POST': | |
username = request.form.get('username') | |
password = request.form.get('password') | |
admin = AdminUser.query.filter_by(username=username).first() | |
if admin and admin.check_password(password): | |
session['admin_id'] = admin.id | |
admin.last_login = datetime.utcnow() | |
db.session.commit() | |
flash('Admin login successful!', 'success') | |
return redirect(url_for('admin_dashboard')) | |
else: | |
flash('Invalid credentials', 'error') | |
return render_template('admin_login.html') | |
def admin_logout(): | |
"""Admin logout""" | |
session.pop('admin_id', None) | |
flash('Admin logged out successfully', 'info') | |
return redirect(url_for('admin_login')) | |
# =================== MAIN ROUTES =================== | |
def index(): | |
"""Home page""" | |
return render_template('index.html') | |
def farmer_dashboard(): | |
"""Farmer dashboard""" | |
farmer = get_current_farmer() | |
farms = Farm.query.filter_by(farmer_id=farmer.id).all() | |
# Get today's advisory | |
today_advisory = None | |
if farms: | |
today_advisory = DailyAdvisory.query.filter_by( | |
farm_id=farms[0].id, | |
date=date.today() | |
).first() | |
# Get recent activities | |
recent_activities = FarmingActivity.query.filter_by( | |
farmer_id=farmer.id | |
).order_by(FarmingActivity.scheduled_date.desc()).limit(5).all() | |
# Get latest yearly plan if any | |
try: | |
latest_plan = YearlyPlan.query.filter_by(farmer_id=farmer.id).order_by(YearlyPlan.created_at.desc()).first() | |
except Exception: | |
latest_plan = None | |
return render_template('farmer_dashboard.html', | |
farmer=farmer, | |
farms=farms, | |
today_advisory=today_advisory, | |
recent_activities=recent_activities, | |
yearly_plan=latest_plan) | |
def farms(): | |
"""List all farms for the current farmer""" | |
farmer = get_current_farmer() | |
farms = Farm.query.filter_by(farmer_id=farmer.id).all() | |
return render_template('farms.html', farms=farms) | |
def edit_farm(farm_id): | |
"""Edit farm details""" | |
farm = Farm.query.get_or_404(farm_id) | |
# Check if farmer owns this farm | |
if farm.farmer_id != get_current_farmer().id: | |
flash('Access denied', 'error') | |
return redirect(url_for('farmer_dashboard')) | |
if request.method == 'POST': | |
farm.farm_name = request.form.get('farm_name') | |
farm.farm_size = float(request.form.get('farm_size') or 0) | |
farm.farm_type = request.form.get('farm_type', 'crop') | |
farm.irrigation_type = request.form.get('irrigation_type') | |
# Update location if provided | |
lat = request.form.get('latitude') | |
lng = request.form.get('longitude') | |
if lat and lng: | |
farm.latitude = float(lat) | |
farm.longitude = float(lng) | |
db.session.commit() | |
flash('Farm updated successfully', 'success') | |
return redirect(url_for('farm_details', farm_id=farm.id)) | |
return render_template('edit_farm.html', farm=farm) | |
def delete_farm(farm_id): | |
"""Delete a farm""" | |
farm = Farm.query.get_or_404(farm_id) | |
# Check if farmer owns this farm | |
if farm.farmer_id != get_current_farmer().id: | |
flash('Access denied', 'error') | |
return redirect(url_for('farmer_dashboard')) | |
# Delete associated records (soil data, activities, advisories) | |
SoilData.query.filter_by(farm_id=farm_id).delete() | |
FarmingActivity.query.filter_by(farm_id=farm_id).delete() | |
DailyAdvisory.query.filter_by(farm_id=farm_id).delete() | |
# Delete the farm | |
db.session.delete(farm) | |
db.session.commit() | |
flash('Farm deleted successfully', 'success') | |
return redirect(url_for('farmer_dashboard')) | |
def add_farm(): | |
"""Add new farm""" | |
if request.method == 'POST': | |
farmer = get_current_farmer() | |
# Get farm details | |
farm_name = request.form.get('farm_name') | |
farm_size = request.form.get('farm_size') # may be empty if drawing used | |
irrigation_type = request.form.get('irrigation_type') | |
latitude = request.form.get('latitude') | |
longitude = request.form.get('longitude') | |
field_coordinates = request.form.get('field_coordinates') | |
farm_type = request.form.get('farm_type') | |
# Get crop types (for crop and mixed farms) | |
crop_types = request.form.getlist('crop_types') | |
# Get livestock details (for livestock and mixed farms) | |
livestock_types = request.form.getlist('livestock_types') | |
livestock_count = request.form.get('livestock_count') | |
housing_type = request.form.get('housing_type') | |
feeding_system = request.form.get('feeding_system') | |
breed_info = request.form.get('breed_info') | |
# Validate required fields (farm_size may be computed from field_coordinates) | |
if not all([farm_name, irrigation_type, latitude, longitude, farm_type]): | |
flash('All required fields must be filled', 'error') | |
return render_template('add_farm.html') | |
# If field_coordinates were provided, attempt to compute farm_size (overrides input) | |
computed_acres = None | |
if field_coordinates: | |
try: | |
coords = json.loads(field_coordinates) | |
# Expect coords as list of {lat, lng} or [lng, lat] | |
computed_acres = calculate_area_acres(coords) | |
except Exception as e: | |
logger.warning(f"Failed to compute area from coordinates: {e}") | |
# Final farm_size: prefer computed acres if available, else fallback to provided input | |
if computed_acres and computed_acres > 0: | |
final_farm_size = float(computed_acres) | |
else: | |
if not farm_size: | |
flash('Please provide farm size or draw the farm boundary to compute it automatically.', 'error') | |
return render_template('add_farm.html') | |
final_farm_size = float(farm_size) | |
# Parse detailed crop inputs (arrays) | |
crop_names = request.form.getlist('crop_name[]') | |
crop_areas = request.form.getlist('crop_area[]') | |
crop_months = request.form.getlist('crop_month[]') | |
crop_details = [] | |
for i in range(len(crop_names)): | |
try: | |
name = crop_names[i].strip() | |
area = float(crop_areas[i]) if crop_areas and i < len(crop_areas) and crop_areas[i] else 0.0 | |
month = crop_months[i] if crop_months and i < len(crop_months) else '' | |
if name: | |
crop_details.append({'name': name, 'area': area, 'sowing_month': month}) | |
except Exception: | |
continue | |
# Create new farm | |
farm = Farm( | |
farmer_id=farmer.id, | |
farm_name=farm_name, | |
farm_size=final_farm_size, | |
irrigation_type=irrigation_type, | |
latitude=float(latitude), | |
longitude=float(longitude), | |
field_coordinates=field_coordinates, | |
farm_type=farm_type, | |
livestock_types=','.join(livestock_types) if livestock_types else None, | |
livestock_count=int(livestock_count) if livestock_count else None, | |
housing_type=housing_type, | |
feeding_system=feeding_system, | |
breed_info=breed_info | |
) | |
# Save crop types and details (for crop and mixed farms) | |
if farm_type in ['crop', 'mixed'] and crop_types: | |
farm.set_crop_types([c for c in crop_types]) | |
farm.set_crop_details(crop_details) | |
db.session.add(farm) | |
db.session.commit() | |
# Add soil data if provided | |
soil_type = request.form.get('soil_type') | |
if soil_type: | |
soil_data = SoilData( | |
farm_id=farm.id, | |
soil_type=soil_type, | |
ph_level=float(request.form.get('ph_level')) if request.form.get('ph_level') else None, | |
nitrogen_level=float(request.form.get('nitrogen_level')) if request.form.get('nitrogen_level') else None, | |
phosphorus_level=float(request.form.get('phosphorus_level')) if request.form.get('phosphorus_level') else None, | |
potassium_level=float(request.form.get('potassium_level')) if request.form.get('potassium_level') else None, | |
moisture_percentage=float(request.form.get('moisture_percentage')) if request.form.get('moisture_percentage') else None | |
) | |
db.session.add(soil_data) | |
db.session.commit() | |
flash('Farm added successfully!', 'success') | |
return redirect(url_for('farmer_dashboard')) | |
return render_template('add_farm.html') | |
def farm_details(farm_id): | |
"""Farm details page""" | |
farm = Farm.query.get_or_404(farm_id) | |
# Check if farmer owns this farm | |
if farm.farmer_id != get_current_farmer().id: | |
flash('Access denied', 'error') | |
return redirect(url_for('farmer_dashboard')) | |
# Get soil data | |
soil_data = SoilData.query.filter_by(farm_id=farm_id).first() | |
# Get recent activities | |
activities = FarmingActivity.query.filter_by(farm_id=farm_id).order_by( | |
FarmingActivity.scheduled_date.desc() | |
).limit(10).all() | |
# Get recent advisories | |
advisories = DailyAdvisory.query.filter_by(farm_id=farm_id).order_by( | |
DailyAdvisory.date.desc() | |
).limit(7).all() | |
# Get current weather | |
current_weather = None | |
if weather_service: | |
current_weather = weather_service.get_current_weather(farm.latitude, farm.longitude) | |
crops_data = farm.get_crop_details() | |
return render_template('farm_details.html', | |
farm=farm, | |
soil_data=soil_data, | |
activities=activities, | |
advisories=advisories, | |
weather=current_weather, | |
crops_data=crops_data) | |
def farmer_generate_yearly_plan(farm_id): | |
"""Generate a comprehensive AI-powered yearly plan for the selected farm.""" | |
farm = Farm.query.get_or_404(farm_id) | |
farmer = get_current_farmer() | |
if not farmer or farm.farmer_id != farmer.id: | |
return jsonify({'error': 'Access denied'}), 403 | |
try: | |
year = date.today().year | |
# Prepare farmer data | |
farmer_data = { | |
'name': farmer.name, | |
'contact_number': getattr(farmer, 'contact_number', 'Unknown'), | |
'address': getattr(farmer, 'address', 'Unknown'), | |
'id': farmer.id | |
} | |
# Get farm data | |
farm_data = { | |
'farm_name': farm.farm_name, | |
'farm_size': farm.farm_size, | |
'crop_types': farm.get_crop_types(), | |
'irrigation_type': getattr(farm, 'irrigation_type', 'Unknown'), | |
'coordinates': {'lat': farm.latitude, 'lon': farm.longitude} if farm.latitude and farm.longitude else None | |
} | |
# Get latest soil data | |
soil_data = {} | |
latest_soil = SoilData.query.filter_by(farm_id=farm.id).order_by(SoilData.id.desc()).first() | |
if latest_soil: | |
soil_data = { | |
'soil_type': latest_soil.soil_type, | |
'ph_level': latest_soil.ph_level, | |
'nitrogen_level': latest_soil.nitrogen_level, | |
'phosphorus_level': getattr(latest_soil, 'phosphorus_level', None), | |
'potassium_level': getattr(latest_soil, 'potassium_level', None), | |
'moisture_percentage': getattr(latest_soil, 'moisture_percentage', None) | |
} | |
else: | |
soil_data = { | |
'soil_type': 'Unknown', | |
'ph_level': 'Unknown', | |
'nitrogen_level': 'Unknown', | |
'phosphorus_level': 'Unknown', | |
'potassium_level': 'Unknown', | |
'moisture_percentage': 'Unknown' | |
} | |
# Generate AI-powered yearly plan | |
farm_plan = None | |
if gemini_service: | |
try: | |
farm_plan = gemini_service.generate_year_plan(farmer_data, farm_data, soil_data) | |
logger.info(f"Generated comprehensive AI yearly plan for farm {farm.farm_name}") | |
except Exception as e: | |
logger.error(f"Failed to generate AI plan for farm {farm.farm_name}: {str(e)}") | |
# Create fallback plan if AI fails | |
if not farm_plan or 'plan' not in farm_plan: | |
crop_details = farm.get_crop_details() | |
fallback_html = f""" | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<style> | |
body {{ font-family: Arial, sans-serif; margin: 20px; background-color: #f5f5f5; }} | |
.header {{ text-align: center; color: #2e7d32; font-size: 28px; font-weight: bold; margin-bottom: 30px; }} | |
.section {{ margin-bottom: 25px; }} | |
.section-title {{ color: #1976d2; font-size: 20px; font-weight: bold; margin-bottom: 15px; }} | |
table {{ width: 100%; border-collapse: collapse; background-color: #e8f5e8; margin-bottom: 20px; }} | |
th {{ background-color: #4caf50; color: white; padding: 12px; text-align: left; }} | |
td {{ padding: 10px; border: 1px solid #ddd; }} | |
.highlight {{ background-color: #ffeb3b; font-weight: bold; }} | |
</style> | |
</head> | |
<body> | |
<div class="header">वार्षिक खेती योजना - {farm.farm_name}</div> | |
<div class="section"> | |
<div class="section-title">फसल विवरण</div> | |
<table> | |
<tr><th>फसल</th><th>बुआई का समय</th><th>कटाई का समय</th></tr> | |
""" | |
for crop in crop_details: | |
crop_name = crop.get('name', 'अज्ञात') | |
sowing_month = crop.get('sowing_month', 'अज्ञात') | |
fallback_html += f"<tr><td>{crop_name}</td><td>{sowing_month}</td><td>मौसम अनुसार</td></tr>" | |
fallback_html += """ | |
</table> | |
</div> | |
<div class="section"> | |
<div class="section-title">सामान्य सुझाव</div> | |
<p>• नियमित मिट्टी की जांच कराएं</p> | |
<p>• <span class="highlight">समय पर</span> सिंचाई करें</p> | |
<p>• उन्नत बीजों का प्रयोग करें</p> | |
</div> | |
</body> | |
</html> | |
""" | |
farm_plan = { | |
'plan': fallback_html, | |
'type': 'fallback_html', | |
'generated_at': datetime.now().isoformat(), | |
'ai_generated': False | |
} | |
# Create comprehensive plan structure | |
comprehensive_plan = { | |
'year': year, | |
'farmer_id': farmer.id, | |
'farms': [{ | |
'farm_id': farm.id, | |
'farm_name': farm.farm_name, | |
'plan': farm_plan['plan'], | |
'plan_type': farm_plan.get('type', 'html'), | |
'generated_at': farm_plan['generated_at'], | |
'ai_generated': farm_plan.get('ai_generated', False) | |
}], | |
'summary': f"AI-Generated Comprehensive Yearly Plan for {farm.farm_name} ({farm.farm_size} acres)", | |
'generated_at': datetime.now().isoformat() | |
} | |
# Update or create YearlyPlan | |
existing = YearlyPlan.query.filter_by(farmer_id=farmer.id, year=year).first() | |
if existing: | |
try: | |
existing_data = json.loads(existing.plan_json) | |
# Update this specific farm's plan | |
farm_found = False | |
for idx, f in enumerate(existing_data.get('farms', [])): | |
if f.get('farm_id') == farm.id: | |
existing_data['farms'][idx] = comprehensive_plan['farms'][0] | |
farm_found = True | |
break | |
if not farm_found: | |
existing_data.setdefault('farms', []).append(comprehensive_plan['farms'][0]) | |
existing_data['summary'] = f"Updated plan for {len(existing_data['farms'])} farms" | |
existing.plan_json = json.dumps(existing_data) | |
existing.summary_text = existing_data['summary'] | |
except Exception: | |
existing.plan_json = json.dumps(comprehensive_plan) | |
existing.summary_text = comprehensive_plan['summary'] | |
db.session.add(existing) | |
else: | |
yp = YearlyPlan( | |
farmer_id=farmer.id, | |
year=year, | |
plan_json=json.dumps(comprehensive_plan), | |
summary_text=comprehensive_plan['summary'] | |
) | |
db.session.add(yp) | |
db.session.commit() | |
logger.info(f"Successfully saved yearly plan for farm {farm.farm_name}") | |
# Send via Telegram if possible | |
if getattr(farmer, 'telegram_chat_id', None) and telegram_service: | |
try: | |
crop_list = ', '.join([crop.get('name', 'Unknown') for crop in farm.get_crop_details()]) | |
summary = f"🌱 New Yearly Plan Generated!\n\nFarm: {farm.farm_name}\nCrops: {crop_list}\nGenerated with AI analysis of soil, weather, and crop data." | |
# Send the plan summary first | |
msg = format_yearly_plan_summary(farmer.name, summary) | |
telegram_service.send_message(farmer.telegram_chat_id, msg) | |
# Send the complete detailed plan content with actual extracted content | |
try: | |
detailed_messages = format_enhanced_yearly_plan_telegram(farmer.name, farm.farm_name, comprehensive_plan) | |
for i, detailed_msg in enumerate(detailed_messages): | |
# Add small delay between messages to avoid rate limiting | |
import time | |
if i > 0: | |
time.sleep(1) | |
telegram_service.send_message(farmer.telegram_chat_id, detailed_msg) | |
logger.info(f"Sent complete yearly plan details via Telegram to farmer {farmer.name}") | |
except Exception as e: | |
logger.error(f"Failed to send detailed plan content: {str(e)}") | |
# Fallback to basic detailed messages | |
try: | |
detailed_messages = format_complete_yearly_plan_telegram(farmer.name, farm.farm_name, comprehensive_plan) | |
for i, detailed_msg in enumerate(detailed_messages): | |
if i > 0: | |
time.sleep(1) | |
telegram_service.send_message(farmer.telegram_chat_id, detailed_msg) | |
except Exception as e2: | |
logger.error(f"Fallback detailed plan also failed: {str(e2)}") | |
# Generate and send the actual PDF file | |
try: | |
farmer_data = { | |
'name': farmer.name, | |
'contact_number': farmer.contact_number, | |
'aadhaar_id': farmer.aadhaar_id, | |
'farms': [farm.as_dict()] | |
} | |
pdf_path = pdf_generator_service.generate_yearly_plan_pdf(farmer_data, comprehensive_plan) | |
if pdf_path and os.path.exists(pdf_path): | |
# Send the actual PDF file via Telegram | |
pdf_caption = f"📄 <b>New Yearly Farming Plan - {farm.farm_name}</b>\n\nGenerated on: {datetime.now().strftime('%d/%m/%Y')}\nCrops: {crop_list}\n\nAI-powered plan with monthly activities and recommendations." | |
document_result = telegram_service.send_document( | |
farmer.telegram_chat_id, | |
pdf_path, | |
caption=pdf_caption | |
) | |
if document_result.get('status') == 'sent': | |
logger.info(f"Generated and sent PDF file via Telegram to farmer {farmer.name}") | |
else: | |
# Fallback to notification if file sending fails | |
pdf_msg = f"📄 Your detailed yearly plan PDF has been generated and is available for download from your dashboard." | |
telegram_service.send_message(farmer.telegram_chat_id, pdf_msg) | |
logger.info(f"Generated PDF and sent notification to farmer {farmer.name}") | |
else: | |
# PDF generation failed | |
pdf_msg = f"📄 Your yearly plan has been generated. PDF will be available on your dashboard soon." | |
telegram_service.send_message(farmer.telegram_chat_id, pdf_msg) | |
logger.info(f"Sent plan notification to farmer {farmer.name} (PDF generation failed)") | |
except Exception as e: | |
logger.error(f"Failed to generate/send PDF: {str(e)}") | |
# Send fallback notification | |
pdf_msg = f"📄 Your yearly plan has been generated and is available on your dashboard." | |
telegram_service.send_message(farmer.telegram_chat_id, pdf_msg) | |
logger.info(f"Sent yearly plan notification to farmer {farmer.name} via Telegram") | |
except Exception as e: | |
logger.exception(f'Failed to send yearly plan via Telegram: {str(e)}') | |
return jsonify({ | |
'success': True, | |
'message': 'Comprehensive yearly plan generated successfully using AI analysis', | |
'plan': comprehensive_plan['farms'][0], | |
'ai_generated': True | |
}) | |
except Exception as e: | |
logger.exception(f"Error generating yearly plan for farm {farm_id}: {e}") | |
return jsonify({'success': False, 'error': f'Failed to generate yearly plan: {str(e)}'}), 500 | |
def farmer_get_yearly_plan(farm_id): | |
"""Return the farmer's yearly plan containing the given farm (for current year).""" | |
farm = Farm.query.get_or_404(farm_id) | |
farmer = get_current_farmer() | |
if not farmer or farm.farmer_id != farmer.id: | |
return jsonify({'error': 'Access denied'}), 403 | |
year = date.today().year | |
plan = YearlyPlan.query.filter_by(farmer_id=farmer.id, year=year).first() | |
if not plan: | |
return jsonify({'error': 'No yearly plan found'}), 404 | |
try: | |
data = json.loads(plan.plan_json) | |
except Exception: | |
data = {'year': plan.year, 'farms': []} | |
# find farm entry | |
for f in data.get('farms', []): | |
if f.get('farm_id') == farm.id: | |
# Check if it's HTML content | |
plan_content = f.get('plan', '') | |
if f.get('plan_type') == 'comprehensive_html' or f.get('plan_type') == 'fallback_html' or '<!DOCTYPE html>' in plan_content: | |
return jsonify({ | |
'success': True, | |
'plan': f, | |
'raw_plan': data, | |
'html_content': plan_content, | |
'is_html': True, | |
'ai_generated': f.get('ai_generated', False) | |
}) | |
else: | |
return jsonify({ | |
'success': True, | |
'plan': f, | |
'raw_plan': data, | |
'is_html': False | |
}) | |
return jsonify({'error': 'No yearly plan entry for this farm'}), 404 | |
def farmer_view_yearly_plan_html(farm_id): | |
"""Display the full HTML yearly plan for the selected farm.""" | |
farm = Farm.query.get_or_404(farm_id) | |
farmer = get_current_farmer() | |
if not farmer or farm.farmer_id != farmer.id: | |
flash('Access denied', 'error') | |
return redirect(url_for('farmer_dashboard')) | |
year = date.today().year | |
plan = YearlyPlan.query.filter_by(farmer_id=farmer.id, year=year).first() | |
if not plan: | |
flash('No yearly plan found for this farm', 'error') | |
return redirect(url_for('farmer_dashboard')) | |
try: | |
data = json.loads(plan.plan_json) | |
except Exception: | |
flash('Error loading yearly plan', 'error') | |
return redirect(url_for('farmer_dashboard')) | |
# Find farm entry | |
for f in data.get('farms', []): | |
if f.get('farm_id') == farm.id: | |
plan_content = f.get('plan', '') | |
if f.get('plan_type') == 'comprehensive_html' or f.get('plan_type') == 'fallback_html' or '<!DOCTYPE html>' in plan_content: | |
# Return the HTML content directly | |
from markupsafe import Markup | |
return Markup(plan_content) | |
else: | |
flash('Plan is not in HTML format', 'error') | |
return redirect(url_for('farmer_dashboard')) | |
flash('No yearly plan entry found for this farm', 'error') | |
return redirect(url_for('farmer_dashboard')) | |
def farmer_edit_yearly_plan(farm_id): | |
"""Edit the yearly plan entry for the selected farm. Accepts JSON { plan_json, summary_text } or partial updates.""" | |
farm = Farm.query.get_or_404(farm_id) | |
farmer = get_current_farmer() | |
if not farmer or farm.farmer_id != farmer.id: | |
return jsonify({'error': 'Access denied'}), 403 | |
payload = request.get_json(force=True, silent=True) or {} | |
year = date.today().year | |
plan_rec = YearlyPlan.query.filter_by(farmer_id=farmer.id, year=year).first() | |
if not plan_rec: | |
return jsonify({'error': 'Yearly plan not found'}), 404 | |
try: | |
p = json.loads(plan_rec.plan_json) | |
except Exception: | |
p = {'year': year, 'farms': []} | |
# update farm entry if present, else append | |
updated = False | |
new_entry = payload.get('plan_entry') | |
if new_entry: | |
for idx, f in enumerate(p.get('farms', [])): | |
if f.get('farm_id') == farm.id: | |
p['farms'][idx] = new_entry | |
updated = True | |
break | |
if not updated: | |
p.setdefault('farms', []).append(new_entry) | |
# optional summary_text update | |
if 'summary_text' in payload: | |
plan_rec.summary_text = payload.get('summary_text') | |
plan_rec.plan_json = json.dumps(p) | |
db.session.add(plan_rec) | |
db.session.commit() | |
return jsonify({'success': True, 'plan': new_entry or {}}) | |
def farmer_delete_yearly_plan(farm_id): | |
"""Remove the selected farm from the yearly plan for the current year (or delete the plan if empty).""" | |
farm = Farm.query.get_or_404(farm_id) | |
farmer = get_current_farmer() | |
if not farmer or farm.farmer_id != farmer.id: | |
return jsonify({'error': 'Access denied'}), 403 | |
year = date.today().year | |
plan_rec = YearlyPlan.query.filter_by(farmer_id=farmer.id, year=year).first() | |
if not plan_rec: | |
return jsonify({'error': 'Yearly plan not found'}), 404 | |
try: | |
p = json.loads(plan_rec.plan_json) | |
except Exception: | |
p = {'year': year, 'farms': []} | |
p['farms'] = [f for f in p.get('farms', []) if f.get('farm_id') != farm.id] | |
if not p['farms']: | |
# delete plan record | |
db.session.delete(plan_rec) | |
else: | |
plan_rec.plan_json = json.dumps(p) | |
plan_rec.summary_text = '\n'.join([f"{f['farm_name']}: {', '.join(f.get('top_tasks', [])[:2])}" for f in p.get('farms', [])]) | |
db.session.add(plan_rec) | |
db.session.commit() | |
return jsonify({'success': True}) | |
def farmer_download_yearly_plan_pdf(farm_id): | |
"""Download yearly plan PDF for a specific farm""" | |
farm = Farm.query.get_or_404(farm_id) | |
farmer = get_current_farmer() | |
if farm.farmer_id != farmer.id: | |
return jsonify({'error': 'Access denied'}), 403 | |
# Get the current yearly plan | |
year = date.today().year | |
plan = YearlyPlan.query.filter_by(farmer_id=farmer.id, year=year).first() | |
if not plan: | |
return jsonify({'error': 'No yearly plan found. Please generate a yearly plan first.'}), 404 | |
try: | |
if pdf_generator_service: | |
# Parse the plan data | |
plan_data = json.loads(plan.plan_json) if plan.plan_json else {} | |
# Check if the plan contains this specific farm | |
farm_plans = plan_data.get('farms', []) | |
farm_found = any(f.get('farm_id') == farm.id for f in farm_plans) | |
if not farm_found: | |
return jsonify({'error': 'This farm is not included in the yearly plan.'}), 404 | |
# Generate PDF for this specific farm or the whole plan | |
html_content = plan_data.get('html_content', plan.summary_text or 'Plan not available') | |
# Prepare farmer data | |
farmer_data = { | |
'name': farmer.name, | |
'contact_number': farmer.contact_number, | |
'aadhaar_id': farmer.aadhaar_id, | |
'farms': [farm.as_dict()] | |
} | |
pdf_path = pdf_generator_service.generate_yearly_plan_pdf(farmer_data, plan_data) | |
if pdf_path and os.path.exists(pdf_path): | |
return send_file(pdf_path, as_attachment=True, | |
download_name=f"yearly_plan_farm_{farm.farm_name}_{year}.pdf") | |
else: | |
return jsonify({'error': 'Failed to generate PDF file'}), 500 | |
else: | |
return jsonify({'error': 'PDF generation service not available'}), 500 | |
except Exception as e: | |
logger.exception(f"Error generating PDF for farm {farm_id}") | |
return jsonify({'error': f'Failed to generate PDF: {str(e)}'}), 500 | |
def farmer_send_yearly_plan_telegram(farm_id): | |
"""Send yearly plan via Telegram for a specific farm""" | |
farm = Farm.query.get_or_404(farm_id) | |
farmer = get_current_farmer() | |
if farm.farmer_id != farmer.id: | |
return jsonify({'error': 'Access denied'}), 403 | |
if not farmer.telegram_chat_id: | |
return jsonify({'error': 'Telegram chat ID not configured. Please register for Telegram notifications first.'}), 400 | |
if not telegram_service: | |
return jsonify({'error': 'Telegram service not available'}), 500 | |
# Get the current yearly plan | |
year = date.today().year | |
plan = YearlyPlan.query.filter_by(farmer_id=farmer.id, year=year).first() | |
if not plan: | |
return jsonify({'error': 'No yearly plan found. Please generate a yearly plan first.'}), 404 | |
try: | |
# Parse the plan data | |
plan_data = json.loads(plan.plan_json) if plan.plan_json else {} | |
# Find this farm in the plan | |
farm_plan = None | |
for f in plan_data.get('farms', []): | |
if f.get('farm_id') == farm.id: | |
farm_plan = f | |
break | |
if not farm_plan: | |
return jsonify({'error': 'This farm is not included in the yearly plan.'}), 404 | |
# Create a summary message | |
crop_list = ', '.join([crop.get('name', 'Unknown') for crop in farm.get_crop_details()]) | |
summary = f"🌱 Your Yearly Plan for {farm.farm_name}\n\nCrops: {crop_list}\n\nPlan includes month-by-month activities, irrigation schedules, and harvest planning.\n\nGenerated with AI analysis of soil, weather, and crop data." | |
# Send the plan summary | |
msg = format_yearly_plan_summary(farmer.name, summary) | |
telegram_service.send_message(farmer.telegram_chat_id, msg) | |
# Send the complete detailed plan content with actual extracted content | |
try: | |
detailed_messages = format_enhanced_yearly_plan_telegram(farmer.name, farm.farm_name, plan_data) | |
for i, detailed_msg in enumerate(detailed_messages): | |
# Add small delay between messages to avoid rate limiting | |
import time | |
if i > 0: | |
time.sleep(1) | |
telegram_service.send_message(farmer.telegram_chat_id, detailed_msg) | |
logger.info(f"Sent complete yearly plan details via Telegram to farmer {farmer.name}") | |
except Exception as e: | |
logger.error(f"Failed to send detailed plan content: {str(e)}") | |
# Fallback to basic detailed messages | |
try: | |
detailed_messages = format_complete_yearly_plan_telegram(farmer.name, farm.farm_name, plan_data) | |
for i, detailed_msg in enumerate(detailed_messages): | |
if i > 0: | |
time.sleep(1) | |
telegram_service.send_message(farmer.telegram_chat_id, detailed_msg) | |
except Exception as e2: | |
logger.error(f"Fallback detailed plan also failed: {str(e2)}") | |
# Generate and send the actual PDF file | |
try: | |
if pdf_generator_service: | |
# Prepare farmer data for PDF generation | |
farmer_data = { | |
'name': farmer.name, | |
'contact_number': farmer.contact_number, | |
'aadhaar_id': farmer.aadhaar_id, | |
'farms': [farm.as_dict()] | |
} | |
# Generate PDF | |
pdf_path = pdf_generator_service.generate_yearly_plan_pdf(farmer_data, plan_data) | |
if pdf_path and os.path.exists(pdf_path): | |
# Send the actual PDF file via Telegram | |
pdf_caption = f"📄 <b>Yearly Farming Plan - {farm.farm_name}</b>\n\nGenerated on: {datetime.now().strftime('%d/%m/%Y')}\nCrops: {crop_list}" | |
document_result = telegram_service.send_document( | |
farmer.telegram_chat_id, | |
pdf_path, | |
caption=pdf_caption | |
) | |
if document_result.get('status') == 'sent': | |
logger.info(f"PDF file sent successfully via Telegram to farmer {farmer.name}") | |
return jsonify({'success': True, 'message': 'Yearly plan and PDF sent via Telegram successfully!'}) | |
else: | |
# Fallback to notification if file sending fails | |
pdf_msg = f"📄 Your detailed yearly plan PDF has been generated. Download it from your dashboard." | |
telegram_service.send_message(farmer.telegram_chat_id, pdf_msg) | |
return jsonify({'success': True, 'message': 'Yearly plan sent via Telegram. PDF notification sent (file sending failed).'}) | |
else: | |
# PDF generation failed, send notification only | |
pdf_msg = f"📄 Your yearly plan summary has been sent. PDF generation failed - please download from dashboard." | |
telegram_service.send_message(farmer.telegram_chat_id, pdf_msg) | |
return jsonify({'success': True, 'message': 'Yearly plan summary sent via Telegram (PDF generation failed).'}) | |
else: | |
# No PDF service available | |
pdf_msg = f"📄 Your yearly plan summary has been sent. PDF service not available." | |
telegram_service.send_message(farmer.telegram_chat_id, pdf_msg) | |
return jsonify({'success': True, 'message': 'Yearly plan summary sent via Telegram (PDF service unavailable).'}) | |
except Exception as pdf_error: | |
logger.error(f"Error with PDF generation/sending: {str(pdf_error)}") | |
# Send fallback notification | |
pdf_msg = f"📄 Your yearly plan summary has been sent. PDF is available for download from your dashboard." | |
telegram_service.send_message(farmer.telegram_chat_id, pdf_msg) | |
logger.info(f"Sent yearly plan via Telegram to farmer {farmer.name} for farm {farm.farm_name}") | |
return jsonify({'success': True, 'message': 'Yearly plan sent via Telegram successfully!'}) | |
except Exception as e: | |
logger.exception(f"Error sending yearly plan via Telegram for farm {farm_id}") | |
return jsonify({'error': f'Failed to send via Telegram: {str(e)}'}), 500 | |
# ==================== DAILY TASK ROUTES ==================== | |
def farmer_get_daily_tasks(): | |
"""Get daily tasks for the current farmer""" | |
farmer = get_current_farmer() | |
if not farmer: | |
return jsonify({'error': 'Access denied'}), 403 | |
target_date = request.args.get('date', date.today().isoformat()) | |
try: | |
task_date = datetime.strptime(target_date, '%Y-%m-%d').date() | |
except ValueError: | |
task_date = date.today() | |
# Get existing tasks from database | |
existing_tasks = DailyTask.query.filter_by( | |
farmer_id=farmer.id, | |
task_date=task_date | |
).all() | |
tasks_data = [] | |
for task in existing_tasks: | |
completion = TaskCompletion.query.filter_by(task_id=task.id).first() | |
task_data = { | |
'id': task.id, | |
'task_type': task.task_type, | |
'task_title': task.task_title, | |
'task_description': task.task_description, | |
'priority': task.priority, | |
'estimated_duration': task.estimated_duration, | |
'weather_dependent': task.weather_dependent, | |
'farm_id': task.farm_id, | |
'crop_specific': task.crop_specific, | |
'task_date': task.task_date.isoformat(), | |
'is_completed': completion is not None if completion else False, | |
'completed_at': completion.completed_at.isoformat() if completion and completion.completed_at else None, | |
'rating': completion.rating if completion else None, | |
'feedback': completion.feedback if completion else None | |
} | |
tasks_data.append(task_data) | |
return jsonify({ | |
'success': True, | |
'tasks': tasks_data, | |
'date': task_date.isoformat(), | |
'total_tasks': len(tasks_data) | |
}) | |
def farmer_generate_daily_tasks(): | |
"""Generate daily tasks for the current farmer""" | |
global daily_task_service | |
farmer = get_current_farmer() | |
if not farmer: | |
return jsonify({'error': 'Access denied'}), 403 | |
data = request.get_json() or {} | |
target_date_str = data.get('date', date.today().isoformat()) | |
try: | |
task_date = datetime.strptime(target_date_str, '%Y-%m-%d').date() | |
except ValueError: | |
task_date = date.today() | |
try: | |
if not daily_task_service: | |
return jsonify({'error': 'Daily task service not available'}), 500 | |
# Check if tasks already exist for this date | |
existing_count = DailyTask.query.filter_by( | |
farmer_id=farmer.id, | |
task_date=task_date | |
).count() | |
if existing_count > 0: | |
return jsonify({ | |
'error': f'Tasks already exist for {task_date}. Delete existing tasks first if you want to regenerate.' | |
}), 400 | |
# Generate new tasks | |
generated_tasks = daily_task_service.generate_daily_tasks(farmer, task_date) | |
# Save tasks to database | |
saved_tasks = [] | |
for task_data in generated_tasks: | |
task = DailyTask( | |
farmer_id=farmer.id, | |
task_date=task_date, | |
task_type=task_data.get('task_type', 'general'), | |
task_title=task_data.get('task_title', 'Farm Task'), | |
task_description=task_data.get('task_description', ''), | |
priority=task_data.get('priority', 'medium'), | |
estimated_duration=task_data.get('estimated_duration', 30), | |
weather_dependent=task_data.get('weather_dependent', False), | |
farm_id=task_data.get('farm_id'), | |
crop_specific=task_data.get('crop_specific') | |
) | |
db.session.add(task) | |
db.session.flush() # Get task ID | |
saved_tasks.append(task) | |
db.session.commit() | |
logger.info(f"Generated {len(saved_tasks)} daily tasks for farmer {farmer.name} for {task_date}") | |
return jsonify({ | |
'success': True, | |
'message': f'Generated {len(saved_tasks)} daily tasks for {task_date}', | |
'tasks_count': len(saved_tasks), | |
'date': task_date.isoformat() | |
}) | |
except Exception as e: | |
db.session.rollback() | |
logger.exception(f"Error generating daily tasks for farmer {farmer.id}") | |
return jsonify({'error': f'Failed to generate daily tasks: {str(e)}'}), 500 | |
def farmer_complete_task(task_id): | |
"""Mark a daily task as completed""" | |
farmer = get_current_farmer() | |
if not farmer: | |
return jsonify({'error': 'Access denied'}), 403 | |
task = DailyTask.query.get_or_404(task_id) | |
if task.farmer_id != farmer.id: | |
return jsonify({'error': 'Access denied'}), 403 | |
data = request.get_json() or {} | |
# Check if already completed | |
existing_completion = TaskCompletion.query.filter_by(task_id=task.id).first() | |
if existing_completion: | |
return jsonify({'error': 'Task already completed'}), 400 | |
try: | |
completion = TaskCompletion( | |
task_id=task.id, | |
completed_at=datetime.utcnow(), | |
rating=data.get('rating'), | |
feedback=data.get('feedback', '').strip() | |
) | |
db.session.add(completion) | |
db.session.commit() | |
logger.info(f"Task {task.id} completed by farmer {farmer.name}") | |
return jsonify({ | |
'success': True, | |
'message': 'Task marked as completed', | |
'completed_at': completion.completed_at.isoformat(), | |
'rating': completion.rating | |
}) | |
except Exception as e: | |
db.session.rollback() | |
logger.exception(f"Error completing task {task_id}") | |
return jsonify({'error': f'Failed to complete task: {str(e)}'}), 500 | |
def farmer_uncomplete_task(task_id): | |
"""Mark a daily task as not completed (remove completion)""" | |
farmer = get_current_farmer() | |
if not farmer: | |
return jsonify({'error': 'Access denied'}), 403 | |
task = DailyTask.query.get_or_404(task_id) | |
if task.farmer_id != farmer.id: | |
return jsonify({'error': 'Access denied'}), 403 | |
try: | |
completion = TaskCompletion.query.filter_by(task_id=task.id).first() | |
if completion: | |
db.session.delete(completion) | |
db.session.commit() | |
logger.info(f"Task {task.id} unmarked as completed by farmer {farmer.name}") | |
return jsonify({ | |
'success': True, | |
'message': 'Task completion removed' | |
}) | |
else: | |
return jsonify({'error': 'Task was not completed'}), 400 | |
except Exception as e: | |
db.session.rollback() | |
logger.exception(f"Error uncompleting task {task_id}") | |
return jsonify({'error': f'Failed to uncomplete task: {str(e)}'}), 500 | |
def farmer_send_daily_tasks_telegram(): | |
"""Send daily tasks to farmer via Telegram""" | |
global telegram_service, daily_task_service | |
farmer = get_current_farmer() | |
if not farmer: | |
return jsonify({'error': 'Access denied'}), 403 | |
if not farmer.telegram_chat_id: | |
return jsonify({'error': 'Telegram chat ID not configured. Please register for Telegram notifications first.'}), 400 | |
if not telegram_service: | |
return jsonify({'error': 'Telegram service not available'}), 500 | |
data = request.get_json() or {} | |
target_date_str = data.get('date', date.today().isoformat()) | |
try: | |
task_date = datetime.strptime(target_date_str, '%Y-%m-%d').date() | |
except ValueError: | |
task_date = date.today() | |
try: | |
# Get tasks for the date | |
tasks = DailyTask.query.filter_by( | |
farmer_id=farmer.id, | |
task_date=task_date | |
).order_by(DailyTask.priority.desc(), DailyTask.estimated_duration.asc()).all() | |
if not tasks: | |
return jsonify({'error': f'No tasks found for {task_date}'}), 404 | |
# Create header message | |
header_msg = f"🌅 <b>Daily Farming Tasks - {task_date.strftime('%d %B %Y')}</b>\n\n" | |
header_msg += f"👋 Good morning, {farmer.name}!\n" | |
header_msg += f"📋 You have {len(tasks)} tasks scheduled for today:\n\n" | |
# Send header | |
telegram_service.send_message(farmer.telegram_chat_id, header_msg) | |
# Send each task | |
for i, task in enumerate(tasks, 1): | |
if not daily_task_service: | |
continue | |
task_msg = daily_task_service.format_task_for_telegram( | |
{ | |
'task_type': task.task_type, | |
'task_title': task.task_title, | |
'task_description': task.task_description, | |
'priority': task.priority, | |
'estimated_duration': task.estimated_duration, | |
'weather_dependent': task.weather_dependent, | |
'crop_specific': task.crop_specific | |
}, | |
farmer.name | |
) | |
task_msg = f"<b>Task {i}/{len(tasks)}</b>\n\n" + task_msg | |
# Add completion instruction | |
task_msg += f"\n💡 <i>Mark as completed when done!</i>" | |
telegram_service.send_message(farmer.telegram_chat_id, task_msg) | |
# Small delay to avoid rate limiting | |
import time | |
time.sleep(0.5) | |
# Send footer message | |
footer_msg = f"✅ <b>Instructions:</b>\n" | |
footer_msg += f"• Check each task when completed\n" | |
footer_msg += f"• Rate your experience (1-5 stars)\n" | |
footer_msg += f"• Add feedback if needed\n\n" | |
footer_msg += f"📱 Manage tasks from your farmer dashboard\n" | |
footer_msg += f"🌟 Have a productive farming day!" | |
telegram_service.send_message(farmer.telegram_chat_id, footer_msg) | |
# Update telegram_sent status for all tasks | |
for task in tasks: | |
task.telegram_sent = True | |
db.session.commit() | |
logger.info(f"Sent {len(tasks)} daily tasks via Telegram to farmer {farmer.name}") | |
return jsonify({ | |
'success': True, | |
'message': f'Sent {len(tasks)} daily tasks via Telegram', | |
'tasks_count': len(tasks) | |
}) | |
except Exception as e: | |
logger.exception(f"Error sending daily tasks via Telegram for farmer {farmer.id}") | |
return jsonify({'error': f'Failed to send via Telegram: {str(e)}'}), 500 | |
def farmer_delete_all_daily_tasks(): | |
"""Delete all daily tasks for a specific date""" | |
farmer = get_current_farmer() | |
if not farmer: | |
return jsonify({'error': 'Access denied'}), 403 | |
data = request.get_json() or {} | |
target_date_str = data.get('date', date.today().isoformat()) | |
try: | |
task_date = datetime.strptime(target_date_str, '%Y-%m-%d').date() | |
except ValueError: | |
return jsonify({'error': 'Invalid date format'}), 400 | |
try: | |
# Get tasks for the date | |
tasks = DailyTask.query.filter_by( | |
farmer_id=farmer.id, | |
task_date=task_date | |
).all() | |
if not tasks: | |
return jsonify({'error': f'No tasks found for {task_date}'}), 404 | |
# Delete related completions first | |
task_ids = [task.id for task in tasks] | |
TaskCompletion.query.filter(TaskCompletion.task_id.in_(task_ids)).delete(synchronize_session=False) | |
# Delete tasks | |
count = len(tasks) | |
for task in tasks: | |
db.session.delete(task) | |
db.session.commit() | |
logger.info(f"Deleted {count} daily tasks for farmer {farmer.name} for {task_date}") | |
return jsonify({ | |
'success': True, | |
'message': f'Deleted {count} tasks for {task_date}', | |
'deleted_count': count | |
}) | |
except Exception as e: | |
db.session.rollback() | |
logger.exception(f"Error deleting daily tasks for farmer {farmer.id}") | |
return jsonify({'error': f'Failed to delete tasks: {str(e)}'}), 500 | |
def farmer_delete_farm(farm_id): | |
"""Delete a farm and its related data for the current farmer.""" | |
farm = Farm.query.get_or_404(farm_id) | |
farmer = get_current_farmer() | |
if not farmer or farm.farmer_id != farmer.id: | |
return jsonify({'error': 'Access denied'}), 403 | |
try: | |
# Remove related data | |
SoilData.query.filter_by(farm_id=farm.id).delete() | |
FarmingActivity.query.filter_by(farm_id=farm.id).delete() | |
WeatherData.query.filter_by(farm_id=farm.id).delete() | |
DailyAdvisory.query.filter_by(farm_id=farm.id).delete() | |
# Remove farm entry from yearly plan if present | |
year = date.today().year | |
plan_rec = YearlyPlan.query.filter_by(farmer_id=farmer.id, year=year).first() | |
if plan_rec: | |
try: | |
p = json.loads(plan_rec.plan_json) | |
except Exception: | |
p = {'year': year, 'farms': []} | |
p['farms'] = [f for f in p.get('farms', []) if f.get('farm_id') != farm.id] | |
if not p['farms']: | |
db.session.delete(plan_rec) | |
else: | |
plan_rec.plan_json = json.dumps(p) | |
plan_rec.summary_text = '\n'.join([f"{f['farm_name']}: {', '.join(f.get('top_tasks', [])[:2])}" for f in p.get('farms', [])]) | |
db.session.add(plan_rec) | |
db.session.delete(farm) | |
db.session.commit() | |
return jsonify({'success': True}) | |
except Exception as e: | |
logger.exception(f"Error deleting farm {farm_id}: {e}") | |
return jsonify({'success': False, 'error': 'Failed to delete farm'}), 500 | |
# =================== ADVISORY AND AI ROUTES =================== | |
def _serialize_for_json(obj): | |
"""Recursively convert non-serializable objects (like datetime/date) to strings for JSON.""" | |
if obj is None: | |
return None | |
if isinstance(obj, (str, int, float, bool)): | |
return obj | |
if isinstance(obj, (datetime, date)): | |
return obj.isoformat() | |
if isinstance(obj, dict): | |
return {k: _serialize_for_json(v) for k, v in obj.items()} | |
if isinstance(obj, (list, tuple)): | |
return [_serialize_for_json(v) for v in obj] | |
# Fallback to string representation | |
try: | |
return str(obj) | |
except Exception: | |
return None | |
def generate_advisory(farm_id): | |
"""Generate daily advisory for a farm""" | |
farm = Farm.query.get_or_404(farm_id) | |
# Check if farmer owns this farm | |
if farm.farmer_id != get_current_farmer().id: | |
return jsonify({'error': 'Access denied'}), 403 | |
if not gemini_service: | |
return jsonify({'error': 'AI service not available'}), 500 | |
try: | |
# Get required data | |
farmer_data = get_current_farmer().as_dict() | |
farm_data = farm.as_dict() | |
# Get soil data | |
soil_data = SoilData.query.filter_by(farm_id=farm_id).first() | |
soil_dict = soil_data.as_dict() if soil_data else {} | |
# Get weather data | |
weather_dict = {} | |
if weather_service: | |
weather_dict = weather_service.get_current_weather(farm.latitude, farm.longitude) | |
# Generate advisory | |
advisory = gemini_service.generate_daily_advisory( | |
farmer_data, farm_data, soil_dict, weather_dict | |
) | |
# Ensure advisory and weather data are JSON serializable | |
serializable_advisory = _serialize_for_json(advisory) | |
serializable_weather = _serialize_for_json(weather_dict) | |
# Save advisory to database | |
daily_advisory = DailyAdvisory( | |
farm_id=farm_id, | |
date=date.today(), | |
task_to_do=advisory.get('task_to_do', ''), | |
task_to_avoid=advisory.get('task_to_avoid', ''), | |
reason_explanation=advisory.get('reason_explanation', ''), | |
crop_stage=advisory.get('crop_stage', ''), | |
weather_context=json.dumps(serializable_weather), | |
gemini_response=json.dumps(serializable_advisory) | |
) | |
# Check if today's advisory already exists | |
existing_advisory = DailyAdvisory.query.filter_by( | |
farm_id=farm_id, | |
date=date.today() | |
).first() | |
if existing_advisory: | |
# Update existing advisory | |
existing_advisory.task_to_do = advisory.get('task_to_do', '') | |
existing_advisory.task_to_avoid = advisory.get('task_to_avoid', '') | |
existing_advisory.reason_explanation = advisory.get('reason_explanation', '') | |
existing_advisory.crop_stage = advisory.get('crop_stage', '') | |
existing_advisory.weather_context = json.dumps(serializable_weather) | |
existing_advisory.gemini_response = json.dumps(serializable_advisory) | |
else: | |
db.session.add(daily_advisory) | |
db.session.commit() | |
return jsonify({ | |
'success': True, | |
'advisory': _serialize_for_json(advisory) | |
}) | |
except Exception as e: | |
logger.exception("Error generating advisory") | |
# Return detailed error in debug mode to aid local debugging | |
if app.debug: | |
return jsonify({'error': 'Failed to generate advisory', 'details': str(e)}), 500 | |
return jsonify({'error': 'Failed to generate advisory'}), 500 | |
def send_sms_advisory(farm_id): | |
"""SMS service has been replaced with Telegram""" | |
return jsonify({'error': 'SMS service no longer available. Please use Telegram messaging.'}), 410 | |
def send_telegram_advisory(farm_id): | |
"""Send Telegram advisory to farmer""" | |
farm = Farm.query.get_or_404(farm_id) | |
# Check if farmer owns this farm | |
if farm.farmer_id != get_current_farmer().id: | |
return jsonify({'error': 'Access denied'}), 403 | |
if not telegram_service: | |
return jsonify({'error': 'Telegram service not available'}), 500 | |
# Get today's advisory | |
advisory = DailyAdvisory.query.filter_by( | |
farm_id=farm_id, | |
date=date.today() | |
).first() | |
if not advisory: | |
return jsonify({'error': 'No advisory found for today'}), 404 | |
try: | |
farmer = get_current_farmer() | |
# Check if farmer has Telegram chat ID | |
if not farmer.telegram_chat_id: | |
return jsonify({'error': 'Telegram chat ID not configured. Please start chat with @Krushi_Mitra_Bot'}), 400 | |
# Format Telegram message | |
advisory_data = { | |
'task_to_do': advisory.task_to_do, | |
'task_to_avoid': advisory.task_to_avoid, | |
'reason_explanation': advisory.reason_explanation | |
} | |
message = format_daily_advisory_telegram(farmer.name, advisory_data) | |
# Send Telegram message | |
result = telegram_service.send_message(farmer.telegram_chat_id, message) | |
# Log message (reuse SMS log table for now) | |
sms_log = SMSLog( | |
farmer_id=farmer.id, | |
phone_number=farmer.telegram_chat_id, # Store chat ID in phone field | |
message_content=message, | |
twilio_sid=str(result.get('message_id')), # Store message ID | |
status=result.get('status') or 'failed', | |
error_message=result.get('error') | |
) | |
db.session.add(sms_log) | |
db.session.commit() | |
if result.get('status') == 'sent': | |
return jsonify({'success': True, 'message': 'Telegram message sent successfully'}) | |
else: | |
return jsonify({'success': False, 'error': result.get('error', 'Failed to send Telegram message')}), 500 | |
except Exception as e: | |
logger.error(f"Error sending Telegram message: {str(e)}") | |
return jsonify({'error': 'Failed to send Telegram message'}), 500 | |
# =================== NEW FEATURE ROUTES =================== | |
def get_weather_alerts(farm_id): | |
"""Get weather alerts for a specific farm""" | |
farm = Farm.query.get_or_404(farm_id) | |
farmer = get_current_farmer() | |
if farm.farmer_id != farmer.id: | |
return jsonify({'error': 'Access denied'}), 403 | |
if not farm.latitude or not farm.longitude: | |
return jsonify({'error': 'Farm coordinates not set'}), 400 | |
try: | |
if weather_alert_service: | |
alerts = weather_alert_service.get_weather_alerts(farm.latitude, farm.longitude) | |
return jsonify({'success': True, 'alerts': alerts}) | |
else: | |
return jsonify({'error': 'Weather alert service not available'}), 500 | |
except Exception as e: | |
logger.exception(f"Error getting weather alerts for farm {farm_id}") | |
return jsonify({'error': f'Failed to get weather alerts: {str(e)}'}), 500 | |
def manage_weather_alerts(farm_id): | |
"""Enable or disable weather alerts for a specific farm""" | |
farm = Farm.query.get_or_404(farm_id) | |
farmer = get_current_farmer() | |
if farm.farmer_id != farmer.id: | |
return jsonify({'error': 'Access denied'}), 403 | |
try: | |
data = request.get_json() | |
action = data.get('action') | |
if action == 'enable': | |
# Update farm to enable weather alerts | |
try: | |
farm.weather_alerts_enabled = True | |
db.session.commit() | |
return jsonify({'success': True, 'message': 'Weather alerts enabled'}) | |
except Exception as e: | |
# Handle case where column doesn't exist yet | |
logger.warning(f"weather_alerts_enabled column may not exist: {str(e)}") | |
return jsonify({'success': True, 'message': 'Weather alerts enabled (feature pending database update)'}) | |
elif action == 'disable': | |
# Update farm to disable weather alerts | |
try: | |
farm.weather_alerts_enabled = False | |
db.session.commit() | |
return jsonify({'success': True, 'message': 'Weather alerts disabled'}) | |
except Exception as e: | |
# Handle case where column doesn't exist yet | |
logger.warning(f"weather_alerts_enabled column may not exist: {str(e)}") | |
return jsonify({'success': True, 'message': 'Weather alerts disabled (feature pending database update)'}) | |
else: | |
return jsonify({'error': 'Invalid action. Use "enable" or "disable"'}), 400 | |
except Exception as e: | |
logger.exception(f"Error managing weather alerts for farm {farm_id}") | |
return jsonify({'error': f'Failed to manage weather alerts: {str(e)}'}), 500 | |
def get_market_prices(): | |
"""Get current market prices for crops or handle actions""" | |
if request.method == 'POST': | |
try: | |
data = request.get_json() | |
action = data.get('action') | |
if action == 'refresh': | |
# Force refresh of market prices | |
farmer = get_current_farmer() | |
farms = Farm.query.filter_by(farmer_id=farmer.id).all() | |
all_crops = set() | |
for farm in farms: | |
crops = farm.get_crop_types() | |
all_crops.update(crops) | |
if market_price_service and all_crops: | |
# Clear cache and get fresh prices | |
for crop in all_crops: | |
try: | |
market_price_service.refresh_crop_price(crop) | |
except Exception as e: | |
logger.error(f"Error refreshing price for {crop}: {str(e)}") | |
return jsonify({'success': True, 'message': 'Market prices refreshed'}) | |
elif action == 'subscribe': | |
# Subscribe to market price alerts | |
farmer = get_current_farmer() | |
# In a real app, you'd save subscription preferences to database | |
return jsonify({'success': True, 'message': 'Subscribed to market price alerts'}) | |
else: | |
return jsonify({'error': 'Invalid action'}), 400 | |
except Exception as e: | |
logger.exception("Error handling market prices action") | |
return jsonify({'error': f'Failed to process action: {str(e)}'}), 500 | |
# GET request - return current prices | |
try: | |
farmer = get_current_farmer() | |
farms = Farm.query.filter_by(farmer_id=farmer.id).all() | |
# Get all unique crops from farmer's farms | |
all_crops = set() | |
for farm in farms: | |
crops = farm.get_crop_types() | |
all_crops.update(crops) | |
crop_prices = {} | |
if market_price_service and all_crops: | |
for crop in all_crops: | |
try: | |
price_data = market_price_service.get_crop_price(crop) | |
crop_prices[crop] = price_data | |
except Exception as e: | |
logger.error(f"Error getting price for {crop}: {str(e)}") | |
crop_prices[crop] = {'error': 'Price not available'} | |
return jsonify({'success': True, 'prices': crop_prices}) | |
except Exception as e: | |
logger.exception("Error getting market prices") | |
return jsonify({'error': f'Failed to get market prices: {str(e)}'}), 500 | |
def get_disease_detection_history(farm_id): | |
"""Get disease detection history for a specific farm""" | |
farm = Farm.query.get_or_404(farm_id) | |
farmer = get_current_farmer() | |
if farm.farmer_id != farmer.id: | |
return jsonify({'error': 'Access denied'}), 403 | |
try: | |
# Get disease detection records from database | |
detections = WeatherAlert.query.filter_by( | |
farm_id=farm_id, | |
alert_type='disease_detection' | |
).order_by(WeatherAlert.created_at.desc()).limit(10).all() | |
detection_list = [] | |
for detection in detections: | |
detection_list.append({ | |
'disease_name': detection.message.replace('Disease detected: ', ''), | |
'severity': detection.severity, | |
'confidence': 0.85, # placeholder since we don't store confidence | |
'treatment_recommendation': detection.recommendations, | |
'detection_date': detection.created_at.isoformat(), | |
'is_active': detection.is_active | |
}) | |
return jsonify({'success': True, 'detections': detection_list}) | |
except Exception as e: | |
logger.exception(f"Error getting disease detection history for farm {farm_id}") | |
return jsonify({'error': f'Failed to get disease detection history: {str(e)}'}), 500 | |
def detect_disease(farm_id): | |
"""Detect crop disease from uploaded image""" | |
farm = Farm.query.get_or_404(farm_id) | |
farmer = get_current_farmer() | |
if farm.farmer_id != farmer.id: | |
return jsonify({'error': 'Access denied'}), 403 | |
if 'image' not in request.files: | |
return jsonify({'error': 'No image uploaded'}), 400 | |
image_file = request.files['image'] | |
if image_file.filename == '': | |
return jsonify({'error': 'No image selected'}), 400 | |
try: | |
if disease_detection_service: | |
# Save uploaded image temporarily | |
import tempfile | |
import os | |
with tempfile.NamedTemporaryFile(delete=False, suffix='.jpg') as tmp_file: | |
image_file.save(tmp_file.name) | |
# Detect disease | |
detection_result = disease_detection_service.analyze_crop_image(tmp_file.name) | |
# Save detection result to database and send via Telegram if configured | |
if detection_result and detection_result.get('disease_detected'): | |
alert = WeatherAlert( | |
farm_id=farm_id, | |
alert_type='disease_detection', | |
severity=detection_result.get('severity', 'medium'), | |
message=f"Disease detected: {detection_result.get('disease_name', 'Unknown')}", | |
recommendations=detection_result.get('treatment', 'Consult agricultural expert'), | |
is_active=True | |
) | |
db.session.add(alert) | |
db.session.commit() | |
# Send results via Telegram if configured | |
farmer = get_current_farmer() | |
if farmer and farmer.telegram_chat_id and telegram_service: | |
try: | |
# Create result message | |
disease_name = detection_result.get('disease_name', 'Unknown Disease') | |
confidence = detection_result.get('confidence_score', 0) | |
treatment = detection_result.get('treatment', 'Consult agricultural expert') | |
telegram_msg = f"""🚨 <b>Disease Detection Alert</b> | |
<b>Farm:</b> {farm.farm_name} | |
<b>Disease Detected:</b> {disease_name} | |
<b>Confidence:</b> {confidence:.1%} | |
<b>Severity:</b> {detection_result.get('severity', 'medium').title()} | |
<b>Treatment Recommendations:</b> | |
{treatment} | |
<i>Take immediate action to prevent spread!</i>""" | |
telegram_service.send_message(farmer.telegram_chat_id, telegram_msg) | |
logger.info(f"Sent disease detection alert via Telegram to farmer {farmer.name}") | |
except Exception as e: | |
logger.error(f"Failed to send disease detection via Telegram: {str(e)}") | |
# Clean up temp file | |
os.unlink(tmp_file.name) | |
return jsonify({'success': True, 'detection': detection_result}) | |
else: | |
return jsonify({'error': 'Disease detection service not available'}), 500 | |
except Exception as e: | |
logger.exception(f"Error detecting disease for farm {farm_id}") | |
return jsonify({'error': f'Failed to detect disease: {str(e)}'}), 500 | |
def farmer_send_image_telegram(): | |
"""Send an image via Telegram""" | |
farmer = get_current_farmer() | |
if not farmer.telegram_chat_id: | |
return jsonify({'error': 'Telegram chat ID not configured. Please register for Telegram notifications first.'}), 400 | |
if not telegram_service: | |
return jsonify({'error': 'Telegram service not available'}), 500 | |
if 'image' not in request.files: | |
return jsonify({'error': 'No image uploaded'}), 400 | |
image_file = request.files['image'] | |
if image_file.filename == '': | |
return jsonify({'error': 'No image selected'}), 400 | |
try: | |
# Save uploaded image temporarily | |
import tempfile | |
import os | |
# Get file extension | |
filename = image_file.filename | |
file_extension = os.path.splitext(filename)[1].lower() | |
# Validate image file type | |
allowed_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp'] | |
if file_extension not in allowed_extensions: | |
return jsonify({'error': 'Invalid file type. Please upload an image file (JPG, PNG, GIF, BMP).'}), 400 | |
with tempfile.NamedTemporaryFile(delete=False, suffix=file_extension) as tmp_file: | |
image_file.save(tmp_file.name) | |
# Get caption from request | |
caption = request.form.get('caption', f"📷 Image from {farmer.name}") | |
# Send image via Telegram | |
photo_result = telegram_service.send_photo( | |
farmer.telegram_chat_id, | |
tmp_file.name, | |
caption=caption | |
) | |
# Clean up temp file | |
os.unlink(tmp_file.name) | |
if photo_result.get('status') == 'sent': | |
logger.info(f"Image sent successfully via Telegram to farmer {farmer.name}") | |
return jsonify({'success': True, 'message': 'Image sent via Telegram successfully!'}) | |
else: | |
return jsonify({'error': f'Failed to send image via Telegram: {photo_result.get("error", "Unknown error")}'}), 500 | |
except Exception as e: | |
logger.exception(f"Error sending image via Telegram for farmer {farmer.id}") | |
return jsonify({'error': f'Failed to send image via Telegram: {str(e)}'}), 500 | |
def get_farmer_alerts(): | |
"""Get all active alerts for the farmer""" | |
try: | |
farmer = get_current_farmer() | |
farms = Farm.query.filter_by(farmer_id=farmer.id).all() | |
farm_ids = [farm.id for farm in farms] | |
# Get active alerts for all farmer's farms | |
alerts = WeatherAlert.query.filter( | |
WeatherAlert.farm_id.in_(farm_ids), | |
WeatherAlert.is_active == True | |
).order_by(WeatherAlert.created_at.desc()).limit(20).all() | |
alert_list = [] | |
for alert in alerts: | |
alert_list.append({ | |
'id': alert.id, | |
'farm_name': alert.farm.farm_name, | |
'type': alert.alert_type, | |
'severity': alert.severity, | |
'message': alert.message, | |
'recommendations': alert.recommendations, | |
'created_at': alert.created_at.isoformat() | |
}) | |
return jsonify({'success': True, 'alerts': alert_list}) | |
except Exception as e: | |
logger.exception("Error getting farmer alerts") | |
return jsonify({'error': f'Failed to get alerts: {str(e)}'}), 500 | |
def download_yearly_plan_pdf(plan_id): | |
"""Download yearly plan as PDF""" | |
plan = YearlyPlan.query.get_or_404(plan_id) | |
farmer = get_current_farmer() | |
if plan.farmer_id != farmer.id: | |
return jsonify({'error': 'Access denied'}), 403 | |
try: | |
if pdf_generator_service: | |
# Generate PDF from the HTML plan | |
plan_data = json.loads(plan.plan_json) if plan.plan_json else {} | |
html_content = plan_data.get('html_content', '<h1>Plan not available</h1>') | |
pdf_path = pdf_generator_service.generate_yearly_plan_pdf( | |
farmer_name=farmer.name, | |
html_content=html_content, | |
year=plan.year | |
) | |
return send_file(pdf_path, as_attachment=True, | |
download_name=f"yearly_plan_{farmer.name}_{plan.year}.pdf") | |
else: | |
return jsonify({'error': 'PDF generation service not available'}), 500 | |
except Exception as e: | |
logger.exception(f"Error generating PDF for plan {plan_id}") | |
return jsonify({'error': f'Failed to generate PDF: {str(e)}'}), 500 | |
# =================== SCHEDULED TASKS =================== | |
def generate_daily_advisories(): | |
"""Generate daily advisories for all farms""" | |
with app.app_context(): | |
logger.info("Starting daily advisory generation") | |
farms = Farm.query.all() | |
for farm in farms: | |
try: | |
# Check if advisory already exists for today | |
existing_advisory = DailyAdvisory.query.filter_by( | |
farm_id=farm.id, | |
date=date.today() | |
).first() | |
if existing_advisory: | |
continue # Skip if already generated | |
# Get required data | |
farmer_data = farm.farmer.as_dict() | |
farm_data = farm.as_dict() | |
# Get soil data | |
soil_data = SoilData.query.filter_by(farm_id=farm.id).first() | |
soil_dict = soil_data.as_dict() if soil_data else {} | |
# Get weather data | |
weather_dict = {} | |
if weather_service: | |
weather_dict = weather_service.get_current_weather(farm.latitude, farm.longitude) | |
# Generate advisory | |
if gemini_service: | |
advisory = gemini_service.generate_daily_advisory( | |
farmer_data, farm_data, soil_dict, weather_dict | |
) | |
# Ensure serializable | |
serializable_advisory = _serialize_for_json(advisory) | |
serializable_weather = _serialize_for_json(weather_dict) | |
# Save advisory | |
daily_advisory = DailyAdvisory( | |
farm_id=farm.id, | |
date=date.today(), | |
task_to_do=advisory.get('task_to_do', ''), | |
task_to_avoid=advisory.get('task_to_avoid', ''), | |
reason_explanation=advisory.get('reason_explanation', ''), | |
crop_stage=advisory.get('crop_stage', ''), | |
weather_context=json.dumps(serializable_weather), | |
gemini_response=json.dumps(serializable_advisory) | |
) | |
db.session.add(daily_advisory) | |
db.session.commit() | |
logger.info(f"Generated advisory for farm {farm.id}") | |
except Exception as e: | |
logger.error(f"Error generating advisory for farm {farm.id}: {str(e)}") | |
logger.info("Daily advisory generation completed") | |
def send_daily_sms(): | |
"""Send daily Telegram advisories to all farmers (SMS function deprecated)""" | |
with app.app_context(): | |
logger.info("Starting daily Telegram advisory sending") | |
if not telegram_service: | |
logger.warning("Telegram service not available") | |
return | |
# Get today's advisories | |
today_advisories = DailyAdvisory.query.filter_by(date=date.today()).all() | |
for advisory in today_advisories: | |
try: | |
farm = advisory.farm | |
farmer = farm.farmer | |
# Determine target chat id (farmer-specific or global fallback) | |
target_chat = farmer.telegram_chat_id or os.getenv('GLOBAL_TELEGRAM_CHAT_ID') | |
if not target_chat: | |
logger.info(f"Farmer {farmer.id} has no telegram_chat_id and no GLOBAL_TELEGRAM_CHAT_ID set, skipping") | |
continue | |
# Format Telegram message | |
advisory_data = { | |
'task_to_do': advisory.task_to_do, | |
'task_to_avoid': advisory.task_to_avoid, | |
'reason_explanation': advisory.reason_explanation | |
} | |
message = telegram_service.format_daily_advisory_telegram(farmer.name, advisory_data) | |
# Send Telegram message | |
result = telegram_service.send_message(target_chat, message) | |
if result: | |
logger.info(f"Sent Telegram message to farmer {farmer.id}") | |
else: | |
logger.error(f"Failed to send Telegram message to farmer {farmer.id}") | |
except Exception as e: | |
logger.error(f"Error sending Telegram message for advisory {advisory.id}: {str(e)}") | |
logger.info("Daily Telegram advisory sending completed") | |
# =================== ADMIN ROUTES =================== | |
def admin_dashboard(): | |
"""Admin dashboard""" | |
# Get statistics | |
total_farmers = Farmer.query.count() | |
total_farms = Farm.query.count() | |
total_advisories = DailyAdvisory.query.count() | |
total_sms = SMSLog.query.count() | |
# Get recent activities | |
recent_farmers = Farmer.query.order_by(Farmer.created_at.desc()).limit(5).all() | |
recent_sms = SMSLog.query.order_by(SMSLog.sent_at.desc()).limit(10).all() | |
return render_template('admin_dashboard.html', | |
total_farmers=total_farmers, | |
total_farms=total_farms, | |
total_advisories=total_advisories, | |
total_sms=total_sms, | |
recent_farmers=recent_farmers, | |
recent_sms=recent_sms) | |
def admin_farmers(): | |
"""Admin farmers management""" | |
farmers = Farmer.query.order_by(Farmer.created_at.desc()).all() | |
return render_template('admin_farmers.html', farmers=farmers) | |
def admin_register_telegram(farmer_id): | |
"""Register or update a farmer's Telegram chat ID. | |
Expected JSON body: { "chat_id": "<telegram_chat_id>" } | |
This allows the system to send Telegram advisories to the farmer. | |
""" | |
farmer = Farmer.query.get_or_404(farmer_id) | |
data = request.get_json(force=True, silent=True) or {} | |
chat_id = data.get('chat_id') | |
if not chat_id: | |
return jsonify({'success': False, 'error': 'chat_id required'}), 400 | |
# Save as string to avoid integer/float issues | |
farmer.telegram_chat_id = str(chat_id) | |
db.session.commit() | |
return jsonify({'success': True, 'farmer_id': farmer.id, 'telegram_chat_id': farmer.telegram_chat_id}) | |
def admin_test_telegram(farmer_id): | |
"""Send a test Telegram message to a farmer to verify chat_id configuration.""" | |
farmer = Farmer.query.get_or_404(farmer_id) | |
# Allow global fallback if farmer has none | |
target_chat = farmer.telegram_chat_id or os.getenv('GLOBAL_TELEGRAM_CHAT_ID') | |
if not target_chat: | |
return jsonify({'success': False, 'error': 'Telegram chat ID not configured. Please start chat with @Krushi_Mitra_Bot or set GLOBAL_TELEGRAM_CHAT_ID in .env'}), 400 | |
if not telegram_service: | |
return jsonify({'success': False, 'error': 'Telegram service not available'}), 500 | |
message = request.json.get('message') if request.is_json else None | |
if not message: | |
message = f"Hello {farmer.name}, this is a test message from Krushi Mitra Bot. Reply to this message if you receive it." | |
try: | |
ok = telegram_service.send_message(target_chat, message) | |
if ok: | |
return jsonify({'success': True, 'message': 'Test Telegram message sent'}) | |
else: | |
return jsonify({'success': False, 'error': 'Failed to send Telegram message'}), 500 | |
except Exception as e: | |
logger.error(f"Error sending Telegram test message to farmer {farmer.id}: {str(e)}") | |
return jsonify({'success': False, 'error': str(e)}), 500 | |
def admin_sms_logs(): | |
"""Admin SMS logs""" | |
# Get filter parameters | |
status_filter = request.args.get('status') | |
date_filter = request.args.get('date') | |
phone_filter = request.args.get('phone') | |
# Build query | |
query = SMSLog.query | |
if status_filter: | |
query = query.filter(SMSLog.delivery_status == status_filter) | |
if date_filter: | |
query = query.filter(SMSLog.sent_at >= date_filter) | |
if phone_filter: | |
query = query.filter(SMSLog.phone_number.contains(phone_filter)) | |
sms_logs = query.order_by(SMSLog.sent_at.desc()).limit(100).all() | |
# Get statistics | |
total_sms = SMSLog.query.count() | |
delivered_sms = SMSLog.query.filter_by(delivery_status='delivered').count() | |
pending_sms = SMSLog.query.filter_by(delivery_status='pending').count() | |
failed_sms = SMSLog.query.filter_by(delivery_status='failed').count() | |
return render_template('admin_sms_logs.html', | |
sms_logs=sms_logs, | |
total_sms=total_sms, | |
delivered_sms=delivered_sms, | |
pending_sms=pending_sms, | |
failed_sms=failed_sms) | |
# =================== API ROUTES =================== | |
def api_weather(farm_id): | |
"""Get weather data for a farm""" | |
farm = Farm.query.get_or_404(farm_id) | |
if farm.farmer_id != current_user.farmer.id: | |
return jsonify({'error': 'Access denied'}), 403 | |
if not weather_service: | |
return jsonify({'error': 'Weather service not available'}), 500 | |
try: | |
weather_data = weather_service.get_current_weather(farm.latitude, farm.longitude) | |
return jsonify(weather_data) | |
except Exception as e: | |
logger.error(f"Error fetching weather: {str(e)}") | |
return jsonify({'error': 'Failed to fetch weather data'}), 500 | |
def api_forecast(farm_id): | |
"""Get weather forecast for a farm""" | |
farm = Farm.query.get_or_404(farm_id) | |
if farm.farmer_id != current_user.farmer.id: | |
return jsonify({'error': 'Access denied'}), 403 | |
if not weather_service: | |
return jsonify({'error': 'Weather service not available'}), 500 | |
try: | |
days = request.args.get('days', 5, type=int) | |
forecast_data = weather_service.get_weather_forecast(farm.latitude, farm.longitude, days) | |
return jsonify(forecast_data) | |
except Exception as e: | |
logger.error(f"Error fetching forecast: {str(e)}") | |
return jsonify({'error': 'Failed to fetch forecast data'}), 500 | |
# =================== ADMIN API ROUTES =================== | |
def admin_add_farmer(): | |
"""Add new farmer via admin""" | |
try: | |
data = request.json | |
# Check if farmer with this Aadhaar already exists | |
existing_farmer = Farmer.query.filter_by(aadhaar_number=data['aadhaar_number']).first() | |
if existing_farmer: | |
return jsonify({'success': False, 'message': 'Farmer with this Aadhaar already exists'}) | |
# Create new farmer | |
farmer = Farmer( | |
name=data['name'], | |
aadhaar_number=data['aadhaar_number'], | |
phone_number=data['phone_number'], | |
email=data.get('email'), | |
district=data['district'], | |
state=data['state'] | |
) | |
db.session.add(farmer) | |
db.session.commit() | |
return jsonify({'success': True, 'message': 'Farmer added successfully'}) | |
except Exception as e: | |
logger.error(f"Error adding farmer: {str(e)}") | |
return jsonify({'success': False, 'message': 'Error adding farmer'}) | |
def admin_get_farmer(farmer_id): | |
"""Get farmer details""" | |
farmer = Farmer.query.get_or_404(farmer_id) | |
farmer_data = farmer.as_dict() | |
farmer_data['farms'] = [farm.as_dict() for farm in farmer.farms] | |
return jsonify(farmer_data) | |
def admin_delete_farmer(farmer_id): | |
"""Delete farmer""" | |
try: | |
farmer = Farmer.query.get_or_404(farmer_id) | |
# Delete associated data | |
for farm in farmer.farms: | |
# Delete farm-related data | |
SoilData.query.filter_by(farm_id=farm.id).delete() | |
FarmingActivity.query.filter_by(farm_id=farm.id).delete() | |
WeatherData.query.filter_by(farm_id=farm.id).delete() | |
DailyAdvisory.query.filter_by(farm_id=farm.id).delete() | |
db.session.delete(farm) | |
# Delete SMS logs | |
SMSLog.query.filter_by(farmer_id=farmer.id).delete() | |
# Delete farmer | |
db.session.delete(farmer) | |
db.session.commit() | |
return jsonify({'success': True, 'message': 'Farmer deleted successfully'}) | |
except Exception as e: | |
logger.error(f"Error deleting farmer: {str(e)}") | |
return jsonify({'success': False, 'message': 'Error deleting farmer'}) | |
def admin_get_sms(sms_id): | |
"""Get SMS details""" | |
sms = SMSLog.query.get_or_404(sms_id) | |
sms_data = sms.as_dict() | |
if sms.farmer: | |
sms_data['farmer'] = {'name': sms.farmer.name, 'phone': sms.farmer.phone_number} | |
return jsonify(sms_data) | |
def admin_retry_sms(sms_id): | |
"""SMS retry disabled - replaced with Telegram""" | |
return jsonify({'success': False, 'message': 'SMS service has been replaced with Telegram messaging'}) | |
def admin_send_test_sms(): | |
"""Test SMS disabled - replaced with Telegram""" | |
return jsonify({'success': False, 'message': 'SMS service has been replaced with Telegram messaging'}) | |
def admin_generate_all_advisories(): | |
"""Generate advisories for all farms""" | |
try: | |
farms = Farm.query.all() | |
count = 0 | |
for farm in farms: | |
try: | |
# Generate advisory using Gemini service | |
location = f"{farm.farm_location}" | |
crop_details = farm.get_crop_details() | |
crops = [detail['name'] for detail in crop_details] if crop_details else [] | |
if gemini_service and crops: | |
# Get weather data | |
weather_data = None | |
if weather_service: | |
weather_data = weather_service.get_weather_data(location) | |
advisory = gemini_service.generate_advisory( | |
crops=crops, | |
location=location, | |
farm_size=farm.farm_size, | |
weather_data=weather_data | |
) | |
if advisory: | |
# Save advisory into DailyAdvisory for today (create or update) | |
serializable_advisory = _serialize_for_json(advisory) | |
serializable_weather = _serialize_for_json(weather_data) | |
existing = DailyAdvisory.query.filter_by(farm_id=farm.id, date=date.today()).first() | |
if existing: | |
existing.task_to_do = advisory.get('task_to_do', '') | |
existing.task_to_avoid = advisory.get('task_to_avoid', '') | |
existing.reason_explanation = advisory.get('reason_explanation', '') | |
existing.crop_stage = advisory.get('crop_stage', '') | |
existing.weather_context = json.dumps(serializable_weather) | |
existing.gemini_response = json.dumps(serializable_advisory) | |
else: | |
daily = DailyAdvisory( | |
farm_id=farm.id, | |
date=date.today(), | |
task_to_do=advisory.get('task_to_do', ''), | |
task_to_avoid=advisory.get('task_to_avoid', ''), | |
reason_explanation=advisory.get('reason_explanation', ''), | |
crop_stage=advisory.get('crop_stage', ''), | |
weather_context=json.dumps(serializable_weather), | |
gemini_response=json.dumps(serializable_advisory) | |
) | |
db.session.add(daily) | |
count += 1 | |
except Exception as e: | |
logger.error(f"Error generating advisory for farm {farm.id}: {str(e)}") | |
continue | |
db.session.commit() | |
return jsonify({'success': True, 'count': count}) | |
except Exception as e: | |
logger.error(f"Error generating all advisories: {str(e)}") | |
return jsonify({'success': False, 'error': str(e)}) | |
def admin_send_all_sms(): | |
"""Send Telegram advisories to all farmers with active farms""" | |
try: | |
if not telegram_service: | |
return jsonify({'success': False, 'error': 'Telegram service not available'}) | |
# Find all farms that have an advisory for today | |
advisories = DailyAdvisory.query.filter_by(date=date.today()).all() | |
sent_count = 0 | |
for adv in advisories: | |
try: | |
farm = adv.farm | |
farmer = farm.farmer | |
if not farmer or not farmer.telegram_chat_id: | |
continue | |
# Build message from advisory fields | |
advisory_data = { | |
'task_to_do': adv.task_to_do, | |
'task_to_avoid': adv.task_to_avoid, | |
'reason_explanation': adv.reason_explanation | |
} | |
message = telegram_service.format_daily_advisory_telegram(farmer.name, advisory_data) | |
# Send Telegram message | |
target_chat = farmer.telegram_chat_id or os.getenv('GLOBAL_TELEGRAM_CHAT_ID') | |
if not target_chat: | |
logger.info(f"Skipping farm {farm.id} because no telegram chat id available") | |
continue | |
result = telegram_service.send_message(target_chat, message) | |
if result: | |
sent_count += 1 | |
logger.info(f"Sent Telegram advisory to farmer {farmer.id}") | |
else: | |
logger.error(f"Failed to send Telegram advisory to farmer {farmer.id}") | |
except Exception as e: | |
logger.error(f"Error sending Telegram advisory for farm {farm.id}: {str(e)}") | |
return jsonify({'success': True, 'sent_count': sent_count, 'total_advisories': len(advisories)}) | |
except Exception as e: | |
logger.error(f"Error sending all Telegram advisories: {str(e)}") | |
return jsonify({'success': False, 'error': str(e)}) | |
# =================== ERROR HANDLERS =================== | |
def not_found_error(error): | |
return render_template('error.html', error_code=404, error_message="Page not found"), 404 | |
def internal_error(error): | |
db.session.rollback() | |
return render_template('error.html', error_code=500, error_message="Internal server error"), 500 | |
# =================== INITIALIZATION =================== | |
def create_admin_user(): | |
"""Create default admin user if not exists""" | |
admin = AdminUser.query.filter_by(username='admin').first() | |
if not admin: | |
admin = AdminUser( | |
username='admin', | |
email='admin@farmmanagement.com', | |
is_super_admin=True | |
) | |
admin.set_password('admin123') # Change this in production | |
db.session.add(admin) | |
db.session.commit() | |
logger.info("Default admin user created") | |
def generate_and_send_daily_tasks(): | |
"""Background task to generate and send daily tasks to farmers via Telegram""" | |
global daily_task_service, telegram_service | |
try: | |
with app.app_context(): | |
logger.info("Starting daily task generation and sending") | |
if not daily_task_service: | |
logger.warning("Daily task service not available") | |
return | |
if not telegram_service: | |
logger.warning("Telegram service not available") | |
return | |
today = date.today() | |
farmers_processed = 0 | |
tasks_generated = 0 | |
# Get all farmers with telegram chat IDs | |
farmers = Farmer.query.filter(Farmer.telegram_chat_id.isnot(None)).all() | |
for farmer in farmers: | |
try: | |
# Check if tasks already exist for today | |
existing_count = DailyTask.query.filter_by( | |
farmer_id=farmer.id, | |
task_date=today | |
).count() | |
if existing_count > 0: | |
logger.info(f"Tasks already exist for farmer {farmer.name} for {today}") | |
continue | |
# Generate tasks for this farmer | |
generated_tasks = daily_task_service.generate_daily_tasks(farmer, today) | |
if not generated_tasks: | |
logger.warning(f"No tasks generated for farmer {farmer.name}") | |
continue | |
# Save tasks to database | |
saved_tasks = [] | |
for task_data in generated_tasks: | |
task = DailyTask( | |
farmer_id=farmer.id, | |
task_date=today, | |
task_type=task_data.get('task_type', 'general'), | |
task_title=task_data.get('task_title', 'Farm Task'), | |
task_description=task_data.get('task_description', ''), | |
priority=task_data.get('priority', 'medium'), | |
estimated_duration=task_data.get('estimated_duration', 30), | |
weather_dependent=task_data.get('weather_dependent', False), | |
farm_id=task_data.get('farm_id'), | |
crop_specific=task_data.get('crop_specific'), | |
telegram_sent=False | |
) | |
db.session.add(task) | |
db.session.flush() | |
saved_tasks.append(task) | |
db.session.commit() | |
tasks_generated += len(saved_tasks) | |
# Send tasks via Telegram | |
try: | |
# Create header message | |
header_msg = f"🌅 <b>Daily Farming Tasks - {today.strftime('%d %B %Y')}</b>\n\n" | |
header_msg += f"👋 Good morning, {farmer.name}!\n" | |
header_msg += f"📋 You have {len(saved_tasks)} tasks scheduled for today:\n\n" | |
# Send header | |
telegram_service.send_message(farmer.telegram_chat_id, header_msg) | |
# Send each task | |
for i, task in enumerate(saved_tasks, 1): | |
task_msg = daily_task_service.format_task_for_telegram( | |
{ | |
'task_type': task.task_type, | |
'task_title': task.task_title, | |
'task_description': task.task_description, | |
'priority': task.priority, | |
'estimated_duration': task.estimated_duration, | |
'weather_dependent': task.weather_dependent, | |
'crop_specific': task.crop_specific | |
}, | |
farmer.name | |
) | |
task_msg = f"<b>Task {i}/{len(saved_tasks)}</b>\n\n" + task_msg | |
task_msg += f"\n💡 <i>Mark as completed when done!</i>" | |
telegram_service.send_message(farmer.telegram_chat_id, task_msg) | |
# Small delay to avoid rate limiting | |
import time | |
time.sleep(0.5) | |
# Send footer message | |
footer_msg = f"✅ <b>Instructions:</b>\n" | |
footer_msg += f"• Check each task when completed\n" | |
footer_msg += f"• Rate your experience (1-5 stars)\n" | |
footer_msg += f"• Add feedback if needed\n\n" | |
footer_msg += f"📱 Manage tasks from your farmer dashboard\n" | |
footer_msg += f"🌟 Have a productive farming day!" | |
telegram_service.send_message(farmer.telegram_chat_id, footer_msg) | |
# Update telegram_sent status | |
for task in saved_tasks: | |
task.telegram_sent = True | |
db.session.commit() | |
farmers_processed += 1 | |
logger.info(f"Sent {len(saved_tasks)} daily tasks to farmer {farmer.name}") | |
except Exception as telegram_error: | |
logger.error(f"Failed to send tasks to farmer {farmer.name}: {str(telegram_error)}") | |
continue | |
except Exception as farmer_error: | |
logger.error(f"Error processing farmer {farmer.name}: {str(farmer_error)}") | |
db.session.rollback() | |
continue | |
logger.info(f"Daily task generation completed: {farmers_processed} farmers, {tasks_generated} tasks generated") | |
except Exception as e: | |
logger.exception(f"Error in daily task generation background job: {str(e)}") | |
def init_scheduler(): | |
"""Initialize background scheduler for daily tasks""" | |
scheduler = BackgroundScheduler() | |
# Schedule daily advisory generation at 6:00 AM | |
scheduler.add_job( | |
func=generate_daily_advisories, | |
trigger="cron", | |
hour=6, | |
minute=0, | |
id='daily_advisories' | |
) | |
# Schedule daily task generation and sending at 6:30 AM | |
scheduler.add_job( | |
func=generate_and_send_daily_tasks, | |
trigger="cron", | |
hour=6, | |
minute=30, | |
id='daily_tasks' | |
) | |
# Schedule daily SMS sending at 7:00 AM | |
scheduler.add_job( | |
func=send_daily_sms, | |
trigger="cron", | |
hour=7, | |
minute=0, | |
id='daily_sms' | |
) | |
scheduler.start() | |
logger.info("Background scheduler initialized") | |
if __name__ == '__main__': | |
with app.app_context(): | |
# Create database tables | |
db.create_all() | |
# Create default admin user | |
create_admin_user() | |
# Initialize services | |
initialize_services() | |
# Initialize scheduler | |
init_scheduler() | |
logger.info("Farm Management Portal started") | |
app.run(debug=True, host='0.0.0.0', port=5000) | |