VincentGOURBIN commited on
Commit
2c89d57
·
verified ·
1 Parent(s): 1654790

Upload Application Gradio principale

Browse files
Files changed (1) hide show
  1. app.py +1674 -0
app.py ADDED
@@ -0,0 +1,1674 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import requests
3
+ import json
4
+ from typing import Dict, List, Tuple, Optional
5
+ from datetime import datetime, timedelta
6
+ import os
7
+ from dotenv import load_dotenv
8
+ import logging
9
+ import time
10
+ import locale
11
+
12
+ # Configuration locale pour les dates en français
13
+ try:
14
+ locale.setlocale(locale.LC_TIME, 'fr_FR.UTF-8')
15
+ except:
16
+ try:
17
+ locale.setlocale(locale.LC_TIME, 'French_France.1252')
18
+ except:
19
+ pass # Garde la locale par défaut si français non disponible
20
+
21
+ # Configuration du logging
22
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
23
+ logger = logging.getLogger(__name__)
24
+
25
+ load_dotenv()
26
+
27
+ def format_timestamp(timestamp, format_type="datetime"):
28
+ """
29
+ Convertit un timestamp Unix en format français lisible.
30
+
31
+ Args:
32
+ timestamp (int|float): Timestamp Unix à convertir
33
+ format_type (str): Type de format souhaité
34
+ - "datetime": "01/07/2025 à 14:30" (par défaut)
35
+ - "date": "Mar 01/07"
36
+ - "time": "14:30"
37
+ - autre: format strftime personnalisé
38
+
39
+ Returns:
40
+ str: Date formatée en français ou "--" si erreur
41
+ """
42
+ if not timestamp or timestamp == 0:
43
+ return "--"
44
+
45
+ try:
46
+ dt = datetime.fromtimestamp(timestamp)
47
+ if format_type == "date":
48
+ return dt.strftime("%a %d/%m")
49
+ elif format_type == "time":
50
+ return dt.strftime("%H:%M")
51
+ elif format_type == "datetime":
52
+ return dt.strftime("%d/%m/%Y à %H:%M")
53
+ else:
54
+ return dt.strftime(format_type)
55
+ except (ValueError, OSError):
56
+ return str(timestamp)
57
+
58
+ class MeteoFranceAPI:
59
+ """
60
+ Client pour l'API privée Météo-France.
61
+
62
+ Utilise l'API interne de Météo-France (même que l'app mobile officielle)
63
+ pour récupérer prévisions, observations, alertes et données de pluie.
64
+
65
+ Attributes:
66
+ base_url (str): URL de base de l'API Météo-France
67
+ token (str): Token d'authentification depuis variable d'environnement
68
+ headers (dict): Headers HTTP pour les requêtes
69
+ """
70
+
71
+ def __init__(self):
72
+ # API privée Météo-France utilisée par les apps mobiles officielles
73
+ self.base_url = "https://webservice.meteofrance.com"
74
+
75
+ # Token depuis variable d'environnement obligatoire
76
+ self.token = os.getenv('METEOFRANCE_TOKEN')
77
+
78
+ if not self.token:
79
+ raise ValueError("Token Météo-France manquant. Définissez METEOFRANCE_TOKEN dans votre fichier .env")
80
+
81
+ self.headers = {
82
+ "User-Agent": "MeteoApp/1.0"
83
+ }
84
+
85
+ def get_location_forecast(self, lat: float, lon: float) -> dict:
86
+ """
87
+ Récupère les prévisions météo complètes pour une position géographique.
88
+
89
+ Args:
90
+ lat (float): Latitude en degrés décimaux
91
+ lon (float): Longitude en degrés décimaux
92
+
93
+ Returns:
94
+ dict: Données de prévision avec clés principales:
95
+ - position: Infos localisation (nom, département, altitude...)
96
+ - forecast: Prévisions horaires sur 7 jours
97
+ - daily_forecast: Prévisions quotidiennes sur 10 jours
98
+ - updated_on: Timestamp de mise à jour
99
+ En cas d'erreur: {"error": "message d'erreur"}
100
+ """
101
+ logger.info(f"🌤️ Récupération prévisions météo pour: lat={lat}, lon={lon}")
102
+
103
+ try:
104
+ # API privée Météo-France - Prévisions complètes
105
+ url = f"{self.base_url}/forecast"
106
+ params = {
107
+ "lat": lat,
108
+ "lon": lon,
109
+ "lang": "fr",
110
+ "token": self.token # Token passé en paramètre de requête
111
+ }
112
+
113
+ # Masquer le token dans les logs
114
+ safe_params = {k: "***" if k == "token" else v for k, v in params.items()}
115
+ logger.info(f"📡 Appel API Météo-France: {url}")
116
+ logger.info(f"📋 Paramètres: {safe_params}")
117
+
118
+ response = requests.get(url, headers=self.headers, params=params)
119
+
120
+ if response.status_code == 200:
121
+ data = response.json()
122
+ logger.info(f"✅ Données météo reçues")
123
+ return data
124
+ else:
125
+ logger.error(f"❌ Erreur API Météo-France {response.status_code}: {response.text}")
126
+ return {"error": f"Erreur API: {response.status_code}"}
127
+ except Exception as e:
128
+ logger.error(f"❌ Erreur connexion Météo-France: {e}")
129
+ return {"error": f"Erreur de connexion: {str(e)}"}
130
+
131
+ def get_wind_alerts_by_department(self, department: str) -> dict:
132
+ """
133
+ Récupère les alertes météo pour un département français spécifique.
134
+
135
+ Args:
136
+ department (str): Code département français (ex: "29", "75", "2A")
137
+
138
+ Returns:
139
+ dict: Données d'alertes avec clés principales:
140
+ - domain_id: Code du département
141
+ - phenomenons_max_colors: Liste des phénomènes avec niveaux d'alerte
142
+ - update_time: Timestamp de mise à jour
143
+ - end_validity_time: Fin de validité des alertes
144
+ En cas d'erreur: {"error": "message d'erreur"}
145
+ """
146
+ logger.info(f"⚠️ Récupération alertes pour département: {department}")
147
+
148
+ try:
149
+ # API privée Météo-France - Alertes météo par département
150
+ url = f"{self.base_url}/v3/warning/currentphenomenons"
151
+ params = {
152
+ "domain": department,
153
+ "depth": 1,
154
+ "with_coastal_bulletin": "true",
155
+ "token": self.token
156
+ }
157
+
158
+ # Masquer le token dans les logs
159
+ safe_params = {k: "***" if k == "token" else v for k, v in params.items()}
160
+ logger.info(f"📡 Appel API Alertes pour département {department}")
161
+
162
+ response = requests.get(url, headers=self.headers, params=params)
163
+
164
+ if response.status_code == 200:
165
+ data = response.json()
166
+ logger.info(f"✅ Alertes reçues")
167
+ return data
168
+ else:
169
+ logger.error(f"❌ Erreur API Alertes {response.status_code}: {response.text}")
170
+ return {"error": f"Erreur API alertes: {response.status_code}"}
171
+ except Exception as e:
172
+ logger.error(f"❌ Erreur alertes: {e}")
173
+ return {"error": f"Erreur alertes: {str(e)}"}
174
+
175
+ def get_wind_alerts(self, lat: float, lon: float) -> dict:
176
+ """Récupère les alertes vent pour une position (fallback)"""
177
+ logger.info(f"⚠️ Récupération alertes vent pour: lat={lat}, lon={lon}")
178
+
179
+ # Déterminer le département à partir des coordonnées GPS
180
+ department = self._get_department_from_coords(lat, lon)
181
+
182
+ return self.get_wind_alerts_by_department(department)
183
+
184
+ def _get_department_from_coords(self, lat: float, lon: float) -> str:
185
+ """Approximation simple du département depuis les coordonnées GPS"""
186
+ logger.info(f"🗺️ Détermination département pour: lat={lat}, lon={lon}")
187
+
188
+ # Logique simplifiée pour les principales régions
189
+ # Paris et Ile-de-France
190
+ if 48.8 <= lat <= 49.0 and 2.2 <= lon <= 2.5:
191
+ dept = "75" # Paris
192
+ elif 48.1 <= lat <= 49.2 and 1.4 <= lon <= 3.6:
193
+ dept = "77" # Seine-et-Marne (approximation IDF)
194
+ # Marseille
195
+ elif 43.2 <= lat <= 43.4 and 5.3 <= lon <= 5.5:
196
+ dept = "13" # Bouches-du-Rhône
197
+ # Lyon
198
+ elif 45.7 <= lat <= 45.8 and 4.8 <= lon <= 4.9:
199
+ dept = "69" # Rhône
200
+ # Toulouse
201
+ elif 43.5 <= lat <= 43.7 and 1.3 <= lon <= 1.5:
202
+ dept = "31" # Haute-Garonne
203
+ # Par défaut, utiliser "france" pour l'ensemble du territoire
204
+ else:
205
+ dept = "france"
206
+
207
+ logger.info(f"🏷️ Département déterminé: {dept}")
208
+ return dept
209
+
210
+ def get_current_observation(self, lat: float, lon: float) -> dict:
211
+ """Récupère les observations météo actuelles"""
212
+ logger.info(f"🌡️ Récupération observations actuelles pour: lat={lat}, lon={lon}")
213
+
214
+ try:
215
+ url = f"{self.base_url}/v2/observation"
216
+ params = {
217
+ "lat": lat,
218
+ "lon": lon,
219
+ "lang": "fr",
220
+ "token": self.token # Token passé en paramètre de requête
221
+ }
222
+
223
+ response = requests.get(url, headers=self.headers, params=params)
224
+
225
+ if response.status_code == 200:
226
+ data = response.json()
227
+ logger.info(f"✅ Observations reçues")
228
+ return data
229
+ else:
230
+ logger.error(f"❌ Erreur API Observations {response.status_code}: {response.text}")
231
+ return {"error": f"Erreur API observations: {response.status_code}"}
232
+ except Exception as e:
233
+ logger.error(f"❌ Erreur observations: {e}")
234
+ return {"error": f"Erreur observations: {str(e)}"}
235
+
236
+ def get_rain_forecast(self, lat: float, lon: float) -> dict:
237
+ """Récupère les prévisions de pluie dans l'heure"""
238
+ logger.info(f"🌧️ Récupération prévisions pluie pour: lat={lat}, lon={lon}")
239
+
240
+ try:
241
+ url = f"{self.base_url}/rain"
242
+ params = {
243
+ "lat": round(lat, 3), # Précision minimum requise
244
+ "lon": round(lon, 3),
245
+ "lang": "fr",
246
+ "token": self.token # Token passé en paramètre de requête
247
+ }
248
+
249
+ response = requests.get(url, headers=self.headers, params=params)
250
+
251
+ if response.status_code == 200:
252
+ data = response.json()
253
+ logger.info(f"✅ Prévisions pluie reçues")
254
+ return data
255
+ else:
256
+ logger.error(f"❌ Erreur API Pluie {response.status_code}: {response.text}")
257
+ return {"error": f"Erreur API pluie: {response.status_code}"}
258
+ except Exception as e:
259
+ logger.error(f"❌ Erreur pluie: {e}")
260
+ return {"error": f"Erreur pluie: {str(e)}"}
261
+
262
+
263
+ class GeocodingAPI:
264
+ """
265
+ Client pour l'API de géocodage IGN Géoplateforme.
266
+
267
+ Utilise l'API officielle française pour convertir des adresses
268
+ en coordonnées GPS avec métadonnées françaises précises.
269
+
270
+ Attributes:
271
+ base_url (str): URL de l'API de géocodage IGN
272
+ """
273
+
274
+ def __init__(self):
275
+ # L'API IGN ne nécessite pas de clé d'accès
276
+ self.base_url = "https://data.geopf.fr/geocodage/search"
277
+
278
+ def geocode_address(self, address: str) -> dict:
279
+ """
280
+ Convertit une adresse française en coordonnées GPS via l'API IGN.
281
+
282
+ Args:
283
+ address (str): Adresse à géolocaliser (ex: "Brest, France")
284
+
285
+ Returns:
286
+ dict: Résultat avec clés:
287
+ - lat (float): Latitude en degrés décimaux
288
+ - lon (float): Longitude en degrés décimaux
289
+ - full_data (dict): Données complètes IGN (propriétés, géométrie...)
290
+ None en cas d'erreur
291
+ """
292
+ logger.info(f"🔍 Géocodage IGN de l'adresse: {address}")
293
+
294
+ try:
295
+ params = {
296
+ "q": address,
297
+ "index": "address", # Recherche par adresse
298
+ "limit": 1,
299
+ "returntruegeometry": "true"
300
+ }
301
+
302
+ response = requests.get(self.base_url, params=params)
303
+
304
+ if response.status_code == 200:
305
+ data = response.json()
306
+ logger.info(f"✅ Géolocalisation réussie")
307
+
308
+ if data.get("features") and len(data["features"]) > 0:
309
+ feature = data["features"][0]
310
+ coords = feature["geometry"]["coordinates"]
311
+ logger.info(f"📍 Coordonnées trouvées: lat={coords[1]}, lon={coords[0]}")
312
+ return {
313
+ "lat": coords[1],
314
+ "lon": coords[0],
315
+ "full_data": feature # Conserver toutes les données pour la carte
316
+ }
317
+ else:
318
+ logger.warning("⚠️ Aucune coordonnée trouvée dans la réponse IGN")
319
+ else:
320
+ logger.error(f"❌ Erreur HTTP IGN {response.status_code}: {response.text}")
321
+ return None
322
+ except Exception as e:
323
+ logger.error(f"❌ Erreur géocodage IGN: {e}")
324
+ return None
325
+
326
+
327
+ def format_weather_data(data: dict) -> str:
328
+ """
329
+ Formate les données météo brutes en texte lisible markdown.
330
+
331
+ Args:
332
+ data (dict): Données météo brutes de l'API Météo-France
333
+ avec clés position, forecast, daily_forecast, updated_on
334
+
335
+ Returns:
336
+ str: Données météo formatées en markdown avec prévisions
337
+ quotidiennes et horaires, ou message d'erreur si échec
338
+ """
339
+ if "error" in data:
340
+ return f"❌ {data['error']}"
341
+
342
+ formatted = "🌤️ **Prévisions Météo**\n\n"
343
+
344
+ try:
345
+ if "position" in data:
346
+ pos = data["position"]
347
+ formatted += f"📍 **Localisation:** {pos.get('name', 'Inconnu')}\n"
348
+ formatted += f"Coordonnées: {pos.get('lat', 0):.3f}, {pos.get('lon', 0):.3f}\n\n"
349
+
350
+ if "updated_on" in data:
351
+ update_time = format_timestamp(data['updated_on'])
352
+ formatted += f"⏰ **Mise à jour:** {update_time}\n\n"
353
+
354
+ # Prévisions quotidiennes
355
+ if "daily_forecast" in data and data["daily_forecast"]:
356
+ formatted += "📅 **Prévisions à 10 jours:**\n"
357
+ for day in data["daily_forecast"][:10]:
358
+ timestamp = day.get("dt", 0)
359
+ date_str = format_timestamp(timestamp, "date")
360
+ temp_min = day.get("T", {}).get("min", "--")
361
+ temp_max = day.get("T", {}).get("max", "--")
362
+ weather = day.get("weather12H", {}).get("desc", "--")
363
+ formatted += f" • {date_str}: {temp_min}°/{temp_max}°C - {weather}\n"
364
+ formatted += "\n"
365
+
366
+ # Prévisions horaires (aujourd'hui)
367
+ if "forecast" in data and data["forecast"]:
368
+ formatted += "🕰️ **Aujourd'hui (par heure):**\n"
369
+ for hour in data["forecast"][:12]: # 12 premières heures
370
+ timestamp = hour.get("dt", 0)
371
+ time_str = format_timestamp(timestamp, "time")
372
+ temp = hour.get("T", {}).get("value", "--")
373
+ weather = hour.get("weather", {}).get("desc", "--")
374
+ formatted += f" • {time_str}: {temp}°C - {weather}\n"
375
+
376
+ except Exception as e:
377
+ logger.error(f"Erreur formatage météo: {e}")
378
+ formatted += f"Données brutes: {json.dumps(data, indent=2, ensure_ascii=False)[:500]}..."
379
+
380
+ return formatted
381
+
382
+ def format_current_conditions(current_data: dict, rain_data: dict = None, forecast_data: dict = None) -> str:
383
+ """
384
+ Formate les conditions météorologiques actuelles avec détails complets.
385
+
386
+ Combine observations actuelles, prévisions de pluie et données de localisation
387
+ pour créer un rapport météo actuel détaillé.
388
+
389
+ Args:
390
+ current_data (dict): Observations météo actuelles de l'API
391
+ rain_data (dict, optional): Prévisions pluie dans l'heure. Defaults to None.
392
+ forecast_data (dict, optional): Données de prévision pour la localisation. Defaults to None.
393
+
394
+ Returns:
395
+ str: Conditions actuelles formatées en markdown avec température,
396
+ vent, précipitations, humidité, pression et visibilité
397
+ """
398
+ if "error" in current_data:
399
+ return f"❌ {current_data['error']}"
400
+
401
+ formatted = "🌡️ **Conditions Actuelles**\n\n"
402
+
403
+ try:
404
+ # Localisation
405
+ if forecast_data and "position" in forecast_data:
406
+ pos = forecast_data["position"]
407
+ formatted += f"📍 **Lieu**: {pos.get('name', 'Localisation inconnue')}\n"
408
+ formatted += f"Coordonnées: {pos.get('lat', 0):.3f}, {pos.get('lon', 0):.3f}\n\n"
409
+
410
+ # Heure d'observation depuis properties.gridded.time
411
+ gridded = current_data.get("properties", {}).get("gridded", {})
412
+ if "time" in gridded:
413
+ obs_time = gridded["time"]
414
+ # Convertir format ISO vers format lisible
415
+ if obs_time:
416
+ try:
417
+ from datetime import datetime
418
+ dt = datetime.fromisoformat(obs_time.replace('Z', '+00:00'))
419
+ formatted += f"⏰ **Observation**: {dt.strftime('%d/%m/%Y à %H:%M')}\n\n"
420
+ except:
421
+ formatted += f"⏰ **Observation**: {obs_time}\n\n"
422
+
423
+ # Température depuis properties.gridded.T
424
+ temp = gridded.get("T", "--")
425
+ formatted += f"🌡️ **Température**: {temp}°C\n"
426
+
427
+ # Vent depuis properties.gridded
428
+ wind_speed = gridded.get("wind_speed", "--")
429
+ wind_dir = gridded.get("wind_direction", "--")
430
+ wind_icon = gridded.get("wind_icon", "")
431
+
432
+ formatted += f"🌬️ **Vent**: {wind_speed} km/h - {wind_dir}° {wind_icon}\n"
433
+
434
+ # Conditions météo depuis properties.gridded
435
+ weather_desc = gridded.get("weather_description", "--")
436
+ weather_icon = gridded.get("weather_icon", "")
437
+ formatted += f"🌤️ **Temps**: {weather_desc} {weather_icon}\n"
438
+
439
+ # Affichage des précipitations (selon meteofrance-api officiel)
440
+ if rain_data and "error" not in rain_data:
441
+ # Vérifier la disponibilité du service pluie
442
+ rain_available = rain_data.get("position", {}).get("rain_product_available", 0)
443
+
444
+ if rain_available == 0:
445
+ formatted += f"💧 **Précipitations**: Service radar indisponible pour cette zone\n"
446
+ elif "forecast" in rain_data and rain_data["forecast"]:
447
+ # Prendre la première prévision (maintenant)
448
+ current_rain = rain_data["forecast"][0] if rain_data["forecast"] else None
449
+ if current_rain:
450
+ rain_intensity = current_rain.get("rain", 0)
451
+ rain_desc = current_rain.get("desc", "Pas de données")
452
+
453
+ # Interprétation selon meteofrance-api : > 1 = pluie
454
+ if rain_intensity <= 1:
455
+ formatted += f"💧 **Précipitations**: Aucune - {rain_desc}\n"
456
+ elif rain_intensity == 2:
457
+ formatted += f"💧 **Précipitations**: 🌦️ Pluie faible - {rain_desc}\n"
458
+ elif rain_intensity == 3:
459
+ formatted += f"💧 **Précipitations**: 🌧️ Pluie modérée - {rain_desc}\n"
460
+ elif rain_intensity >= 4:
461
+ formatted += f"💧 **Précipitations**: ⛈️ Pluie forte - {rain_desc}\n"
462
+ else:
463
+ formatted += f"💧 **Précipitations**: Intensité {rain_intensity} - {rain_desc}\n"
464
+ else:
465
+ formatted += f"💧 **Précipitations**: Données non disponibles\n"
466
+ else:
467
+ formatted += f"💧 **Précipitations**: Pas de prévisions disponibles\n"
468
+ else:
469
+ formatted += f"💧 **Précipitations**: Service indisponible\n"
470
+
471
+ # Humidité
472
+ humidity = current_data.get("humidity", "--")
473
+ if humidity != "--":
474
+ formatted += f"💧 **Humidité**: {humidity}%\n"
475
+
476
+ # Pression
477
+ pressure = current_data.get("pressure", {}).get("value", "--")
478
+ if pressure != "--":
479
+ formatted += f"📊 **Pression**: {pressure} hPa\n"
480
+
481
+ # Visibilité
482
+ visibility = current_data.get("visibility", "--")
483
+ if visibility != "--":
484
+ formatted += f"👁️ **Visibilité**: {visibility} km\n"
485
+
486
+ # Prévisions de pluie dans l'heure (améliorées)
487
+ if rain_data and "error" not in rain_data:
488
+ formatted += "\n🌧️ **Pluie dans l'heure:**\n"
489
+
490
+ # Informations sur la localisation pluie
491
+ if "position" in rain_data:
492
+ rain_pos = rain_data["position"]
493
+ quality = rain_data.get("quality", 0)
494
+ updated = rain_data.get("updated_on", "")
495
+ if isinstance(updated, (int, float)):
496
+ update_str = format_timestamp(updated)
497
+ else:
498
+ update_str = updated
499
+ formatted += f"📍 Lieu: {rain_pos.get('name', 'Inconnu')} (Dept. {rain_pos.get('dept', '??')})\n"
500
+ formatted += f"⏰ Mis à jour: {update_str}\n"
501
+ formatted += f"📊 Qualité: {quality}/10\n\n"
502
+
503
+ if "forecast" in rain_data and rain_data["forecast"]:
504
+ next_rain = rain_data["forecast"][:6] # 6 prochains points (1h30)
505
+ for point in next_rain:
506
+ rain_intensity = point.get("rain", 0)
507
+ rain_desc_api = point.get("desc", "")
508
+ time_point = point.get("dt", "")
509
+
510
+ if isinstance(time_point, (int, float)):
511
+ time_str = format_timestamp(time_point, "time")
512
+ else:
513
+ time_str = time_point
514
+
515
+ # Utiliser la description de l'API si disponible, sinon notre mapping
516
+ if rain_desc_api and rain_desc_api != "Pas de valeur":
517
+ rain_desc = rain_desc_api
518
+ else:
519
+ if rain_intensity == 0:
520
+ rain_desc = "Pas de pluie"
521
+ elif rain_intensity == 1:
522
+ rain_desc = "Pluie faible"
523
+ elif rain_intensity == 2:
524
+ rain_desc = "Pluie modérée"
525
+ elif rain_intensity == 3:
526
+ rain_desc = "Pluie forte"
527
+ else:
528
+ rain_desc = f"Intensité {rain_intensity}"
529
+
530
+ # Émoji selon l'intensité
531
+ if rain_intensity == 0:
532
+ emoji = "☀️"
533
+ elif rain_intensity == 1:
534
+ emoji = "🌦️"
535
+ elif rain_intensity == 2:
536
+ emoji = "🌧️"
537
+ elif rain_intensity >= 3:
538
+ emoji = "⛈️"
539
+ else:
540
+ emoji = "❓"
541
+
542
+ formatted += f" • {time_str}: {emoji} {rain_desc}\n"
543
+ else:
544
+ formatted += " • Données non disponibles\n"
545
+
546
+ except Exception as e:
547
+ logger.error(f"Erreur formatage conditions actuelles: {e}")
548
+ formatted += f"\nErreur formatage: {e}\n"
549
+
550
+ return formatted
551
+
552
+ def format_hourly_forecast(forecast_data: dict, rain_data: dict = None) -> str:
553
+ """
554
+ Formate les prévisions météo horaires sur 24h avec intégration des précipitations.
555
+
556
+ Args:
557
+ forecast_data (dict): Données de prévision horaire de l'API Météo-France
558
+ rain_data (dict, optional): Données de précipitations à intégrer. Defaults to None.
559
+
560
+ Returns:
561
+ str: Prévisions horaires formatées en markdown avec température,
562
+ météo, vent et informations de pluie pour chaque heure
563
+ """
564
+ if "error" in forecast_data:
565
+ return f"❌ {forecast_data['error']}"
566
+
567
+ formatted = "🕰️ **Prévisions Heure par Heure (24h)**\n\n"
568
+
569
+ try:
570
+ # Créer un dictionnaire des prévisions pluie par timestamp pour lookup rapide
571
+ rain_by_time = {}
572
+ if rain_data and "forecast" in rain_data and rain_data["forecast"]:
573
+ for rain_point in rain_data["forecast"]:
574
+ rain_timestamp = rain_point.get("dt", 0)
575
+ rain_by_time[rain_timestamp] = rain_point
576
+
577
+ if "forecast" in forecast_data and forecast_data["forecast"]:
578
+ for hour in forecast_data["forecast"][:24]:
579
+ timestamp = hour.get("dt", 0)
580
+ time_str = format_timestamp(timestamp, "time")
581
+
582
+ temp = hour.get("T", {}).get("value", "--")
583
+ weather = hour.get("weather", {}).get("desc", "--")
584
+ wind_speed = hour.get("wind", {}).get("speed", "--")
585
+ wind_dir = hour.get("wind", {}).get("direction", "--")
586
+ wind_gust = hour.get("wind", {}).get("gust", "--")
587
+
588
+ # Récupérer les données de pluie pour cette heure
589
+ rain_info = ""
590
+ if timestamp in rain_by_time:
591
+ rain_point = rain_by_time[timestamp]
592
+ rain_intensity = rain_point.get("rain", 0)
593
+ rain_desc = rain_point.get("desc", "")
594
+
595
+ if rain_intensity <= 1:
596
+ rain_info = " - ☀️ Sec"
597
+ elif rain_intensity == 2:
598
+ rain_info = " - 🌦️ Pluie faible"
599
+ elif rain_intensity == 3:
600
+ rain_info = " - 🌧️ Pluie modérée"
601
+ elif rain_intensity >= 4:
602
+ rain_info = " - ⛈️ Pluie forte"
603
+ else:
604
+ rain_info = f" - 💧 Intensité {rain_intensity}"
605
+
606
+ formatted += f"**{time_str}**: {temp}°C - {weather}{rain_info}\n"
607
+ formatted += f" Vent: {wind_speed} km/h ({wind_dir}°)"
608
+ if wind_gust != "--" and wind_gust != 0:
609
+ formatted += f" - Rafales: {wind_gust} km/h"
610
+ formatted += "\n\n"
611
+
612
+ except Exception as e:
613
+ logger.error(f"Erreur formatage prévisions horaires: {e}")
614
+ formatted += f"Erreur: {e}\n"
615
+
616
+ return formatted
617
+
618
+ def format_daily_forecast(forecast_data: dict) -> str:
619
+ """
620
+ Formate les prévisions météorologiques quotidiennes sur 10 jours.
621
+
622
+ Args:
623
+ forecast_data (dict): Données de prévision quotidienne de l'API Météo-France
624
+ avec clé daily_forecast contenant la liste des jours
625
+
626
+ Returns:
627
+ str: Prévisions sur 10 jours formatées en markdown avec températures
628
+ min/max, description météo et données de vent pour chaque jour
629
+ """
630
+ if "error" in forecast_data:
631
+ return f"❌ {forecast_data['error']}"
632
+
633
+ formatted = "📅 **Prévisions à 10 Jours**\n\n"
634
+
635
+ try:
636
+ if "daily_forecast" in forecast_data and forecast_data["daily_forecast"]:
637
+ for day in forecast_data["daily_forecast"][:10]:
638
+ timestamp = day.get("dt", 0)
639
+ date_str = format_timestamp(timestamp, "date")
640
+
641
+ temp_min = day.get("T", {}).get("min", "--")
642
+ temp_max = day.get("T", {}).get("max", "--")
643
+ weather = day.get("weather12H", {}).get("desc", "--")
644
+ wind_speed = day.get("wind", {}).get("speed", "--")
645
+ wind_dir = day.get("wind", {}).get("direction", "--")
646
+
647
+ formatted += f"**{date_str}**: {temp_min}°/{temp_max}°C\n"
648
+ formatted += f" {weather}\n"
649
+ if wind_speed != "--":
650
+ formatted += f" Vent: {wind_speed} km/h ({wind_dir}°)\n"
651
+ formatted += "\n"
652
+
653
+ except Exception as e:
654
+ logger.error(f"Erreur formatage prévisions 10 jours: {e}")
655
+ formatted += f"Erreur: {e}\n"
656
+
657
+ return formatted
658
+
659
+ def format_wind_data(forecast_data: dict, current_data: dict = None) -> str:
660
+ """
661
+ Formate les données de vent actuelles et prévisions pour l'affichage.
662
+
663
+ Args:
664
+ forecast_data (dict): Prévisions météo avec données de vent horaires et quotidiennes
665
+ current_data (dict, optional): Observations actuelles de vent. Defaults to None.
666
+
667
+ Returns:
668
+ str: Données de vent formatées en markdown avec conditions actuelles,
669
+ prévisions horaires sur 24h et prévisions quotidiennes sur 10 jours
670
+ """
671
+ if "error" in forecast_data:
672
+ return f"❌ {forecast_data['error']}"
673
+
674
+ formatted = "💨 **Informations Vent**\n\n"
675
+
676
+ try:
677
+ # Conditions actuelles de vent
678
+ if current_data and "error" not in current_data:
679
+ formatted += "🌬️ **Conditions actuelles:**\n"
680
+ wind_speed = current_data.get("wind_speed", "--")
681
+ wind_dir = current_data.get("wind_direction", "--")
682
+ wind_icon = current_data.get("wind_icon", "")
683
+ formatted += f" • Vitesse: {wind_speed} km/h\n"
684
+ formatted += f" • Direction: {wind_dir}° {wind_icon}\n\n"
685
+
686
+ # Prévisions de vent par heure
687
+ if "forecast" in forecast_data and forecast_data["forecast"]:
688
+ formatted += "🕰️ **Prévisions vent (24h):**\n"
689
+ for hour in forecast_data["forecast"][:24]:
690
+ timestamp = hour.get("dt", 0)
691
+ time_str = format_timestamp(timestamp, "time")
692
+ wind_speed = hour.get("wind", {}).get("speed", "--")
693
+ wind_dir = hour.get("wind", {}).get("direction", "--")
694
+ wind_gust = hour.get("wind", {}).get("gust", "--")
695
+
696
+ formatted += f" • {time_str}: {wind_speed} km/h"
697
+ if wind_gust != "--" and wind_gust != 0:
698
+ formatted += f" (rafales: {wind_gust} km/h)"
699
+ formatted += f" - {wind_dir}°\n"
700
+
701
+ # Prévisions quotidiennes de vent
702
+ if "daily_forecast" in forecast_data and forecast_data["daily_forecast"]:
703
+ formatted += "\n📅 **Vent à 10 jours:**\n"
704
+ for day in forecast_data["daily_forecast"][:10]:
705
+ timestamp = day.get("dt", 0)
706
+ date_str = format_timestamp(timestamp, "date")
707
+ wind_speed = day.get("wind", {}).get("speed", "--")
708
+ wind_dir = day.get("wind", {}).get("direction", "--")
709
+ formatted += f" • {date_str}: {wind_speed} km/h - {wind_dir}°\n"
710
+
711
+ except Exception as e:
712
+ logger.error(f"Erreur formatage vent: {e}")
713
+ formatted += f"Données brutes: {json.dumps(forecast_data, indent=2, ensure_ascii=False)[:500]}..."
714
+
715
+ return formatted
716
+
717
+ def format_alerts_data(data: dict) -> str:
718
+ """
719
+ Formate les alertes météorologiques Météo-France selon leur niveau de danger.
720
+
721
+ Analyse les données d'alertes brutes, détermine le niveau maximum d'alerte
722
+ et formate l'affichage avec codes couleur appropriés (vert/jaune/orange/rouge).
723
+
724
+ Args:
725
+ data (dict): Données d'alertes de l'API Météo-France avec clés:
726
+ - phenomenons_max_colors: Liste des phénomènes avec niveaux
727
+ - domain_id: Code département
728
+ - update_time: Timestamp de mise à jour
729
+
730
+ Returns:
731
+ str: Alertes formatées en markdown avec niveau global, détail des
732
+ phénomènes actifs et descriptions selon référentiel officiel
733
+ """
734
+ if "error" in data:
735
+ return f"❌ {data['error']}"
736
+
737
+ try:
738
+ # Calculer le niveau d'alerte maximum selon la logique meteofrance-api
739
+ max_color_id = 1 # Par défaut vert
740
+ active_alerts = []
741
+
742
+ # Traitement pour les données directes de l'API
743
+ if "phenomenons_max_colors" in data and data["phenomenons_max_colors"] is not None:
744
+ for item in data["phenomenons_max_colors"]:
745
+ if item is not None:
746
+ color_id = item.get("phenomenon_max_color_id", 1)
747
+ if color_id > max_color_id:
748
+ max_color_id = color_id
749
+ if color_id > 1:
750
+ active_alerts.append(item)
751
+
752
+ # Traitement pour le format avec timelaps (cas multi-départements)
753
+ elif "timelaps" in data and isinstance(data["timelaps"], list):
754
+ # Trouver le département avec le niveau d'alerte le plus élevé
755
+ # ou chercher un département spécifique si on peut l'identifier
756
+ target_dept = None
757
+
758
+ # data["timelaps"] est une liste de départements
759
+ for dept_data in data["timelaps"]:
760
+ if dept_data and "phenomenons_max_color" in dept_data:
761
+ dept_max = 1
762
+ dept_alerts = []
763
+
764
+ for item in dept_data["phenomenons_max_color"]:
765
+ if item is not None:
766
+ color_id = item.get("phenomenon_max_color_id", 1)
767
+ dept_max = max(dept_max, color_id)
768
+ if color_id > 1:
769
+ dept_alerts.append(item)
770
+
771
+ # Si ce département a un niveau plus élevé que l'actuel
772
+ if dept_max > max_color_id:
773
+ max_color_id = dept_max
774
+ active_alerts = dept_alerts
775
+ target_dept = dept_data.get("domain_id", "Inconnu")
776
+
777
+ # Traitement pour les sous-domaines si pas d'alertes directes
778
+ elif "subdomains_phenomenons_max_color" in data and data["subdomains_phenomenons_max_color"] is not None:
779
+ for subdomain in data["subdomains_phenomenons_max_color"]:
780
+ if subdomain and "phenomenons_max_color" in subdomain and subdomain["phenomenons_max_color"] is not None:
781
+ for item in subdomain["phenomenons_max_color"]:
782
+ if item is not None:
783
+ color_id = item.get("phenomenon_max_color_id", 1)
784
+ if color_id > max_color_id:
785
+ max_color_id = color_id
786
+ if color_id > 1:
787
+ active_alerts.append(item)
788
+
789
+ # Traitement pour le format alternatif
790
+ elif "phenomenon_items" in data:
791
+ for phenomenon_id, phenomenon_data in data["phenomenon_items"].items():
792
+ color_id = phenomenon_data.get("phenomenon_max_color_id", 1)
793
+ if color_id > max_color_id:
794
+ max_color_id = color_id
795
+ if color_id > 1:
796
+ active_alerts.append({
797
+ "phenomenon_id": phenomenon_id,
798
+ "phenomenon_max_color_id": color_id
799
+ })
800
+
801
+ # Noms des phénomènes (référence officielle meteofrance-api)
802
+ phenomena_names = {
803
+ "0": None,
804
+ "1": "Vent violent",
805
+ "2": "Pluie-inondation",
806
+ "3": "Orages",
807
+ "4": "Inondation",
808
+ "5": "Neige-verglas",
809
+ "6": "Canicule",
810
+ "7": "Grand-froid",
811
+ "8": "Avalanches",
812
+ "9": "Vagues-submersion"
813
+ }
814
+
815
+ # Correspondance couleurs (CORRIGÉE)
816
+ colors = {
817
+ 1: "🟢 Vert",
818
+ 2: "🟡 Jaune",
819
+ 3: "🟠 Orange",
820
+ 4: "🔴 Rouge"
821
+ }
822
+
823
+ color_descriptions = {
824
+ 1: "Pas de vigilance particulière",
825
+ 2: "Soyez attentifs",
826
+ 3: "Soyez très vigilants",
827
+ 4: "Vigilance absolue"
828
+ }
829
+
830
+ # Formatage selon le niveau maximum
831
+ if max_color_id >= 4:
832
+ formatted = "🔴 **ALERTE ROUGE - VIGILANCE ABSOLUE**\n\n"
833
+ elif max_color_id >= 3:
834
+ formatted = "🟠 **ALERTE ORANGE - SOYEZ TRÈS VIGILANTS**\n\n"
835
+ elif max_color_id >= 2:
836
+ formatted = "🟡 **VIGILANCE JAUNE - SOYEZ ATTENTIFS**\n\n"
837
+ else:
838
+ formatted = "🟢 **Pas d'alerte en cours**\n\n"
839
+
840
+ # Affichage du département si disponible
841
+ if 'target_dept' in locals() and target_dept:
842
+ formatted += f"📍 **Département**: {target_dept}\n"
843
+ elif "domain_id" in data:
844
+ formatted += f"📍 **Département**: {data['domain_id']}\n"
845
+
846
+ # Niveau global
847
+ formatted += f"🏷️ **Niveau global**: {colors.get(max_color_id, 'Inconnu')} - {color_descriptions.get(max_color_id, '')}\n\n"
848
+
849
+ # Détail des alertes actives
850
+ if active_alerts:
851
+ formatted += "🚨 **Détail des alertes:**\n"
852
+ for alert in active_alerts:
853
+ phenomenon_id = str(alert.get("phenomenon_id", ""))
854
+ color_id = alert.get("phenomenon_max_color_id", 1)
855
+ phenomenon_name = phenomena_names.get(phenomenon_id, f"Phénomène {phenomenon_id}")
856
+ color_desc = colors.get(color_id, f"Niveau {color_id}")
857
+
858
+ formatted += f" • **{phenomenon_name}**: {color_desc}\n"
859
+
860
+ # Timestamp de mise à jour
861
+ if "update_time" in data:
862
+ update_time = data['update_time']
863
+ if isinstance(update_time, (int, float)):
864
+ time_str = format_timestamp(update_time)
865
+ else:
866
+ time_str = update_time
867
+ formatted += f"\n⏰ **Mise à jour**: {time_str}\n"
868
+
869
+ except Exception as e:
870
+ logger.error(f"Erreur formatage alertes: {e}")
871
+ formatted = f"❌ Erreur formatage alertes: {e}\n\n"
872
+ formatted += f"Données brutes: {json.dumps(data, indent=2, ensure_ascii=False)[:500]}..."
873
+
874
+ return formatted
875
+
876
+ def get_weather_forecast(address: str):
877
+ """
878
+ Récupère les prévisions météo complètes pour une adresse française.
879
+
880
+ Effectue la géolocalisation via IGN, récupère les données météo via l'API officielle
881
+ Météo-France, et formate les résultats pour l'affichage avec alertes, conditions
882
+ actuelles et prévisions.
883
+
884
+ Args:
885
+ address (str): Adresse française à géolocaliser (ex: "Brest, France", "75001 Paris", "Nice")
886
+
887
+ Returns:
888
+ tuple: Quatre éléments formatés en markdown:
889
+ - alerts_info (str): Alertes météo avec niveaux de vigilance (vert/jaune/orange/rouge)
890
+ - current_conditions (str): Conditions actuelles (température, vent, précipitations)
891
+ - hourly_forecast (str): Prévisions horaires sur 24h avec données de pluie
892
+ - daily_forecast (str): Prévisions quotidiennes sur 10 jours
893
+
894
+ Note:
895
+ Utilise les APIs officielles françaises :
896
+ - IGN Géoplateforme pour la géolocalisation
897
+ - Météo-France pour les données météorologiques
898
+
899
+ Examples:
900
+ >>> alerts, current, hourly, daily = get_weather_forecast("Paris, France")
901
+ >>> print(current) # Affiche température, vent, etc.
902
+ """
903
+ logger.info(f"🚀 Début de la requête pour: {address}")
904
+
905
+ if not address.strip():
906
+ logger.warning("⚠️ Adresse vide fournie")
907
+ error_msg = "❌ Veuillez saisir une adresse"
908
+ return error_msg, error_msg, error_msg, error_msg
909
+
910
+ # Géocodage de l'adresse
911
+ geocoder = GeocodingAPI()
912
+ geocoding_result = geocoder.geocode_address(address)
913
+
914
+ if not geocoding_result:
915
+ error_msg = "❌ Impossible de localiser cette adresse"
916
+ logger.error(f"❌ Géocodage échoué pour: {address}")
917
+ return error_msg, error_msg, error_msg, error_msg
918
+
919
+ lat = geocoding_result["lat"]
920
+ lon = geocoding_result["lon"]
921
+ logger.info(f"✅ Coordonnées obtenues: lat={lat}, lon={lon}")
922
+
923
+ # Récupération des données météo
924
+ meteo_api = MeteoFranceAPI()
925
+ weather_data = meteo_api.get_location_forecast(lat, lon)
926
+ current_weather = meteo_api.get_current_observation(lat, lon)
927
+ rain_forecast = meteo_api.get_rain_forecast(lat, lon)
928
+
929
+ # Utiliser le département depuis l'API forecast pour les alertes
930
+ department = None
931
+ if weather_data and "position" in weather_data:
932
+ department = weather_data["position"].get("dept")
933
+
934
+ if department:
935
+ logger.info(f"🏷️ Utilisation département depuis API forecast: {department}")
936
+ wind_alerts = meteo_api.get_wind_alerts_by_department(department)
937
+ else:
938
+ logger.warning("⚠️ Pas de département trouvé, utilisation géolocalisation")
939
+ wind_alerts = meteo_api.get_wind_alerts(lat, lon)
940
+
941
+ # Formatage des résultats
942
+ alerts_info = format_alerts_data(wind_alerts)
943
+ current_conditions = format_current_conditions(current_weather, rain_forecast, weather_data)
944
+ hourly_forecast = format_hourly_forecast(weather_data, rain_forecast)
945
+ daily_forecast = format_daily_forecast(weather_data)
946
+
947
+ logger.info("✅ Requête terminée avec succès")
948
+ return alerts_info, current_conditions, hourly_forecast, daily_forecast
949
+
950
+ # Fonctions individuelles pour MCP
951
+ def get_weather_alerts(address: str) -> str:
952
+ """
953
+ Récupère les alertes météorologiques pour une adresse française.
954
+
955
+ Retourne les niveaux de vigilance Météo-France (vert/jaune/orange/rouge)
956
+ avec détail des phénomènes dangereux.
957
+
958
+ Args:
959
+ address (str): Adresse française à analyser (ex: "Paris, France", "29200 Brest")
960
+
961
+ Returns:
962
+ str: Alertes météo formatées avec niveaux de vigilance et phénomènes actifs
963
+ """
964
+ alerts_info, _, _, _ = get_weather_forecast(address)
965
+ return alerts_info
966
+
967
+ def get_current_weather(address: str) -> str:
968
+ """
969
+ Récupère les conditions météorologiques actuelles pour une adresse.
970
+
971
+ Fournit température, vent, précipitations, humidité, pression et visibilité
972
+ en temps réel depuis les stations Météo-France.
973
+
974
+ Args:
975
+ address (str): Adresse française à analyser (ex: "Nice", "75001 Paris")
976
+
977
+ Returns:
978
+ str: Conditions actuelles détaillées avec données de vent et précipitations
979
+ """
980
+ _, current_conditions, _, _ = get_weather_forecast(address)
981
+ return current_conditions
982
+
983
+ def get_hourly_forecast(address: str) -> str:
984
+ """
985
+ Récupère les prévisions météorologiques horaires sur 24h.
986
+
987
+ Prévisions détaillées heure par heure avec température, météo,
988
+ vent, rafales et précipitations intégrées.
989
+
990
+ Args:
991
+ address (str): Adresse française à analyser (ex: "Toulouse", "13001 Marseille")
992
+
993
+ Returns:
994
+ str: Prévisions horaires sur 24h avec toutes les données météo
995
+ """
996
+ _, _, hourly_forecast, _ = get_weather_forecast(address)
997
+ return hourly_forecast
998
+
999
+ def get_daily_forecast(address: str) -> str:
1000
+ """
1001
+ Récupère les prévisions météorologiques quotidiennes sur 10 jours.
1002
+
1003
+ Prévisions étendues avec températures minimales et maximales,
1004
+ conditions générales et données de vent quotidiennes.
1005
+
1006
+ Args:
1007
+ address (str): Adresse française à analyser (ex: "Bordeaux", "69001 Lyon")
1008
+
1009
+ Returns:
1010
+ str: Prévisions quotidiennes sur 10 jours avec températures et météo
1011
+ """
1012
+ _, _, _, daily_forecast = get_weather_forecast(address)
1013
+ return daily_forecast
1014
+
1015
+ def get_complete_weather_forecast(address: str) -> str:
1016
+ """
1017
+ Récupère un rapport météorologique complet pour une adresse française.
1018
+
1019
+ Combine alertes, conditions actuelles, prévisions horaires et quotidiennes
1020
+ en un rapport unifié depuis les APIs officielles françaises.
1021
+
1022
+ Args:
1023
+ address (str): Adresse française à analyser (ex: "Brest", "06000 Nice")
1024
+
1025
+ Returns:
1026
+ str: Rapport météo complet avec alertes, conditions actuelles et prévisions
1027
+ """
1028
+ alerts_info, current_conditions, hourly_forecast, daily_forecast = get_weather_forecast(address)
1029
+
1030
+ complete_report = f"""
1031
+ {alerts_info}
1032
+
1033
+ {current_conditions}
1034
+
1035
+ {hourly_forecast}
1036
+
1037
+ {daily_forecast}
1038
+ """
1039
+ return complete_report
1040
+
1041
+ def get_weather_emoji(description: str) -> str:
1042
+ """
1043
+ Retourne l'émoji météo approprié selon la description textuelle.
1044
+
1045
+ Args:
1046
+ description (str): Description météo en français (ex: "ensoleillé", "nuageux")
1047
+
1048
+ Returns:
1049
+ str: Émoji Unicode correspondant à la condition météo
1050
+ (☀️, ☁️, 🌧️, ⛈️, ❄️, 🌫️, ⛅, 🌤️)
1051
+ """
1052
+ description = description.lower()
1053
+ if "ensoleill" in description or "clair" in description:
1054
+ return "☀️"
1055
+ elif "nuage" in description:
1056
+ return "☁️"
1057
+ elif "pluie" in description or "averse" in description:
1058
+ return "🌧️"
1059
+ elif "orage" in description:
1060
+ return "⛈️"
1061
+ elif "neige" in description:
1062
+ return "❄️"
1063
+ elif "brouillard" in description:
1064
+ return "🌫️"
1065
+ elif "éclaircies" in description:
1066
+ return "⛅"
1067
+ else:
1068
+ return "🌤️"
1069
+
1070
+ def create_wind_compass(direction: int, speed: float) -> str:
1071
+ """
1072
+ Génère une boussole HTML interactive pour visualiser la direction du vent.
1073
+
1074
+ Crée un élément SVG-like en HTML/CSS avec flèche orientée selon la direction
1075
+ et couleur selon la vitesse du vent.
1076
+
1077
+ Args:
1078
+ direction (int): Direction du vent en degrés (0-360°, 0° = Nord)
1079
+ speed (float): Vitesse du vent en km/h
1080
+
1081
+ Returns:
1082
+ str: Code HTML de la boussole avec flèche orientée et colorée:
1083
+ - Vert: < 10 km/h
1084
+ - Orange: 10-20 km/h
1085
+ - Rouge: 20-30 km/h
1086
+ - Violet: > 30 km/h
1087
+ """
1088
+ if not direction or direction < 0:
1089
+ direction = 0
1090
+
1091
+ # Convertir en radians pour le CSS transform
1092
+ rotation = direction - 90 # Ajuster pour que 0° = Nord
1093
+
1094
+ # Couleur selon la vitesse
1095
+ if speed < 10:
1096
+ color = "#4CAF50" # Vert
1097
+ elif speed < 20:
1098
+ color = "#FF9800" # Orange
1099
+ elif speed < 30:
1100
+ color = "#F44336" # Rouge
1101
+ else:
1102
+ color = "#9C27B0" # Violet
1103
+
1104
+ compass_html = f"""
1105
+ <div style="display: inline-block; position: relative; width: 60px; height: 60px;
1106
+ border: 2px solid {color}; border-radius: 50%; margin: 0 10px;">
1107
+ <!-- Boussole background -->
1108
+ <div style="position: absolute; top: 2px; left: 50%; transform: translateX(-50%);
1109
+ font-size: 8px; color: {color}; font-weight: bold;">N</div>
1110
+ <div style="position: absolute; bottom: 2px; left: 50%; transform: translateX(-50%);
1111
+ font-size: 8px; color: {color}; font-weight: bold;">S</div>
1112
+ <div style="position: absolute; left: 2px; top: 50%; transform: translateY(-50%);
1113
+ font-size: 8px; color: {color}; font-weight: bold;">O</div>
1114
+ <div style="position: absolute; right: 2px; top: 50%; transform: translateY(-50%);
1115
+ font-size: 8px; color: {color}; font-weight: bold;">E</div>
1116
+
1117
+ <!-- Flèche du vent -->
1118
+ <div style="position: absolute; top: 50%; left: 50%;
1119
+ width: 3px; height: 20px; background: {color};
1120
+ transform: translate(-50%, -50%) rotate({rotation}deg);
1121
+ transform-origin: center; border-radius: 2px;">
1122
+ <!-- Pointe de la flèche -->
1123
+ <div style="position: absolute; top: -3px; left: 50%;
1124
+ transform: translateX(-50%);
1125
+ width: 0; height: 0;
1126
+ border-left: 4px solid transparent;
1127
+ border-right: 4px solid transparent;
1128
+ border-bottom: 6px solid {color};"></div>
1129
+ </div>
1130
+
1131
+ <!-- Centre -->
1132
+ <div style="position: absolute; top: 50%; left: 50%;
1133
+ width: 4px; height: 4px; background: {color};
1134
+ border-radius: 50%; transform: translate(-50%, -50%);"></div>
1135
+ </div>
1136
+ """
1137
+ return compass_html
1138
+
1139
+ def create_wind_compass_from_text(wind_text: str) -> str:
1140
+ """
1141
+ Extrait vitesse et direction du vent depuis un texte formaté et génère une boussole.
1142
+
1143
+ Parse le texte au format "X.X km/h - Y° [direction]" pour extraire
1144
+ les valeurs numériques et créer la visualisation boussole.
1145
+
1146
+ Args:
1147
+ wind_text (str): Texte formaté de vent (ex: "12.5 km/h - 270° O")
1148
+
1149
+ Returns:
1150
+ str: Code HTML de boussole générée, ou chaîne vide si parsing échoue
1151
+ """
1152
+ import re
1153
+
1154
+ # Regex pour extraire vitesse et direction: "2.8 km/h - 322° NO"
1155
+ match = re.search(r'(\d+\.?\d*)\s*km/h.*?(\d+)°', wind_text)
1156
+ if match:
1157
+ speed = float(match.group(1))
1158
+ direction = int(match.group(2))
1159
+ return create_wind_compass(direction, speed)
1160
+ return ""
1161
+
1162
+
1163
+ def format_alerts_card_html(alerts_info: str) -> str:
1164
+ """
1165
+ Convertit les alertes météo formatées en carte HTML stylisée.
1166
+
1167
+ Applique les styles CSS appropriés selon le niveau d'alerte détecté
1168
+ dans le texte (rouge, orange, jaune, ou standard).
1169
+
1170
+ Args:
1171
+ alerts_info (str): Alertes formatées en markdown depuis format_alerts_data()
1172
+
1173
+ Returns:
1174
+ str: Carte HTML avec classe CSS appropriée au niveau d'alerte:
1175
+ - alert-card-red: Alerte rouge
1176
+ - alert-card-orange: Alerte orange
1177
+ - alert-card-yellow: Alerte jaune
1178
+ - weather-card: Aucune alerte ou erreur
1179
+ """
1180
+ if "❌" in alerts_info:
1181
+ return f'<div class="weather-card">{alerts_info}</div>'
1182
+
1183
+ # Déterminer la classe CSS selon le niveau d'alerte
1184
+ if "ROUGE" in alerts_info.upper():
1185
+ card_class = "alert-card-red"
1186
+ elif "ORANGE" in alerts_info.upper():
1187
+ card_class = "alert-card-orange"
1188
+ elif "JAUNE" in alerts_info.upper():
1189
+ card_class = "alert-card-yellow"
1190
+ else:
1191
+ card_class = "weather-card"
1192
+
1193
+ # Nettoyer le texte markdown
1194
+ clean_text = alerts_info.replace("**", "<strong>").replace("**", "</strong>")
1195
+ clean_text = clean_text.replace("\n", "<br>")
1196
+
1197
+ return f'<div class="{card_class}">{clean_text}</div>'
1198
+
1199
+ def format_current_card_html(current_conditions: str) -> str:
1200
+ """
1201
+ Convertit les conditions actuelles en carte HTML avec layout optimisé.
1202
+
1203
+ Parse les données markdown pour extraire température, localisation, météo,
1204
+ vent et précipitations, puis génère une carte avec boussole de vent intégrée.
1205
+
1206
+ Args:
1207
+ current_conditions (str): Conditions actuelles formatées depuis format_current_conditions()
1208
+
1209
+ Returns:
1210
+ str: Carte HTML avec layout responsive affichant:
1211
+ - Localisation et description météo
1212
+ - Température avec émoji large
1213
+ - Boussole de vent interactive
1214
+ - Informations de précipitations
1215
+ """
1216
+ if "❌" in current_conditions:
1217
+ return f'<div class="weather-card">{current_conditions}</div>'
1218
+
1219
+ # Extraire les informations principales
1220
+ lines = current_conditions.split("\n")
1221
+ location = "Localisation inconnue"
1222
+ temp = "--"
1223
+ weather_desc = "--"
1224
+ wind = "--"
1225
+ observation_time = "--"
1226
+ precipitation = "Données non disponibles"
1227
+
1228
+ for line in lines:
1229
+ if "Lieu**:" in line:
1230
+ location = line.split(":", 1)[1].strip()
1231
+ elif "Température**:" in line:
1232
+ temp = line.split(":", 1)[1].strip()
1233
+ elif "Temps**:" in line:
1234
+ weather_desc = line.split(":", 1)[1].strip().split()[0] # Premier mot
1235
+ elif "Vent**:" in line:
1236
+ wind = line.split(":", 1)[1].strip()
1237
+ elif "Observation**:" in line:
1238
+ observation_time = line.split(":", 1)[1].strip()
1239
+ elif "Précipitations**:" in line:
1240
+ precipitation = line.split(":", 1)[1].strip()
1241
+
1242
+ weather_emoji = get_weather_emoji(weather_desc)
1243
+ temp_num = temp.replace("°C", "")
1244
+
1245
+ html = f"""
1246
+ <div class="current-card">
1247
+ <div style="display: flex; justify-content: space-between; align-items: center;">
1248
+ <div style="flex: 1;">
1249
+ <div style="font-size: 1.1em; opacity: 0.8;">{location}</div>
1250
+ <div style="font-size: 0.9em; opacity: 0.7;">{weather_desc}</div>
1251
+ <div style="font-size: 0.8em; opacity: 0.7; margin-top: 2px;">💧 {precipitation}</div>
1252
+ <div style="font-size: 0.8em; opacity: 0.6; margin-top: 4px;">{observation_time}</div>
1253
+ </div>
1254
+ <div style="display: flex; align-items: center; gap: 15px;">
1255
+ <!-- Vent à droite -->
1256
+ <div style="text-align: center; font-size: 0.85em;">
1257
+ <div style="opacity: 0.8; margin-bottom: 5px;">🌬️ Vent</div>
1258
+ <div style="font-weight: bold;">{wind.split(' - ')[0] if ' - ' in wind else wind}</div>
1259
+ {create_wind_compass_from_text(wind)}
1260
+ </div>
1261
+ <!-- Température -->
1262
+ <div style="text-align: center;">
1263
+ <div style="font-size: 2.5em;">{weather_emoji}</div>
1264
+ <div class="temp-large">{temp_num}°</div>
1265
+ </div>
1266
+ </div>
1267
+ </div>
1268
+ </div>
1269
+ """
1270
+ return html
1271
+
1272
+ def format_hourly_card_html(hourly_forecast: str) -> str:
1273
+ """
1274
+ Convertit les prévisions horaires en carte HTML avec grille horizontale.
1275
+
1276
+ Parse les données markdown pour créer une grille de prévisions horaires
1277
+ avec émojis météo, températures et vitesses de vent.
1278
+
1279
+ Args:
1280
+ hourly_forecast (str): Prévisions horaires formatées depuis format_hourly_forecast()
1281
+
1282
+ Returns:
1283
+ str: Carte HTML avec grille d'éléments horaires (max 12h):
1284
+ - Heure, émoji météo, température, vitesse vent
1285
+ - Layout responsive avec items arrondis
1286
+ """
1287
+ if "❌" in hourly_forecast:
1288
+ return f'<div class="weather-card">{hourly_forecast}</div>'
1289
+
1290
+ # Parser les données horaires
1291
+ lines = hourly_forecast.split("\n")
1292
+ hourly_items = []
1293
+
1294
+ i = 0
1295
+ while i < len(lines):
1296
+ line = lines[i].strip()
1297
+ if "**" in line and ":" in line: # Ligne d'heure
1298
+ time_part = line.split("**")[1].split(":")[0]
1299
+ temp_weather = line.split(": ", 1)[1] if ": " in line else "--"
1300
+
1301
+ # Récupérer le vent sur la ligne suivante
1302
+ wind_info = "--"
1303
+ if i + 1 < len(lines) and "Vent:" in lines[i + 1]:
1304
+ wind_line = lines[i + 1].strip()
1305
+ # Extraire seulement la vitesse du vent
1306
+ if "km/h" in wind_line:
1307
+ wind_speed = wind_line.split()[1] if len(wind_line.split()) > 1 else "--"
1308
+ wind_info = f"{wind_speed} km/h"
1309
+
1310
+ temp = temp_weather.split(" - ")[0] if " - " in temp_weather else "--"
1311
+ weather = temp_weather.split(" - ")[1] if " - " in temp_weather else "--"
1312
+
1313
+ hourly_items.append({
1314
+ "time": time_part,
1315
+ "temp": temp,
1316
+ "weather": weather,
1317
+ "wind": wind_info
1318
+ })
1319
+ i += 1
1320
+
1321
+ # Générer le HTML
1322
+ items_html = ""
1323
+ for item in hourly_items[:12]: # Prendre les 12 premières heures
1324
+ emoji = get_weather_emoji(item["weather"])
1325
+ items_html += f"""
1326
+ <div class="hourly-item">
1327
+ <div style="font-weight: bold; min-width: 50px;">{item["time"]}</div>
1328
+ <div style="font-size: 1.3em; margin: 0 8px;">{emoji}</div>
1329
+ <div style="font-weight: bold; min-width: 45px;">{item["temp"]}</div>
1330
+ <div style="font-size: 0.8em; opacity: 0.8; min-width: 50px; text-align: right;">{item["wind"]}</div>
1331
+ </div>
1332
+ """
1333
+
1334
+ html = f"""
1335
+ <div class="hourly-card">
1336
+ <div style="font-size: 1.3em; margin-bottom: 12px;"><strong>🕐 Prochaines heures</strong></div>
1337
+ {items_html}
1338
+ </div>
1339
+ """
1340
+ return html
1341
+
1342
+ def format_daily_card_html(daily_forecast: str) -> str:
1343
+ """
1344
+ Convertit les prévisions quotidiennes en carte HTML compacte.
1345
+
1346
+ Parse les prévisions sur 10 jours pour créer une liste verticale
1347
+ avec émojis météo et plages de températures.
1348
+
1349
+ Args:
1350
+ daily_forecast (str): Prévisions quotidiennes formatées depuis format_daily_forecast()
1351
+
1352
+ Returns:
1353
+ str: Carte HTML avec liste de jours (max 7 affichés):
1354
+ - Jour, émoji météo, températures min/max
1355
+ - Items avec fond semi-transparent
1356
+ """
1357
+ if "❌" in daily_forecast:
1358
+ return f'<div class="weather-card">{daily_forecast}</div>'
1359
+
1360
+ # Parser les données quotidiennes
1361
+ lines = daily_forecast.split("\n")
1362
+ daily_items = []
1363
+
1364
+ i = 0
1365
+ while i < len(lines):
1366
+ line = lines[i].strip()
1367
+ if "**" in line and "°" in line: # Ligne de jour
1368
+ day_part = line.split("**")[1].split(":")[0]
1369
+ temps_part = line.split(": ", 1)[1] if ": " in line else "--"
1370
+
1371
+ # Récupérer la météo sur la ligne suivante
1372
+ weather_desc = "--"
1373
+ if i + 1 < len(lines) and lines[i + 1].strip():
1374
+ weather_desc = lines[i + 1].strip()
1375
+
1376
+ daily_items.append({
1377
+ "day": day_part,
1378
+ "temps": temps_part,
1379
+ "weather": weather_desc
1380
+ })
1381
+ i += 1
1382
+
1383
+ # Générer le HTML
1384
+ items_html = ""
1385
+ for item in daily_items[:7]: # Prendre les 7 premiers jours
1386
+ emoji = get_weather_emoji(item["weather"])
1387
+ items_html += f"""
1388
+ <div class="daily-item">
1389
+ <div style="font-weight: bold; min-width: 55px; font-size: 0.9em;">{item["day"]}</div>
1390
+ <div style="font-size: 1.3em; margin: 0 8px;">{emoji}</div>
1391
+ <div style="font-weight: bold; text-align: right; font-size: 0.9em;">{item["temps"]}</div>
1392
+ </div>
1393
+ """
1394
+
1395
+ html = f"""
1396
+ <div class="daily-card">
1397
+ <div style="font-size: 1.3em; margin-bottom: 12px;"><strong>📅 Prochains jours</strong></div>
1398
+ {items_html}
1399
+ </div>
1400
+ """
1401
+ return html
1402
+
1403
+ # Fonctions HTML pour les cartes visuelles (conservées)
1404
+ def get_weather_forecast_html(address: str):
1405
+ """
1406
+ Version HTML des prévisions météo avec cartes visuelles.
1407
+
1408
+ Args:
1409
+ address (str): Adresse à géolocaliser (ex: "Paris, France")
1410
+
1411
+ Returns:
1412
+ tuple: (alerts_html, current_html, hourly_html, daily_html)
1413
+ - alerts_html: Carte HTML des alertes météo
1414
+ - current_html: Carte HTML des conditions actuelles
1415
+ - hourly_html: Carte HTML des prévisions horaires
1416
+ - daily_html: Carte HTML des prévisions sur 10 jours
1417
+ """
1418
+ # Récupérer les données météo formatées
1419
+ alerts_info, current_conditions, hourly_forecast, daily_forecast = get_weather_forecast(address)
1420
+
1421
+ # Convertir en cartes HTML stylisées
1422
+ alerts_html = format_alerts_card_html(alerts_info)
1423
+ current_html = format_current_card_html(current_conditions)
1424
+ hourly_html = format_hourly_card_html(hourly_forecast)
1425
+ daily_html = format_daily_card_html(daily_forecast)
1426
+
1427
+ return alerts_html, current_html, hourly_html, daily_html
1428
+
1429
+ # Interface Gradio pour MCP avec cartes visuelles
1430
+ def create_mcp_interface():
1431
+ """
1432
+ Crée l'interface Gradio pour le serveur MCP avec cartes visuelles et fonctions individuelles.
1433
+
1434
+ Combine la belle interface avec cartes HTML et les fonctions MCP séparées :
1435
+ - Interface web: Cartes visuelles avec dégradés et boussoles
1436
+ - Fonctions MCP: 5 outils texte individuels pour Claude Desktop
1437
+
1438
+ Returns:
1439
+ gr.Blocks: Interface Gradio hybride optimisée pour MCP et web
1440
+ """
1441
+ # CSS personnalisé pour le style cartes météo
1442
+ custom_css = """
1443
+ .gradio-container {
1444
+ max-width: 900px !important;
1445
+ margin: 0 auto !important;
1446
+ }
1447
+
1448
+ .weather-card {
1449
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1450
+ border-radius: 20px;
1451
+ padding: 20px;
1452
+ margin: 8px 0;
1453
+ color: white;
1454
+ box-shadow: 0 8px 32px rgba(0,0,0,0.1);
1455
+ backdrop-filter: blur(10px);
1456
+ width: 100%;
1457
+ }
1458
+
1459
+ .alert-card-red {
1460
+ background: linear-gradient(135deg, #ff416c 0%, #ff4757 100%);
1461
+ border-radius: 20px;
1462
+ padding: 20px;
1463
+ margin: 8px 0;
1464
+ color: white;
1465
+ box-shadow: 0 8px 32px rgba(255,65,108,0.3);
1466
+ border: 2px solid #ff4757;
1467
+ width: 100%;
1468
+ }
1469
+
1470
+ .alert-card-orange {
1471
+ background: linear-gradient(135deg, #f39c12 0%, #e67e22 100%);
1472
+ border-radius: 20px;
1473
+ padding: 20px;
1474
+ margin: 8px 0;
1475
+ color: white;
1476
+ box-shadow: 0 8px 32px rgba(243,156,18,0.3);
1477
+ width: 100%;
1478
+ }
1479
+
1480
+ .alert-card-yellow {
1481
+ background: linear-gradient(135deg, #f1c40f 0%, #f39c12 100%);
1482
+ border-radius: 20px;
1483
+ padding: 20px;
1484
+ margin: 8px 0;
1485
+ color: white;
1486
+ box-shadow: 0 8px 32px rgba(241,196,15,0.3);
1487
+ width: 100%;
1488
+ }
1489
+
1490
+ .current-card {
1491
+ background: linear-gradient(135deg, #2c3e50 0%, #3498db 100%);
1492
+ border-radius: 20px;
1493
+ padding: 20px;
1494
+ margin: 8px 0;
1495
+ color: white;
1496
+ box-shadow: 0 8px 32px rgba(52,152,219,0.2);
1497
+ width: 100%;
1498
+ }
1499
+
1500
+ .hourly-card {
1501
+ background: linear-gradient(135deg, #8e44ad 0%, #3498db 100%);
1502
+ border-radius: 20px;
1503
+ padding: 20px;
1504
+ margin: 8px 0;
1505
+ color: white;
1506
+ box-shadow: 0 8px 32px rgba(142,68,173,0.2);
1507
+ width: 100%;
1508
+ }
1509
+
1510
+ .daily-card {
1511
+ background: linear-gradient(135deg, #27ae60 0%, #2ecc71 100%);
1512
+ border-radius: 20px;
1513
+ padding: 20px;
1514
+ margin: 8px 0;
1515
+ color: white;
1516
+ box-shadow: 0 8px 32px rgba(46,204,113,0.2);
1517
+ width: 100%;
1518
+ }
1519
+
1520
+ .temp-large {
1521
+ font-size: 2.5em;
1522
+ font-weight: bold;
1523
+ margin: 5px 0;
1524
+ }
1525
+
1526
+ .hourly-item {
1527
+ display: flex;
1528
+ justify-content: space-between;
1529
+ align-items: center;
1530
+ padding: 8px 12px;
1531
+ margin: 4px 0;
1532
+ background: rgba(255,255,255,0.1);
1533
+ border-radius: 12px;
1534
+ font-size: 0.9em;
1535
+ }
1536
+
1537
+ .daily-item {
1538
+ display: flex;
1539
+ justify-content: space-between;
1540
+ align-items: center;
1541
+ padding: 10px 12px;
1542
+ margin: 4px 0;
1543
+ background: rgba(255,255,255,0.1);
1544
+ border-radius: 12px;
1545
+ }
1546
+ """
1547
+
1548
+ with gr.Blocks(
1549
+ title="🌦️ Météo France - Serveur MCP",
1550
+ theme=gr.themes.Soft(),
1551
+ css=custom_css
1552
+ ) as interface:
1553
+
1554
+ gr.Markdown("# 🌦️ Météo France")
1555
+ gr.Markdown("""
1556
+ Prévisions météorologiques détaillées avec alertes
1557
+
1558
+ **5 fonctions MCP disponibles :**
1559
+ `get_weather_alerts` | `get_current_weather` | `get_hourly_forecast` | `get_daily_forecast` | `get_complete_weather_forecast`
1560
+ """)
1561
+
1562
+ # Interface principale avec cartes visuelles
1563
+ with gr.Row():
1564
+ address_input = gr.Textbox(
1565
+ label="📍 Adresse",
1566
+ placeholder="Entrez une adresse (ex: Paris, France)",
1567
+ scale=3
1568
+ )
1569
+ submit_btn = gr.Button("🔄 Actualiser", variant="primary", scale=1)
1570
+
1571
+ # Alertes en premier (priorité haute)
1572
+ with gr.Row():
1573
+ alerts_output = gr.HTML(label="🚨 Alertes Météo")
1574
+
1575
+ # Conditions actuelles
1576
+ with gr.Row():
1577
+ current_output = gr.HTML(label="🌡️ Maintenant")
1578
+
1579
+ # Prévisions heure par heure
1580
+ with gr.Row():
1581
+ hourly_output = gr.HTML(label="🕐 Heure par Heure")
1582
+
1583
+ # Prévisions à 10 jours
1584
+ with gr.Row():
1585
+ daily_output = gr.HTML(label="📅 10 Jours")
1586
+
1587
+ # Connexions interface web (cartes HTML)
1588
+ submit_btn.click(
1589
+ fn=get_weather_forecast_html,
1590
+ inputs=[address_input],
1591
+ outputs=[alerts_output, current_output, hourly_output, daily_output]
1592
+ )
1593
+
1594
+ address_input.submit(
1595
+ fn=get_weather_forecast_html,
1596
+ inputs=[address_input],
1597
+ outputs=[alerts_output, current_output, hourly_output, daily_output]
1598
+ )
1599
+
1600
+ # Boutons invisibles pour exposer les fonctions MCP
1601
+ # Ces boutons créent les endpoints API nécessaires pour MCP
1602
+ with gr.Row(visible=False):
1603
+ mcp_input = gr.Textbox()
1604
+ mcp_output = gr.Textbox()
1605
+
1606
+ alerts_mcp_btn = gr.Button("MCP Alerts")
1607
+ current_mcp_btn = gr.Button("MCP Current")
1608
+ hourly_mcp_btn = gr.Button("MCP Hourly")
1609
+ daily_mcp_btn = gr.Button("MCP Daily")
1610
+ complete_mcp_btn = gr.Button("MCP Complete")
1611
+
1612
+ # Connexions MCP (invisibles mais exposées via API)
1613
+ alerts_mcp_btn.click(
1614
+ fn=get_weather_alerts,
1615
+ inputs=mcp_input,
1616
+ outputs=mcp_output,
1617
+ show_api=True,
1618
+ api_name="get_weather_alerts"
1619
+ )
1620
+
1621
+ current_mcp_btn.click(
1622
+ fn=get_current_weather,
1623
+ inputs=mcp_input,
1624
+ outputs=mcp_output,
1625
+ show_api=True,
1626
+ api_name="get_current_weather"
1627
+ )
1628
+
1629
+ hourly_mcp_btn.click(
1630
+ fn=get_hourly_forecast,
1631
+ inputs=mcp_input,
1632
+ outputs=mcp_output,
1633
+ show_api=True,
1634
+ api_name="get_hourly_forecast"
1635
+ )
1636
+
1637
+ daily_mcp_btn.click(
1638
+ fn=get_daily_forecast,
1639
+ inputs=mcp_input,
1640
+ outputs=mcp_output,
1641
+ show_api=True,
1642
+ api_name="get_daily_forecast"
1643
+ )
1644
+
1645
+ complete_mcp_btn.click(
1646
+ fn=get_complete_weather_forecast,
1647
+ inputs=mcp_input,
1648
+ outputs=mcp_output,
1649
+ show_api=True,
1650
+ api_name="get_complete_weather_forecast"
1651
+ )
1652
+
1653
+ # Crédit Météo-France obligatoire
1654
+ with gr.Row():
1655
+ gr.HTML("""
1656
+ <div style="text-align: center; margin-top: 20px; padding: 15px;
1657
+ background: rgba(0,0,0,0.05); border-radius: 10px;
1658
+ font-size: 0.9em; color: #666;">
1659
+ 📡 <strong>Données météorologiques</strong> fournies par
1660
+ <a href="https://meteofrance.fr" target="_blank" style="color: #0066cc; text-decoration: none;">
1661
+ <strong>Météo-France</strong>
1662
+ </a> |
1663
+ 🗺️ <strong>Géolocalisation</strong> par
1664
+ <a href="https://geoservices.ign.fr" target="_blank" style="color: #0066cc; text-decoration: none;">
1665
+ <strong>IGN Géoplateforme</strong>
1666
+ </a>
1667
+ </div>
1668
+ """)
1669
+
1670
+ return interface
1671
+
1672
+ if __name__ == "__main__":
1673
+ interface = create_mcp_interface()
1674
+ interface.launch(mcp_server=True)