324 lines
13 KiB
Python
324 lines
13 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Vue synthétique combinée : Qualité de l'air et Pollen pour Nancy
|
||
================================================================
|
||
|
||
Ce script combine les données d'indices ATMO (qualité de l'air) et
|
||
d'indices Pollen pour la ville de Nancy (54395) afin de fournir
|
||
une vue synthétique de la situation environnementale.
|
||
|
||
Fonctionnalités:
|
||
- Récupération simultanée des indices ATMO et Pollen
|
||
- Analyse croisée qualité air / risque pollinique
|
||
- Vue synthétique avec recommandations
|
||
- Affichage avec émojis et couleurs
|
||
- Détection des risques combinés
|
||
"""
|
||
|
||
import sys
|
||
import os
|
||
from datetime import datetime
|
||
|
||
# Ajouter le répertoire parent au PYTHONPATH pour importer le package local
|
||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
|
||
from atmo_data_wrapper import AtmoDataClient, AASQA_CODES
|
||
|
||
def get_risk_level_combined(atmo_code: int, pollen_max: int, pollen_alert: bool) -> dict:
|
||
"""Détermine le niveau de risque combiné air + pollen"""
|
||
|
||
# Conversion des codes en niveaux de risque (0-4)
|
||
atmo_risk = min(atmo_code, 4) if atmo_code > 0 else 0
|
||
pollen_risk = min(pollen_max, 4) if pollen_max > 0 else 0
|
||
|
||
# Bonus si alerte pollen active
|
||
if pollen_alert:
|
||
pollen_risk = min(pollen_risk + 1, 4)
|
||
|
||
# Calcul du risque combiné (moyenne pondérée)
|
||
combined_risk = int((atmo_risk * 0.6 + pollen_risk * 0.4))
|
||
|
||
risk_levels = {
|
||
0: {"level": "Très faible", "emoji": "🟢", "color": "#50F0E6", "advice": "Conditions excellentes"},
|
||
1: {"level": "Faible", "emoji": "🟡", "color": "#50CCAA", "advice": "Conditions favorables"},
|
||
2: {"level": "Modéré", "emoji": "🟠", "color": "#F0E641", "advice": "Prudence pour personnes sensibles"},
|
||
3: {"level": "Élevé", "emoji": "🔴", "color": "#FF5050", "advice": "Éviter activités extérieures prolongées"},
|
||
4: {"level": "Très élevé", "emoji": "🟣", "color": "#A020F0", "advice": "Rester à l'intérieur si possible"}
|
||
}
|
||
|
||
result = risk_levels.get(combined_risk, risk_levels[2])
|
||
result["risk_score"] = combined_risk
|
||
result["atmo_contribution"] = atmo_risk
|
||
result["pollen_contribution"] = pollen_risk
|
||
|
||
return result
|
||
|
||
def format_pollutants_summary(atmo_indice) -> str:
|
||
"""Formate un résumé des polluants"""
|
||
summary = atmo_indice.get_pollutants_summary()
|
||
parts = []
|
||
|
||
for polluant, data in summary.items():
|
||
if data['code'] > 2: # Seulement les polluants problématiques
|
||
parts.append(f"{polluant}({data['code']})")
|
||
|
||
return ", ".join(parts) if parts else "Aucun polluant problématique"
|
||
|
||
def format_pollens_summary(pollen_indice) -> str:
|
||
"""Formate un résumé des pollens"""
|
||
summary = pollen_indice.get_pollens_summary()
|
||
parts = []
|
||
|
||
for code_taxon, data in summary.items():
|
||
if data['code'] >= 3: # Pollens avec indice moyen à élevé
|
||
espece = data['espece']
|
||
niveau = data['code']
|
||
emoji = data['emoji']
|
||
parts.append(f"{espece} {emoji}({niveau})")
|
||
|
||
return ", ".join(parts) if parts else "Tous pollens à niveau faible"
|
||
|
||
def get_detailed_recommendations(atmo_indice, pollen_indice, combined_risk: dict) -> list:
|
||
"""Génère des recommandations détaillées"""
|
||
recommendations = []
|
||
|
||
# Recommandations basées sur la qualité de l'air
|
||
if atmo_indice.is_poor_quality():
|
||
worst_pol, code = atmo_indice.get_worst_pollutant()
|
||
recommendations.append(f"🌬️ Qualité air dégradée ({worst_pol}: niveau {code})")
|
||
|
||
if code >= 4:
|
||
recommendations.append(" → Éviter le sport en extérieur")
|
||
recommendations.append(" → Aérer pendant les pics de trafic")
|
||
|
||
# Recommandations basées sur les pollens
|
||
if pollen_indice.is_alert_active():
|
||
recommendations.append("🌸 Alerte pollen active")
|
||
recommendations.append(" → Éviter sorties tôt le matin")
|
||
recommendations.append(" → Garder fenêtres fermées")
|
||
|
||
dangerous_pollens = pollen_indice.get_dangerous_pollens()
|
||
if dangerous_pollens:
|
||
pollens_str = ", ".join(dangerous_pollens)
|
||
recommendations.append(f"🤧 Pollens à risque élevé: {pollens_str}")
|
||
recommendations.append(" → Prévoir antihistaminiques si nécessaire")
|
||
|
||
# Recommandations combinées
|
||
if combined_risk["risk_score"] >= 3:
|
||
recommendations.append("⚠️ Risque combiné élevé")
|
||
recommendations.append(" → Limiter activités extérieures")
|
||
recommendations.append(" → Privilégier intérieur climatisé/filtré")
|
||
|
||
# Recommandations positives
|
||
if combined_risk["risk_score"] <= 1:
|
||
recommendations.append("✅ Conditions favorables")
|
||
recommendations.append(" → Profitez des activités extérieures")
|
||
recommendations.append(" → Idéal pour aérer le logement")
|
||
|
||
return recommendations
|
||
|
||
def display_synthesis_header():
|
||
"""Affiche l'en-tête de la synthèse"""
|
||
print("=" * 70)
|
||
print("🌍 SYNTHÈSE ENVIRONNEMENTALE - Nancy")
|
||
print("=" * 70)
|
||
print(f"📅 Date: {datetime.now().strftime('%d/%m/%Y à %H:%M')}")
|
||
print(f"📍 Ville: Nancy (54395) - Région Grand Est")
|
||
print()
|
||
|
||
def display_section_header(title: str, emoji: str = "📊"):
|
||
"""Affiche un en-tête de section"""
|
||
print(f"{emoji} {title}")
|
||
print("-" * 50)
|
||
|
||
def main():
|
||
"""Fonction principale"""
|
||
display_synthesis_header()
|
||
|
||
try:
|
||
# Connexion au client
|
||
print("🔗 Connexion à l'API Atmo Data...")
|
||
client = AtmoDataClient()
|
||
success = client.auto_login()
|
||
|
||
if not success:
|
||
print("❌ Échec de la connexion")
|
||
return
|
||
|
||
print("✅ Connexion réussie")
|
||
print()
|
||
|
||
# === RÉCUPÉRATION DES DONNÉES ===
|
||
print("📥 Récupération des données...")
|
||
|
||
# Données ATMO spécifiques à Nancy (code INSEE 54395)
|
||
atmo_data = client.get_indices_atmo(
|
||
code_zone="54395", # Code INSEE de Nancy
|
||
format="geojson"
|
||
)
|
||
|
||
# Données Pollen pour Nancy
|
||
pollen_data = client.get_indices_pollens(
|
||
code_zone="54395",
|
||
format="geojson"
|
||
)
|
||
|
||
if not atmo_data or len(atmo_data) == 0:
|
||
print("❌ Aucune donnée ATMO trouvée pour Nancy (54395)")
|
||
print("ℹ️ Essai avec les données régionales...")
|
||
|
||
# Fallback: récupérer les données régionales du Grand Est
|
||
atmo_data = client.get_indices_atmo(aasqa="44", format="geojson")
|
||
|
||
if not atmo_data or len(atmo_data) == 0:
|
||
print("❌ Aucune donnée ATMO trouvée pour la région Grand Est")
|
||
return
|
||
else:
|
||
print(f"✅ Données régionales récupérées ({len(atmo_data)} stations)")
|
||
|
||
if not pollen_data or len(pollen_data) == 0:
|
||
print("❌ Aucune donnée Pollen trouvée pour Nancy")
|
||
return
|
||
|
||
# Sélection des données pertinentes
|
||
# Pour ATMO: prendre la première donnée (spécifique à Nancy si disponible)
|
||
atmo_indice = atmo_data[0]
|
||
|
||
# Pour Pollen: prendre la première donnée
|
||
pollen_indice = pollen_data[0]
|
||
|
||
print(f"✅ Données récupérées - ATMO: {atmo_indice.lib_zone}, Pollen: {pollen_indice.lib_zone}")
|
||
print()
|
||
|
||
# === ANALYSE QUALITÉ DE L'AIR ===
|
||
display_section_header("QUALITÉ DE L'AIR", "🌬️")
|
||
|
||
print(f"Zone: {atmo_indice.lib_zone}")
|
||
print(f"Indice global: {atmo_indice.get_qualificatif()} {atmo_indice.get_emoji()} (niveau {atmo_indice.code_qual})")
|
||
|
||
hex_color, rgb_color = atmo_indice.get_color()
|
||
print(f"Couleur: {hex_color}")
|
||
|
||
# Polluants problématiques
|
||
worst_pol, worst_code = atmo_indice.get_worst_pollutant()
|
||
print(f"Polluant principal: {worst_pol} (niveau {worst_code})")
|
||
|
||
# Résumé des polluants
|
||
pollutants_summary = format_pollutants_summary(atmo_indice)
|
||
print(f"Polluants élevés: {pollutants_summary}")
|
||
|
||
# Coordonnées et données réglementaires
|
||
if atmo_indice.has_coordinates():
|
||
print(f"Coordonnées: {atmo_indice.coordinates}")
|
||
|
||
print(f"Type de zone: {atmo_indice.type_zone or 'Non spécifié'}")
|
||
print(f"Source: {atmo_indice.get_source()}")
|
||
print()
|
||
|
||
# === ANALYSE POLLEN ===
|
||
display_section_header("INDICES POLLEN", "🌸")
|
||
|
||
print(f"Zone: {pollen_indice.lib_zone}")
|
||
print(f"Alerte active: {'✅ OUI' if pollen_indice.is_alert_active() else '❌ Non'}")
|
||
|
||
# Pollen le plus élevé
|
||
highest_pollen, highest_code = pollen_indice.get_highest_pollen()
|
||
if highest_code > 0:
|
||
highest_name = pollen_indice.get_pollens_summary()[highest_pollen]['espece']
|
||
highest_emoji = pollen_indice.get_pollens_summary()[highest_pollen]['emoji']
|
||
print(f"Pollen dominant: {highest_name} {highest_emoji} (niveau {highest_code})")
|
||
else:
|
||
print("Pollen dominant: Aucun pollen détecté")
|
||
|
||
# Taxons responsables selon l'API
|
||
responsible_pollens = pollen_indice.get_responsible_pollens()
|
||
if responsible_pollens:
|
||
print(f"Taxons responsables: {', '.join(responsible_pollens)}")
|
||
|
||
# Résumé des pollens élevés
|
||
pollens_summary = format_pollens_summary(pollen_indice)
|
||
print(f"Pollens à surveiller: {pollens_summary}")
|
||
|
||
# Concentration la plus élevée
|
||
highest_conc_taxon, highest_conc_value = pollen_indice.get_highest_concentration()
|
||
if highest_conc_value > 0:
|
||
conc_name = pollen_indice.get_pollens_summary()[highest_conc_taxon]['espece']
|
||
print(f"Concentration max: {conc_name} ({highest_conc_value:.1f} grains/m³)")
|
||
|
||
print(f"Source: {pollen_indice.get_source()}")
|
||
print()
|
||
|
||
# === ANALYSE COMBINÉE ===
|
||
display_section_header("RISQUE COMBINÉ", "⚖️")
|
||
|
||
# Calcul du risque combiné
|
||
combined_risk = get_risk_level_combined(
|
||
atmo_indice.code_qual,
|
||
highest_code,
|
||
pollen_indice.is_alert_active()
|
||
)
|
||
|
||
print(f"Niveau de risque: {combined_risk['level']} {combined_risk['emoji']}")
|
||
print(f"Score global: {combined_risk['risk_score']}/4")
|
||
print(f" • Contribution air: {combined_risk['atmo_contribution']}/4")
|
||
print(f" • Contribution pollen: {combined_risk['pollen_contribution']}/4")
|
||
print(f"Conseil général: {combined_risk['advice']}")
|
||
print()
|
||
|
||
# === RECOMMANDATIONS ===
|
||
display_section_header("RECOMMANDATIONS", "💡")
|
||
|
||
recommendations = get_detailed_recommendations(atmo_indice, pollen_indice, combined_risk)
|
||
|
||
if recommendations:
|
||
for i, rec in enumerate(recommendations, 1):
|
||
print(f"{rec}")
|
||
else:
|
||
print("✅ Aucune recommandation particulière")
|
||
print(" Conditions normales pour toutes activités")
|
||
|
||
print()
|
||
|
||
# === RÉSUMÉ TECHNIQUE ===
|
||
display_section_header("INFORMATIONS TECHNIQUES", "🔬")
|
||
|
||
print("Qualité de l'air:")
|
||
print(f" • Date échéance: {atmo_indice.date_ech}")
|
||
print(f" • Date diffusion: {atmo_indice.date_dif}")
|
||
print(f" • AASQA: {atmo_indice.get_aasqa_name()}")
|
||
|
||
# Concentrations si disponibles
|
||
concentrations = atmo_indice.get_concentrations()
|
||
significant_conc = {k: v for k, v in concentrations.items() if v > 0}
|
||
if significant_conc:
|
||
print(" • Concentrations (μg/m³):")
|
||
for polluant, conc in significant_conc.items():
|
||
print(f" - {polluant}: {conc}")
|
||
|
||
print()
|
||
print("Pollen:")
|
||
all_concentrations = pollen_indice.get_concentrations()
|
||
significant_pollen = {k: v for k, v in all_concentrations.items() if v > 0}
|
||
if significant_pollen:
|
||
print(" • Concentrations (grains/m³):")
|
||
for taxon, conc in significant_pollen.items():
|
||
taxon_name = pollen_indice.get_pollens_summary()[taxon]['espece']
|
||
print(f" - {taxon_name}: {conc:.1f}")
|
||
|
||
print()
|
||
|
||
# === FOOTER ===
|
||
print("=" * 70)
|
||
print("🌱 Données fournies par Atmo France et les AASQA")
|
||
print("📊 Synthèse générée par Atmo Data Wrapper")
|
||
print("=" * 70)
|
||
|
||
except Exception as e:
|
||
print(f"❌ Erreur lors de l'exécution: {e}")
|
||
import traceback
|
||
print("\nDétails de l'erreur:")
|
||
print(traceback.format_exc())
|
||
sys.exit(1)
|
||
|
||
if __name__ == "__main__":
|
||
main()
|