Why it helps
Built for quick decisions when weather conditions change.
+
+ Step 2
+ Enter city, state, and country
diff --git a/Frontend/script.js b/Frontend/script.js
index dbb5097..6826c05 100644
--- a/Frontend/script.js
+++ b/Frontend/script.js
@@ -243,6 +243,57 @@ window.onload = function () {
).join("
");
};
+const SMS_API_URL =
+ window.location.hostname === "127.0.0.1" ||
+ window.location.hostname === "localhost"
+ ? "http://127.0.0.1:5000/subscribe-alert"
+ : window.location.origin + "/subscribe-alert";
+
+// Sending Alert Function
+async function enableSmsAlerts() {
+
+ const city = document.getElementById("sms-city").value.trim();
+ const phone = document.getElementById("sms-phone").value.trim();
+ const status = document.getElementById("sms-status");
+
+ if (!city || !phone) {
+ status.innerHTML = "Please enter city and phone number.";
+ return;
+ }
+
+ try {
+ status.innerHTML = "Registering...";
+
+ const response = await fetch(
+ SMS_API_URL,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type":
+ "application/json"
+ },
+ body: JSON.stringify({
+ city,
+ phone
+ })
+ }
+ );
+
+ const data = await response.json();
+
+ if (data.success) {
+ status.innerHTML = "SMS alerts enabled successfully.";
+
+ } else {
+ status.innerHTML = data.message || "Failed to subscribe.";
+ }
+
+ } catch (error) {
+
+ console.error(error);
+ status.innerHTML = "Server error.";
+ }
+}
const scrollTopBtn = document.getElementById("scrollTopBtn");
if (scrollTopBtn) {
@@ -262,3 +313,52 @@ if (scrollTopBtn) {
});
}
};
+
+// Sending Alert Function
+const SMS_API_URL = "http://localhost:5000/subscribe-alert";
+
+async function enableSmsAlerts() {
+
+ const city = document.getElementById("sms-city").value.trim();
+ const phone = document.getElementById("sms-phone").value.trim();
+ const status = document.getElementById("sms-status");
+
+ if (!city || !phone) {
+ status.innerHTML = "Please enter city and phone number.";
+ return;
+ }
+
+ try {
+ status.innerHTML = "Registering...";
+
+ const response = await fetch(
+ SMS_API_URL,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type":
+ "application/json"
+ },
+ body: JSON.stringify({
+ city,
+ phone
+ })
+ }
+ );
+
+ const data = await response.json();
+
+ if (response.ok && data.success) {
+ status.innerHTML = "SMS alerts enabled successfully.";
+
+ } else {
+ status.innerHTML = data.message || data.subscription?.message || "Failed to subscribe.";
+ }
+
+ } catch (error) {
+
+ console.error(error);
+ status.innerHTML = "Server error.";
+ }
+}
+
diff --git a/Frontend/style.css b/Frontend/style.css
index 8b5cb06..c96dccf 100644
--- a/Frontend/style.css
+++ b/Frontend/style.css
@@ -951,3 +951,68 @@ body::before {
[data-theme="light"] .footer-links a:hover {
color: #0369a1;
}
+
+/* SMS Alert Feature */
+.glass-card {
+ padding: 24px;
+ border-radius: 18px;
+ background: var(--panel);
+ border: 1px solid var(--border-color);
+ backdrop-filter: blur(12px);
+ box-shadow: var(--shadow);
+}
+
+.input-container {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 16px;
+ margin: 16px 0;
+}
+
+.input-group {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.input-group label {
+ font-size: 0.85rem;
+ color: var(--muted);
+ font-weight: 600;
+}
+
+.input-group input {
+ padding: 10px 12px;
+ border-radius: 10px;
+ border: 1px solid var(--border-strong);
+ background: var(--input-bg);
+ color: var(--text-primary);
+ outline: none;
+ width: 100%;
+}
+
+.sms-alert-section button {
+ width: 100%;
+ display: block;
+ margin-top: 8px;
+ padding: 12px 24px;
+ border-radius: 999px;
+ border: none;
+ background: linear-gradient(135deg, var(--accent), var(--accent-2));
+ color: white;
+ font-weight: 600;
+ cursor: pointer;
+ transition: transform 0.2s ease;
+}
+
+.sms-alert-section button:hover {
+ transform: translateY(-2px);
+}
+
+.sms-alert-section #sms-status {
+ text-align: center;
+ color: #7dd3fc;
+ width: 100%;
+ display: block;
+ font-weight: bold;
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index a67758a..ef417fd 100644
--- a/README.md
+++ b/README.md
@@ -97,6 +97,14 @@ Climate Shield includes an integrated AI chatbot that provides:
The chatbot is lightweight and rule-based.
+## Automatic SMS Alert
+- **OpenWeatherMap-powered forecasts** — fetches real-time weather forecast data (rainfall, wind speed, humidity, temperature, and storm conditions) for each subscriber's city using the OpenWeatherMap API.
+
+- **Automatic severe weather detection** — analyzes the forecast against defined thresholds to detect heavy rain, strong winds, flood risk, heatwaves, and thunderstorms.
+
+- **Instant SMS notifications via Vonage** — when a severe condition is detected, an SMS alert is sent directly to the subscriber's phone with the relevant weather details.
+
+- **Simple subscription with automated scheduling** — users subscribe once with their city and phone number, and APScheduler periodically rechecks OpenWeatherMap data to send alerts automatically.
---
# 🖥 Frontend
diff --git a/backend/alertsystem.py b/backend/alertsystem.py
index be9636d..e945634 100644
--- a/backend/alertsystem.py
+++ b/backend/alertsystem.py
@@ -4,8 +4,15 @@
from dotenv import load_dotenv
+from flask_cors import CORS
+
+from sms_alert import save_subscriber,send_weather_alert,check_weather_and_send_alerts
+from apscheduler.schedulers.background import BackgroundScheduler
+
load_dotenv()
+
+
GIS_ALERTS_URL = os.environ.get("GIS_ALERTS_URL", "https://example.com/gis/alerts")
def fetch_gis_alert_data():
@@ -46,6 +53,9 @@ def fetch_gis_alert_data():
from flask_cors import CORS
+from sms_alert import save_subscriber,send_weather_alert,check_weather_and_send_alerts
+from apscheduler.schedulers.background import BackgroundScheduler
+
# =========================================================
# APP CONFIG
# =========================================================
@@ -600,6 +610,46 @@ def chatbot():
"message":
"Chatbot unavailable."
})
+
+# ============================================
+# SENDING ALERT TO THE SUBSCRIBER
+# ============================================
+@app.route("/subscribe-alert", methods=["POST"])
+def subscribe_alerts():
+
+ data = request.get_json()
+
+ city = data.get("city", "").strip()
+ phone = data.get("phone", "").strip()
+
+ print("Received:", city, phone)
+
+ result = save_subscriber(city, phone)
+
+ if not result["success"]:
+ return jsonify(result), 409
+
+ sms_result = send_weather_alert(city, phone)
+
+ print("SMS RESULT:", sms_result)
+
+ return jsonify({
+ "subscription": result,
+ "sms": sms_result})
+
+
+scheduler = BackgroundScheduler()
+
+scheduler.add_job(
+ func=check_weather_and_send_alerts,
+ trigger="interval",
+ minutes=1 # for testing : every one minute sms alert will send to registered
+)
+
+scheduler.start()
+
+print("Weather alert scheduler started.")
+
# =========================================================
# LOCAL RUN
diff --git a/backend/sms_alert.py b/backend/sms_alert.py
new file mode 100644
index 0000000..9f95c21
--- /dev/null
+++ b/backend/sms_alert.py
@@ -0,0 +1,143 @@
+import requests
+import os
+import vonage
+import json
+
+API_KEY = os.getenv("OPENWEATHER_API_KEY")
+
+client = vonage.Client(
+ key=os.getenv("VONAGE_API_KEY"),
+ secret=os.getenv("VONAGE_API_SECRET")
+)
+
+vonage_sms = vonage.Sms(client)
+
+SMS_SUBSCRIBERS_FILE = "subscribers.json"
+
+
+def load_subscribers():
+ if not os.path.exists(SMS_SUBSCRIBERS_FILE):
+ return []
+
+ try:
+ with open(SMS_SUBSCRIBERS_FILE, "r", encoding="utf-8") as file:
+ return json.load(file)
+ except (json.JSONDecodeError, IOError):
+ return []
+
+def save_subscriber(city, phone):
+
+ print("Saving subscriber...")
+ print("Current folder:", os.getcwd())
+
+ subscribers = load_subscribers()
+ city = city.strip()
+ phone = phone.strip()
+
+ for subscriber in subscribers:
+ if subscriber["phone"] == phone:
+ return {
+ "success": False,
+ "message": "This phone number is already subscribed for this city."}
+
+ subscribers.append({"city": city,"phone": phone})
+
+ with open(SMS_SUBSCRIBERS_FILE, "w", encoding="utf-8") as file:
+ json.dump(subscribers, file, indent=4)
+
+ print("Subscriber saved successfully")
+
+ return {"success": True,"message":"SMS enabled successfully"}
+
+def send_weather_alert(city, phone):
+
+ forecast_response = requests.get(
+ "https://api.openweathermap.org/data/2.5/forecast",
+ params={
+ "q": city,
+ "appid": API_KEY,
+ "units": "metric"
+ },
+ timeout=15
+ )
+
+ forecast_response.raise_for_status()
+ forecast_data = forecast_response.json()
+ alerts = []
+
+ first_forecast = forecast_data["list"][0]
+ temperature = first_forecast["main"]["temp"]
+ humidity = first_forecast["main"]["humidity"]
+ weather = first_forecast["weather"][0]["main"]
+ rainfall = first_forecast.get("rain",{}).get("3h",0)
+ wind_speed = round(first_forecast["wind"]["speed"] * 3.6,1)
+
+ if rainfall >= 10:
+ alerts.append("Heavy Rain Warning")
+
+ if wind_speed >= 30:
+ alerts.append("Strong Wind Warning")
+
+ if humidity >= 85 and rainfall >= 5:
+ alerts.append("Potential Flood Risk")
+
+ if temperature >= 35:
+ alerts.append("Heatwave Alert")
+
+ if "thunderstorm" in weather.lower():
+ alerts.append("Thunderstorm Alert")
+
+ alerts = list(set(alerts))
+
+ if not alerts:
+ return {
+ "success": True,
+ "alerts": [],
+ "message":
+ "No severe weather conditions detected."
+ }
+
+ sms_text = (
+ "Climate Shield Alert\n\n"
+ f"Location: {city}\n"
+ f"Temperature: {temperature}°C\n"
+ f"Humidity: {humidity}%\n"
+ f"Rainfall: {rainfall} mm\n"
+ f"Wind Speed: {wind_speed} km/h\n\n"
+ "Alerts:\n"
+ + "\n".join(
+ f"- {alert}"
+ for alert in alerts
+ )
+ )
+
+ response = vonage_sms.send_message({
+ "from": "ClimateShield",
+ "to": phone,
+ "text": sms_text
+ })
+
+ print("SMS Response:")
+ print(response)
+
+ return {
+ "success": True,
+ "alerts": alerts,
+ "sms_response": response
+ }
+
+def check_weather_and_send_alerts():
+
+ subscribers = load_subscribers()
+ print(f"Checking weather for {len(subscribers)} subscribers...")
+
+ for subscriber in subscribers:
+ city = subscriber["city"]
+ phone = subscriber["phone"]
+
+ try:
+ result = send_weather_alert(city,phone)
+ print(f"Checked alerts for {city}:",result)
+
+ except Exception as e:
+ print(f"Failed for {city}: {e}")
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 913fefa..475dba0 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,4 +3,7 @@ flask-cors
requests
gunicorn
python-dotenv
-pytest
\ No newline at end of file
+pytest
+apscheduler
+vonage==2.5.5
+