|
from flask import Flask, render_template, request, Response |
|
import requests |
|
import datetime |
|
from twilio.rest import Client |
|
from geopy.geocoders import Photon |
|
from geopy.exc import GeocoderTimedOut, GeocoderServiceError |
|
|
|
app = Flask(__name__) |
|
|
|
|
|
photon_geolocator = Photon(user_agent="MyWeatherApp", timeout=10) |
|
|
|
def parse_iso_datetime(timestr): |
|
""" |
|
Parse an ISO8601 datetime string (removing any trailing 'Z'). |
|
""" |
|
if timestr.endswith("Z"): |
|
timestr = timestr[:-1] |
|
return datetime.datetime.fromisoformat(timestr) |
|
|
|
def find_closest_hour_index(hour_times, current_time_str): |
|
""" |
|
Find the index in hour_times that is closest to the current_time_str. |
|
""" |
|
if not hour_times: |
|
return None |
|
dt_current = parse_iso_datetime(current_time_str) |
|
min_diff = None |
|
best_index = None |
|
for i, ht in enumerate(hour_times): |
|
dt_ht = parse_iso_datetime(ht) |
|
diff = abs((dt_ht - dt_current).total_seconds()) |
|
if min_diff is None or diff < min_diff: |
|
min_diff = diff |
|
best_index = i |
|
return best_index |
|
|
|
def get_weather_icon(code): |
|
"""Map the Open-Meteo weathercode to an emoji icon.""" |
|
if code == 0: |
|
return "☀️" |
|
elif code in [1, 2, 3]: |
|
return "⛅" |
|
elif code in [45, 48]: |
|
return "🌫️" |
|
elif code in [51, 53, 55]: |
|
return "🌦️" |
|
elif code in [56, 57]: |
|
return "🌧️" |
|
elif code in [61, 63, 65]: |
|
return "🌧️" |
|
elif code in [66, 67]: |
|
return "🌧️" |
|
elif code in [71, 73, 75, 77]: |
|
return "❄️" |
|
elif code in [80, 81, 82]: |
|
return "🌦️" |
|
elif code in [85, 86]: |
|
return "❄️" |
|
elif code in [95, 96, 99]: |
|
return "⛈️" |
|
else: |
|
return "❓" |
|
|
|
def get_weather_description(code): |
|
"""Short textual description for the weathercode.""" |
|
descriptions = { |
|
0: "Clear sky", |
|
1: "Mainly clear", |
|
2: "Partly cloudy", |
|
3: "Overcast", |
|
45: "Fog", |
|
48: "Depositing rime fog", |
|
51: "Light drizzle", |
|
53: "Moderate drizzle", |
|
55: "Dense drizzle", |
|
56: "Freezing drizzle", |
|
57: "Freezing drizzle", |
|
61: "Slight rain", |
|
63: "Moderate rain", |
|
65: "Heavy rain", |
|
66: "Freezing rain", |
|
67: "Freezing rain", |
|
71: "Slight snow fall", |
|
73: "Moderate snow fall", |
|
75: "Heavy snow fall", |
|
77: "Snow grains", |
|
80: "Slight rain showers", |
|
81: "Moderate rain showers", |
|
82: "Violent rain showers", |
|
85: "Slight snow showers", |
|
86: "Heavy snow showers", |
|
95: "Thunderstorm", |
|
96: "Thunderstorm w/ slight hail", |
|
99: "Thunderstorm w/ heavy hail" |
|
} |
|
return descriptions.get(code, "Unknown") |
|
|
|
def reverse_geocode(lat, lon): |
|
""" |
|
Use Photon (via geopy) to convert latitude and longitude into a human-readable address. |
|
If the geocoding fails, returns a fallback string with the coordinates. |
|
""" |
|
try: |
|
location = photon_geolocator.reverse((lat, lon), exactly_one=True) |
|
if location: |
|
return location.address |
|
except (GeocoderTimedOut, GeocoderServiceError) as e: |
|
print("Photon reverse geocode error:", e) |
|
return f"Lat: {lat}, Lon: {lon}" |
|
|
|
|
|
|
|
|
|
def check_and_collect_alerts(forecast_list): |
|
""" |
|
Check the forecast for hazardous weather conditions and collect alert messages. |
|
""" |
|
alerts = [] |
|
for day in forecast_list: |
|
if day.get("tmax") and day.get("tmax") > 40: |
|
alerts.append(f"{day['day_name']} ({day['date_str']}): Extreme heat. Please take necessary precautions.") |
|
elif day.get("desc") and any(keyword in day['desc'].lower() for keyword in ["rain", "drizzle", "overcast", "thunderstorm"]): |
|
alerts.append(f"{day['day_name']} ({day['date_str']}): {day['desc']}. Please take necessary precautions.") |
|
return alerts |
|
|
|
def send_whatsapp_message(message, location, location_address): |
|
""" |
|
Send a WhatsApp message using Twilio API. |
|
""" |
|
google_maps_url = f"https://www.google.com/maps?q={location[0]},{location[1]}" |
|
message_content = ( |
|
f"Weather Alerts for Your Farm :\n" |
|
f"{message}\n" |
|
f"Location (lat, lon): {location[0]}, {location[1]}\n" |
|
f"Location: {location_address}\n" |
|
f"Map: {google_maps_url}" |
|
) |
|
account_sid = 'AC490e071f8d01bf0df2f03d086c788d87' |
|
auth_token = '224b23b950ad5a4052aba15893fdf083' |
|
client = Client(account_sid, auth_token) |
|
msg = client.messages.create( |
|
from_='whatsapp:+14155238886', |
|
body=message_content, |
|
to='whatsapp:+917559355282' |
|
) |
|
print("Twilio Message SID:", msg.sid) |
|
|
|
@app.route("/", methods=["GET", "POST"]) |
|
def index(): |
|
|
|
default_lat = 18.5196 |
|
default_lon = 73.8553 |
|
|
|
if request.method == "POST": |
|
try: |
|
lat = float(request.form.get("lat", default_lat)) |
|
lon = float(request.form.get("lon", default_lon)) |
|
except ValueError: |
|
lat, lon = default_lat, default_lon |
|
else: |
|
lat = float(request.args.get("lat", default_lat)) |
|
lon = float(request.args.get("lon", default_lon)) |
|
|
|
location_address = reverse_geocode(lat, lon) |
|
|
|
|
|
url = "https://api.open-meteo.com/v1/forecast" |
|
params = { |
|
"latitude": lat, |
|
"longitude": lon, |
|
"hourly": ( |
|
"temperature_2m,relative_humidity_2m,precipitation," |
|
"cloudcover,windspeed_10m,pressure_msl,soil_moisture_3_to_9cm,uv_index" |
|
), |
|
"daily": ( |
|
"weathercode,temperature_2m_max,temperature_2m_min," |
|
"sunrise,sunset,uv_index_max" |
|
), |
|
"current_weather": True, |
|
"forecast_days": 14, |
|
"timezone": "auto" |
|
} |
|
resp = requests.get(url, params=params) |
|
data = resp.json() |
|
|
|
timezone = data.get("timezone", "Local") |
|
current_weather = data.get("current_weather", {}) |
|
current_temp = current_weather.get("temperature") |
|
current_time = current_weather.get("time") |
|
current_code = current_weather.get("weathercode") |
|
current_icon = get_weather_icon(current_code) |
|
current_desc = get_weather_description(current_code) |
|
current_wind_speed = current_weather.get("windspeed", 0.0) |
|
current_wind_dir = current_weather.get("winddirection", 0) |
|
|
|
if current_time: |
|
dt_current = parse_iso_datetime(current_time) |
|
current_time_formatted = dt_current.strftime("%A, %b %d, %Y %I:%M %p") |
|
else: |
|
current_time_formatted = "" |
|
|
|
hourly_data = data.get("hourly", {}) |
|
hour_times = hourly_data.get("time", []) |
|
hour_temp = hourly_data.get("temperature_2m", []) |
|
hour_humidity = hourly_data.get("relative_humidity_2m", []) |
|
hour_precip = hourly_data.get("precipitation", []) |
|
hour_clouds = hourly_data.get("cloudcover", []) |
|
hour_wind = hourly_data.get("windspeed_10m", []) |
|
hour_pressure = hourly_data.get("pressure_msl", []) |
|
hour_soil = hourly_data.get("soil_moisture_3_to_9cm", []) |
|
hour_uv = hourly_data.get("uv_index", []) |
|
|
|
current_index = None |
|
if current_time: |
|
current_index = find_closest_hour_index(hour_times, current_time) |
|
|
|
feels_like = current_temp |
|
if current_index is not None and current_index < len(hour_humidity): |
|
h = hour_humidity[current_index] |
|
feels_like = round(current_temp - 0.2 * (100 - h) / 10, 1) |
|
|
|
today_highlights = {} |
|
if current_index is not None: |
|
today_highlights["humidity"] = hour_humidity[current_index] if current_index < len(hour_humidity) else None |
|
today_highlights["precipitation"] = hour_precip[current_index] if current_index < len(hour_precip) else None |
|
today_highlights["clouds"] = hour_clouds[current_index] if current_index < len(hour_clouds) else None |
|
today_highlights["windspeed"] = hour_wind[current_index] if current_index < len(hour_wind) else None |
|
today_highlights["pressure"] = hour_pressure[current_index] if current_index < len(hour_pressure) else None |
|
today_highlights["soil_moisture"] = hour_soil[current_index] if current_index < len(hour_soil) else None |
|
today_highlights["uv_index"] = hour_uv[current_index] if current_index < len(hour_uv) else None |
|
else: |
|
for k in ["humidity", "precipitation", "cloudcover", "windspeed", "pressure", "soil_moisture", "uv_index"]: |
|
today_highlights[k] = None |
|
|
|
daily_data = data.get("daily", {}) |
|
daily_sunrise = daily_data.get("sunrise", []) |
|
daily_sunset = daily_data.get("sunset", []) |
|
if len(daily_sunrise) > 0: |
|
today_highlights["sunrise"] = daily_sunrise[0][11:16] |
|
else: |
|
today_highlights["sunrise"] = None |
|
if len(daily_sunset) > 0: |
|
today_highlights["sunset"] = daily_sunset[0][11:16] |
|
else: |
|
today_highlights["sunset"] = None |
|
|
|
daily_times = daily_data.get("time", []) |
|
daily_codes = daily_data.get("weathercode", []) |
|
daily_tmax = daily_data.get("temperature_2m_max", []) |
|
daily_tmin = daily_data.get("temperature_2m_min", []) |
|
forecast_list = [] |
|
|
|
def get_hour_temp(date_str, hour_str): |
|
target = date_str + "T" + hour_str + ":00" |
|
best_idx = None |
|
best_diff = None |
|
dt_target = parse_iso_datetime(target) |
|
for i, ht in enumerate(hour_times): |
|
dt_ht = parse_iso_datetime(ht) |
|
diff = abs((dt_ht - dt_target).total_seconds()) |
|
if best_diff is None or diff < best_diff: |
|
best_diff = diff |
|
best_idx = i |
|
if best_idx is not None and best_idx < len(hour_temp): |
|
return hour_temp[best_idx] |
|
return None |
|
|
|
for i in range(len(daily_times)): |
|
date_str = daily_times[i] |
|
dt_obj = parse_iso_datetime(date_str) |
|
day_name = dt_obj.strftime("%A") |
|
short_date = dt_obj.strftime("%b %d") |
|
|
|
code = daily_codes[i] if i < len(daily_codes) else None |
|
icon = get_weather_icon(code) |
|
desc = get_weather_description(code) |
|
|
|
tmax = daily_tmax[i] if i < len(daily_tmax) else None |
|
tmin = daily_tmin[i] if i < len(daily_tmin) else None |
|
avg_temp = round((tmax + tmin) / 2, 1) if tmax is not None and tmin is not None else None |
|
|
|
morning_temp = get_hour_temp(date_str, "09") |
|
evening_temp = get_hour_temp(date_str, "21") |
|
|
|
sr = daily_sunrise[i][11:16] if i < len(daily_sunrise) else None |
|
ss = daily_sunset[i][11:16] if i < len(daily_sunset) else None |
|
|
|
forecast_list.append({ |
|
"day_name": day_name, |
|
"date_str": short_date, |
|
"icon": icon, |
|
"desc": desc, |
|
"avg_temp": avg_temp, |
|
"morning_temp": morning_temp, |
|
"evening_temp": evening_temp, |
|
"sunrise": sr, |
|
"sunset": ss, |
|
"tmax": tmax, |
|
"tmin": tmin |
|
}) |
|
|
|
alerts = check_and_collect_alerts(forecast_list) |
|
if alerts: |
|
alert_message = "\n".join(alerts) |
|
send_whatsapp_message(alert_message, (lat, lon), location_address) |
|
alerts_sent = True |
|
else: |
|
alerts_sent = False |
|
|
|
return render_template( |
|
"index.html", |
|
lat=lat, |
|
lon=lon, |
|
location_address=location_address, |
|
current_temp=current_temp, |
|
current_icon=current_icon, |
|
current_desc=current_desc, |
|
current_time=current_time_formatted, |
|
current_wind_speed=current_wind_speed, |
|
current_wind_dir=current_wind_dir, |
|
feels_like=feels_like, |
|
today_highlights=today_highlights, |
|
forecast_list=forecast_list, |
|
timezone=timezone, |
|
alerts_sent=alerts_sent |
|
) |
|
|
|
if __name__ == "__main__": |
|
app.run(debug=True) |