pranit144 commited on
Commit
73f907e
·
verified ·
1 Parent(s): 1672e0a

Upload 4 files

Browse files
Files changed (4) hide show
  1. app.py +151 -0
  2. requirements.txt +9 -0
  3. templates/index.html +262 -0
  4. templates/predict.html +132 -0
app.py ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, request, jsonify
2
+ import requests
3
+ import google.generativeai as genai
4
+ import os
5
+ import json
6
+
7
+ app = Flask(__name__)
8
+
9
+ # Mapping of SoilGrids parameter codes
10
+ PARAM_MAP = {
11
+ "bdod": "Bulk Density", "cec": "Cation Exchange Capacity", "cfvo": "Coarse Fragment Volume",
12
+ "clay": "Clay Content", "nitrogen": "Nitrogen Content", "ocd": "Organic Carbon Density",
13
+ "ocs": "Organic Carbon Stock", "phh2o": "Soil pH", "sand": "Sand Content",
14
+ "silt": "Silt Content", "soc": "Soil Organic Carbon", "wv0010": "Water Content (0-10cm)",
15
+ "wv0033": "Water Content (0-33cm)", "wv1500": "Water Content (1500mm)"
16
+ }
17
+
18
+ @app.route('/')
19
+ def index():
20
+ return render_template('index.html')
21
+
22
+ @app.route('/get_soil_report', methods=['POST'])
23
+ def get_soil_report():
24
+ data = request.get_json()
25
+ lat, lon = data.get("lat"), data.get("lon")
26
+ if not lat or not lon:
27
+ return jsonify({"error": "Latitude and Longitude are required"}), 400
28
+
29
+ headers = {"accept": "application/json"}
30
+
31
+ # Fetch Soil Classification
32
+ try:
33
+ class_response = requests.get(
34
+ "https://rest.isric.org/soilgrids/v2.0/classification/query",
35
+ params={"lon": lon, "lat": lat, "number_classes": 5},
36
+ headers=headers, timeout=15
37
+ )
38
+ class_response.raise_for_status()
39
+ class_data = class_response.json()
40
+ except requests.exceptions.RequestException as e:
41
+ return jsonify({"error": f"Failed to fetch soil classification: {e}"}), 500
42
+
43
+ soil_classification = {
44
+ "soil_type": class_data.get("wrb_class_name", "Unknown"),
45
+ "probabilities": class_data.get("wrb_class_probability", [])
46
+ }
47
+
48
+ # Fetch Soil Properties
49
+ try:
50
+ prop_response = requests.get(
51
+ "https://rest.isric.org/soilgrids/v2.0/properties/query",
52
+ params={
53
+ "lon": lon, "lat": lat,
54
+ "property": list(PARAM_MAP.keys()),
55
+ "depth": "5-15cm", "value": "mean"
56
+ },
57
+ headers=headers, timeout=15
58
+ )
59
+ prop_response.raise_for_status()
60
+ prop_data = prop_response.json()
61
+ except requests.exceptions.RequestException as e:
62
+ return jsonify({"error": f"Failed to fetch soil properties: {e}"}), 500
63
+
64
+ properties_list = []
65
+ layers = prop_data.get("properties", {}).get("layers", [])
66
+ for layer in layers:
67
+ param_code = layer.get("name")
68
+ name = PARAM_MAP.get(param_code, param_code.upper())
69
+ depth_info = layer.get("depths", [{}])[0]
70
+ value = depth_info.get("values", {}).get("mean")
71
+ unit = layer.get("unit_measure", {}).get("mapped_units", "")
72
+
73
+ if value is not None:
74
+ if param_code == "phh2o":
75
+ value /= 10.0
76
+ unit = "pH"
77
+ elif param_code in ["wv0010", "wv0033", "wv1500"]:
78
+ value /= 100.0
79
+ unit = "cm³/cm³"
80
+
81
+ properties_list.append({"parameter": name, "value": value, "unit": unit})
82
+
83
+ return jsonify({"classification": soil_classification, "properties": properties_list})
84
+
85
+ @app.route('/analyze_soil', methods=['POST'])
86
+ def analyze_soil():
87
+ api_key = os.getenv("GEMINI_API")
88
+ if not api_key:
89
+ error_msg = "API key not configured. The server administrator must set the GEMINI_API environment variable."
90
+ return jsonify({"error": error_msg}), 500
91
+
92
+ data = request.get_json()
93
+ soil_report = data.get("soil_report")
94
+ language = data.get("language", "English")
95
+
96
+ if not soil_report:
97
+ return jsonify({"error": "Soil report data is missing"}), 400
98
+
99
+ prompt = f"""
100
+ Analyze the following soil report and provide recommendations.
101
+ The response MUST be a single, valid JSON object, without any markdown formatting or surrounding text.
102
+ The user wants the analysis in this language: {language}.
103
+ Soil Report Data: {json.dumps(soil_report, indent=2)}
104
+ JSON Structure to follow:
105
+ {{
106
+ "soilType": "The primary soil type from the report",
107
+ "generalInsights": ["Insight 1", "Insight 2", "Insight 3"],
108
+ "parameters": [{{"parameter": "Parameter Name", "value": "Value with Unit", "range": "Low/Normal/High", "comment": "Brief comment."}}],
109
+ "cropRecommendations": [{{"crop": "Crop Name", "reason": "Brief reason."}}],
110
+ "managementRecommendations": {{"fertilization": "Recommendation.", "irrigation": "Recommendation."}}
111
+ }}
112
+ """
113
+
114
+ try:
115
+ genai.configure(api_key=api_key)
116
+
117
+ # --- NEW: Fallback Logic Implementation ---
118
+ models_to_try = ['gemini-2.5-flash', 'gemini-2.0-flash', 'gemini-1.5-flash']
119
+ analysis_json = None
120
+ last_error = None
121
+
122
+ for model_name in models_to_try:
123
+ try:
124
+ print(f"Attempting to use model: {model_name}")
125
+ model = genai.GenerativeModel(model_name)
126
+ response = model.generate_content(prompt)
127
+
128
+ cleaned_response = response.text.strip().replace("```json", "").replace("```", "")
129
+ analysis_json = json.loads(cleaned_response)
130
+
131
+ print(f"Successfully generated content with {model_name}")
132
+ break # Exit the loop on success
133
+
134
+ except Exception as e:
135
+ print(f"Model {model_name} failed: {e}")
136
+ last_error = e
137
+ continue # Try the next model in the list
138
+
139
+ if not analysis_json:
140
+ # This block is reached only if all models in the loop failed.
141
+ raise Exception("All specified AI models failed to generate a response.") from last_error
142
+
143
+ return jsonify(analysis_json)
144
+
145
+ except Exception as e:
146
+ # This catches the final error if all models fail, or any other setup error.
147
+ print(f"Error during Gemini API processing: {e}")
148
+ return jsonify({"error": f"Failed to get analysis from AI models: {e}"}), 500
149
+
150
+ if __name__ == '__main__':
151
+ app.run(debug=True, port=7860)
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ gunicorn
2
+ flask
3
+ scikit-learn==1.5.2
4
+ tensorflow==2.17.0
5
+ numpy
6
+ requests
7
+ pillow
8
+ pandas
9
+ joblib==1.4.2
templates/index.html ADDED
@@ -0,0 +1,262 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Soil Analysis Dashboard</title>
7
+ <!-- Bootstrap CSS -->
8
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
9
+ <!-- Leaflet CSS -->
10
+ <link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />
11
+ <style>
12
+ body { background-color: #f4f8f4; }
13
+ .navbar-brand { font-weight: bold; }
14
+ #map { height: 400px; border-radius: 0.5rem; border: 1px solid #ddd; }
15
+ .card { border: 1px solid #28a745; }
16
+ .btn-success { background-color: #28a745; border-color: #28a745; }
17
+ .btn-success:hover { background-color: #218838; border-color: #1e7e34; }
18
+ .loader { display: none; }
19
+ </style>
20
+ </head>
21
+ <body>
22
+ <nav class="navbar navbar-dark bg-success">
23
+ <div class="container-fluid">
24
+ <span class="navbar-brand mb-0 h1 mx-auto">🌱 Soil Analysis Dashboard</span>
25
+ </div>
26
+ </nav>
27
+
28
+ <div class="container my-4">
29
+ <!-- Step 1: Location -->
30
+ <div class="card mb-4">
31
+ <div class="card-header fw-bold">Step 1: Select Location</div>
32
+ <div class="card-body">
33
+ <div class="row">
34
+ <div class="col-lg-8 mb-3 mb-lg-0">
35
+ <div id="map"></div>
36
+ </div>
37
+ <div class="col-lg-4">
38
+ <div class="mb-3">
39
+ <label for="lat" class="form-label">Latitude</label>
40
+ <input type="text" id="lat" class="form-control" placeholder="Click map or enter manually">
41
+ </div>
42
+ <div class="mb-3">
43
+ <label for="lon" class="form-label">Longitude</label>
44
+ <input type="text" id="lon" class="form-control" placeholder="Click map or enter manually">
45
+ </div>
46
+ <button id="current-location" class="btn btn-secondary w-100 mb-2">Use Current Location</button>
47
+ <button id="fetch-report" class="btn btn-success w-100">
48
+ <span class="spinner-border spinner-border-sm loader" role="status" aria-hidden="true"></span>
49
+ Fetch Soil Report
50
+ </button>
51
+ </div>
52
+ </div>
53
+ </div>
54
+ </div>
55
+
56
+ <!-- Alert for errors -->
57
+ <div id="alert-container" class="mt-3"></div>
58
+
59
+ <!-- Step 2: Soil Report (Initially Hidden) -->
60
+ <div id="soil-report-section" class="d-none">
61
+ <div class="card mb-4">
62
+ <div class="card-header fw-bold">Step 2: Soil Report</div>
63
+ <div class="card-body">
64
+ <div class="row">
65
+ <div class="col-md-6" id="classification-report"></div>
66
+ <div class="col-md-6" id="properties-report"></div>
67
+ </div>
68
+ </div>
69
+ </div>
70
+ </div>
71
+
72
+ <!-- Step 3: Analysis (Initially Hidden) -->
73
+ <div id="analysis-section" class="d-none">
74
+ <div class="card mb-4">
75
+ <div class="card-header fw-bold">Step 3: AI Analysis & Recommendations</div>
76
+ <div class="card-body">
77
+ <div class="row align-items-end">
78
+ <div class="col-md-8 mb-3 mb-md-0">
79
+ <label for="language" class="form-label">Response Language</label>
80
+ <select class="form-select" id="language">
81
+ <option value="English">English</option><option value="Hindi">Hindi</option><option value="Bengali">Bengali</option>
82
+ <option value="Telugu">Telugu</option><option value="Marathi">Marathi</option><option value="Tamil">Tamil</option>
83
+ <option value="Gujarati">Gujarati</option><option value="Urdu">Urdu</option><option value="Kannada">Kannada</option>
84
+ <option value="Odia">Odia</option><option value="Malayalam">Malayalam</option>
85
+ </select>
86
+ </div>
87
+ <div class="col-md-4">
88
+ <button id="analyze-soil" class="btn btn-success w-100">
89
+ <span class="spinner-border spinner-border-sm loader" role="status" aria-hidden="true"></span>
90
+ Analyze with AI
91
+ </button>
92
+ </div>
93
+ </div>
94
+ <hr>
95
+ <div id="analysis-result"></div>
96
+ </div>
97
+ </div>
98
+ </div>
99
+ </div>
100
+
101
+ <!-- Bootstrap & Leaflet JS -->
102
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
103
+ <script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
104
+ <script>
105
+ // --- Global State ---
106
+ let soilReportData = null;
107
+ let map, marker;
108
+
109
+ // --- UI Helper Functions ---
110
+ const showAlert = (message, type = 'danger') => {
111
+ const alertContainer = document.getElementById('alert-container');
112
+ alertContainer.innerHTML = `<div class="alert alert-${type} alert-dismissible fade show" role="alert">
113
+ ${message}
114
+ <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
115
+ </div>`;
116
+ };
117
+
118
+ const toggleLoader = (buttonId, show) => {
119
+ const button = document.getElementById(buttonId);
120
+ const loader = button.querySelector('.loader');
121
+ button.disabled = show;
122
+ loader.style.display = show ? 'inline-block' : 'none';
123
+ };
124
+
125
+ // --- Map Initialization ---
126
+ const initializeMap = () => {
127
+ map = L.map('map').setView([20.5937, 78.9629], 5);
128
+ L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
129
+ attribution: '&copy; OpenStreetMap contributors'
130
+ }).addTo(map);
131
+
132
+ map.on('click', e => {
133
+ const { lat, lng } = e.latlng;
134
+ document.getElementById('lat').value = lat.toFixed(6);
135
+ document.getElementById('lon').value = lng.toFixed(6);
136
+ if (marker) marker.setLatLng(e.latlng);
137
+ else marker = L.marker(e.latlng).addTo(map);
138
+ });
139
+ };
140
+
141
+ // --- Event Listeners ---
142
+ document.getElementById('current-location').addEventListener('click', () => {
143
+ if (navigator.geolocation) {
144
+ navigator.geolocation.getCurrentPosition(pos => {
145
+ const { latitude, longitude } = pos.coords;
146
+ document.getElementById('lat').value = latitude.toFixed(6);
147
+ document.getElementById('lon').value = longitude.toFixed(6);
148
+ const latlng = L.latLng(latitude, longitude);
149
+ map.setView(latlng, 13);
150
+ if (marker) marker.setLatLng(latlng);
151
+ else marker = L.marker(latlng).addTo(map);
152
+ }, () => showAlert('Could not get your location.'));
153
+ } else {
154
+ showAlert('Geolocation is not supported by your browser.');
155
+ }
156
+ });
157
+
158
+ document.getElementById('fetch-report').addEventListener('click', () => {
159
+ const lat = document.getElementById('lat').value;
160
+ const lon = document.getElementById('lon').value;
161
+ if (!lat || !lon) return showAlert('Please provide latitude and longitude.');
162
+
163
+ toggleLoader('fetch-report', true);
164
+ fetch('/get_soil_report', {
165
+ method: 'POST',
166
+ headers: { 'Content-Type': 'application/json' },
167
+ body: JSON.stringify({ lat, lon })
168
+ })
169
+ .then(response => response.json().then(data => ({ ok: response.ok, data })))
170
+ .then(({ ok, data }) => {
171
+ if (!ok) throw new Error(data.error || 'Unknown error occurred.');
172
+ soilReportData = data;
173
+ renderSoilReport(data);
174
+ document.getElementById('soil-report-section').classList.remove('d-none');
175
+ document.getElementById('analysis-section').classList.remove('d-none');
176
+ })
177
+ .catch(err => showAlert(`Error fetching soil report: ${err.message}`))
178
+ .finally(() => toggleLoader('fetch-report', false));
179
+ });
180
+
181
+ document.getElementById('analyze-soil').addEventListener('click', () => {
182
+ if (!soilReportData) return showAlert('Please fetch a soil report first.');
183
+
184
+ toggleLoader('analyze-soil', true);
185
+ document.getElementById('analysis-result').innerHTML = '';
186
+
187
+ // The Authorization header is no longer sent from the frontend
188
+ fetch('/analyze_soil', {
189
+ method: 'POST',
190
+ headers: { 'Content-Type': 'application/json' },
191
+ body: JSON.stringify({
192
+ soil_report: soilReportData,
193
+ language: document.getElementById('language').value
194
+ })
195
+ })
196
+ .then(response => response.json().then(data => ({ ok: response.ok, data })))
197
+ .then(({ ok, data }) => {
198
+ if (!ok) throw new Error(data.error || 'Failed to get analysis.');
199
+ renderAnalysis(data);
200
+ })
201
+ .catch(err => showAlert(`Error during analysis: ${err.message}`))
202
+ .finally(() => toggleLoader('analyze-soil', false));
203
+ });
204
+
205
+ // --- Rendering Functions (No change here) ---
206
+ const renderSoilReport = (data) => {
207
+ let classHtml = `<h5>Soil Classification</h5><p><strong>Type:</strong> ${data.classification.soil_type}</p>`;
208
+ if (data.classification.probabilities.length) {
209
+ classHtml += `<table class="table table-sm table-bordered"><thead><tr><th>Type</th><th>Probability</th></tr></thead><tbody>`;
210
+ data.classification.probabilities.forEach(([type, prob]) => {
211
+ classHtml += `<tr><td>${type}</td><td>${prob}%</td></tr>`;
212
+ });
213
+ classHtml += `</tbody></table>`;
214
+ }
215
+ document.getElementById('classification-report').innerHTML = classHtml;
216
+
217
+ let propHtml = `<h5>Soil Properties (5-15cm)</h5><table class="table table-sm table-striped"><thead><tr><th>Parameter</th><th>Value</th></tr></thead><tbody>`;
218
+ data.properties.forEach(({ parameter, value, unit }) => {
219
+ const displayValue = (typeof value === 'number') ? `${value.toFixed(2)} ${unit}` : 'N/A';
220
+ propHtml += `<tr><td>${parameter}</td><td>${displayValue}</td></tr>`;
221
+ });
222
+ propHtml += `</tbody></table>`;
223
+ document.getElementById('properties-report').innerHTML = propHtml;
224
+ };
225
+
226
+ const renderAnalysis = (data) => {
227
+ let analysisHtml = `
228
+ <div class="mb-4">
229
+ <h4 class="text-success">${data.soilType}</h4>
230
+ <ul class="list-unstyled">
231
+ ${data.generalInsights.map(insight => `<li>- ${insight}</li>`).join('')}
232
+ </ul>
233
+ </div>
234
+
235
+ <h5 class="mt-4">Parameter Analysis</h5>
236
+ <table class="table table-bordered">
237
+ <thead class="table-light"><tr><th>Parameter</th><th>Value</th><th>Range</th><th>Comment</th></tr></thead>
238
+ <tbody>
239
+ ${data.parameters.map(p => `<tr><td>${p.parameter}</td><td>${p.value}</td><td>${p.range}</td><td>${p.comment}</td></tr>`).join('')}
240
+ </tbody>
241
+ </table>
242
+
243
+ <h5 class="mt-4">Crop Recommendations</h5>
244
+ <table class="table table-striped">
245
+ <thead class="table-light"><tr><th>Crop</th><th>Reason</th></tr></thead>
246
+ <tbody>
247
+ ${data.cropRecommendations.map(c => `<tr><td>${c.crop}</td><td>${c.reason}</td></tr>`).join('')}
248
+ </tbody>
249
+ </table>
250
+
251
+ <h5 class="mt-4">Management Recommendations</h5>
252
+ <p><strong>Fertilization:</strong> ${data.managementRecommendations.fertilization}</p>
253
+ <p><strong>Irrigation:</strong> ${data.managementRecommendations.irrigation}</p>
254
+ `;
255
+ document.getElementById('analysis-result').innerHTML = analysisHtml;
256
+ };
257
+
258
+ // --- Initialize App ---
259
+ document.addEventListener('DOMContentLoaded', initializeMap);
260
+ </script>
261
+ </body>
262
+ </html>
templates/predict.html ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Pump Prediction</title>
7
+ <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
8
+ <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
9
+ <style>
10
+ body { background-color: #f4f8f4; }
11
+ .card { border-radius: 15px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); }
12
+ .card-header { background-color: #28a745; color: white; font-weight: bold; text-align: center; font-size: 1.5rem; }
13
+ .status-card { text-align: center; }
14
+ .status-label { font-size: 1.2rem; color: #555; }
15
+ .status-text { font-size: 2rem; font-weight: bold; padding: 10px 20px; border-radius: 8px; display: inline-block; transition: all 0.3s ease; }
16
+ .status-text.on { color: #28a745; background-color: #e9f5e9; border: 2px solid #28a745; }
17
+ .status-text.off { color: #dc3545; background-color: #f8d7da; border: 2px solid #dc3545; }
18
+ .chart-container { width: 100%; height: 400px; }
19
+ </style>
20
+ </head>
21
+ <body>
22
+ <div class="container my-4">
23
+ <div class="card">
24
+ <div class="card-header">Live Irrigation Status</div>
25
+ <div class="card-body">
26
+ <div class="row text-center mb-4">
27
+ <div class="col-md-6">
28
+ <div class="status-label">Current Pump Status</div>
29
+ <div id="pumpStatus" class="status-text off">Loading...</div>
30
+ </div>
31
+ <div class="col-md-6 mt-3 mt-md-0">
32
+ <div class="status-label">Time Elapsed</div>
33
+ <div id="time-counter" class="status-text" style="color: #5a2d0c; background-color: #f0e6e0; border-color: #5a2d0c;">0 seconds</div>
34
+ </div>
35
+ </div>
36
+
37
+ <div class="row">
38
+ <div class="col-lg-6 mb-4">
39
+ <div class="card h-100">
40
+ <div class="card-body">
41
+ <div id="gauge" class="chart-container"></div>
42
+ </div>
43
+ </div>
44
+ </div>
45
+ <div class="col-lg-6 mb-4">
46
+ <div class="card h-100">
47
+ <div class="card-body">
48
+ <div id="graph1" class="chart-container"></div>
49
+ </div>
50
+ </div>
51
+ </div>
52
+ <div class="col-lg-6 mb-4">
53
+ <div class="card h-100">
54
+ <div class="card-body">
55
+ <div id="graph2" class="chart-container"></div>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ <div class="col-lg-6 mb-4">
60
+ <div class="card h-100">
61
+ <div class="card-body">
62
+ <div id="graph3" class="chart-container"></div>
63
+ </div>
64
+ </div>
65
+ </div>
66
+ </div>
67
+ </div>
68
+ </div>
69
+ </div>
70
+
71
+ <script>
72
+ let alertSound; // Will be initialized after user interaction
73
+
74
+ function initializeAudio() {
75
+ if (!alertSound) {
76
+ alertSound = new Audio('{{ url_for("static", filename="alarn_tune.mp3") }}');
77
+ console.log("Audio initialized.");
78
+ }
79
+ }
80
+ document.body.addEventListener('click', initializeAudio, { once: true });
81
+
82
+ function fetchPumpStatus() {
83
+ fetch('/update_pump_status')
84
+ .then(response => response.json())
85
+ .then(data => {
86
+ const statusElement = document.getElementById('pumpStatus');
87
+ const newStatus = data.pump_status;
88
+ const oldStatus = statusElement.innerText;
89
+
90
+ statusElement.innerText = newStatus;
91
+ statusElement.className = 'status-text ' + (newStatus === 'On' ? 'on' : 'off');
92
+
93
+ if (newStatus === 'Off' && oldStatus === 'On' && alertSound) {
94
+ alertSound.play().catch(e => console.error("Audio play failed:", e));
95
+ }
96
+ });
97
+ }
98
+
99
+ function fetchGraphData() {
100
+ fetch('/update_graph')
101
+ .then(response => response.json())
102
+ .then(data => {
103
+ if (data.length === 0) return;
104
+
105
+ const time = data.map((_, i) => i * 2); // Assuming 2 second intervals
106
+ const soilMoisture = data.map(entry => entry[0]);
107
+ const pumpStatus = data.map(entry => entry[1]);
108
+ const currentSoilMoisture = soilMoisture[soilMoisture.length - 1];
109
+
110
+ // Responsive layout for all plots
111
+ const responsiveLayout = { margin: { t: 40, b: 50, l: 50, r: 20 }, autosize: true };
112
+
113
+ Plotly.react('gauge', getGaugeData(currentSoilMoisture), { ...responsiveLayout, title: 'Soil Moisture' });
114
+ Plotly.react('graph1', getPumpStatusData(time, pumpStatus), { ...responsiveLayout, title: 'Pump Status vs. Time', yaxis: { tickvals: [-1, 1], ticktext: ['Off', 'On'], range: [-1.5, 1.5] }});
115
+ Plotly.react('graph2', getSoilMoistureData(time, soilMoisture), { ...responsiveLayout, title: 'Soil Moisture vs. Time' });
116
+ Plotly.react('graph3', getMoistureVsStatusData(soilMoisture, pumpStatus), { ...responsiveLayout, title: 'Pump Status vs. Soil Moisture', yaxis: { tickvals: [-1, 1], ticktext: ['Off', 'On'], range: [-1.5, 1.5] }});
117
+ });
118
+ }
119
+
120
+ // --- Chart Data Functions ---
121
+ const getGaugeData = value => [{ type: "indicator", mode: "gauge+number", value: value, gauge: { axis: { range: [0, 100] }, steps: [{ range: [0, 30], color: "#ea4335" },{ range: [30, 60], color: "#fbbc05" },{ range: [60, 100], color: "#34a853" }]}}];
122
+ const getPumpStatusData = (x, y) => [{ x, y, mode: 'lines+markers', type: 'scatter', line: { color: '#4285f4' } }];
123
+ const getSoilMoistureData = (x, y) => [{ x, y, mode: 'lines+markers', type: 'scatter', line: { color: '#34a853' } }];
124
+ const getMoistureVsStatusData = (x, y) => [{ x, y, mode: 'lines', type: 'scatter', line: { color: '#ea4335' } }];
125
+
126
+ let timeCounter = 0;
127
+ setInterval(() => { document.getElementById('time-counter').innerText = `${++timeCounter} seconds`; }, 1000);
128
+ setInterval(fetchPumpStatus, 2000);
129
+ setInterval(fetchGraphData, 2000);
130
+ </script>
131
+ </body>
132
+ </html>