7 minute(s) de lecture

ESP32météo Cet article détaille comment interroger une API de météorologie avec un micro-contrôleur ESP32-C3 équipé d’une puce wifi donc. J’ai naturellement utilisé la nouvelle PYBSTICK-ESP32-C3 de Garatronic, que l’on retrouve en vente sur le shop de McHobby. ELle a l’avantage d’être minuscule, et d’embarquer 2 boutons poussoirs ainsi qu’une led RGB bien pratiques pour mon futur projet.

configuration

Il faut dans un premier temps configurer le micro-contrôleur pour qu’il accède au WIFI. Tout est expliqué dans cet article dans la section WIFI. Créez le programme boot.py à la racine du micro-contrôleur afin qu’il puisse se connecter au WIFI à chaque reboot, avec la led RGB comme témoin de couleur:

  • bleu: non connecté.
  • bleu clignotant: en cours de connection.
  • vert: connecté
  • rouge: erreur de connection
############################
# Connexion Wifi
############################
WIFI_SSID = "MON_SSID"
WIFI_PASSWORD = "MON_PASSWORD_WIFI"
MAX_ITERATION = 20

import time, network
from RGBLed import RGB_led

#RGB led setup
led = RGB_led()
led.color((0,0,64)) #blue color

#wifi setup
station = network.WLAN(network.STA_IF)
station.active(True)

print("Scanning for WiFi networks, please wait...")
authmodes = ['Open', 'WEP', 'WPA-PSK' 'WPA2-PSK4', 'WPA/WPA2-PSK']
for (ssid, bssid, channel, RSSI, authmode, hidden) in station.scan():
  print("* {:s}".format(ssid))

#try to connect to WiFi
nb_iteration=0
while not station.isconnected():
    # Try to connect to WiFi access point
    print("Connecting...")
    try:
        station.connect(WIFI_SSID, WIFI_PASSWORD)
    except:
        pass
    nb_iteration+=1
    if (nb_iteration==MAX_ITERATION):
        break
    time.sleep(0.5)
    led.blink((0,0,64))

led.color((0,0,64)) #blue color

API météo

J’utilise l’API de météo fournie par meteo-concept. Il y a l’offre “basique” gratuite qui permet d’interroger la météo 500 fois par jour max avec les prévisions sur les 12 prochaines heures, pour n’importe quelle ville: c’est largement suffisant, on peut mettre en place un service d’interrogation toutes les 3mn par exemple qui peut ainsi tourner h24 7J/7.

Pour pouvoir interroger cette API il faut obtenir un token (une grande chaîne de caractères) qui sera par la suite recopiée dans le code ci-dessous.

Il faut aussi aller rechercher le code INSEE de votre ville (une chaîne de 5 nombres): à ne pas confondre avec le code postal (plusieurs villes partagent le même code postal). Pour l’obtenir, c’est par ici.

code micro-python

Créons un nouveau programme sur l’ESP32-C3, nommez-le comme bon vous semble (si vous le nommez main.py il sera exécuté à chaque redémarrage après le boot)

bibliothèques

Il faut d’abord importer les bibliothèques suivantes:

import urequests as requests
import json

test wifi

Nous allons ensuite tester la connection WIFI, allumer la led RGB en conséquence, afficher l’adresse IP.

# Display connection details
if station.isconnected():
    print("Connected!")
    print("My IP Address:", station.ifconfig()[0])
    led.color((64,0,0)) #green color
else:
    print("connection to wifi failed!")
    led.color((0,54,0)) #red color

classe Meteo()

Cette classe va contenir les méthodes qui vont interroger l’API et fournir des données météorologiques pour une ville donnée (code INSEE) avec une prévisions heure par heure (forecast)

Pensez à bien replacer “MON-TOKEN” par le token que vous avez récupéré lors de votre inscription.

Dans cette classe on retrouve:

  • self.dic_STATUS: Un dictionnaire qui permet de décoder les codes retour de l’API
  • self.dic_WEATHER: un dictionnaire des codes de bulletin météo fourni par l’API.
  • get_forecast(self): méthode qui met à jour la prévision météo sur les 12 prochaines heures dans self.forecast.
  • decode_detail_meteo(self, id_forecast=0): méthode qui décode la variable self.forecast et retourne l’heure de prédiction, le bulletin, la température, l’humidité, la probabilité qu’il pleuve, et la vitesse moyenne du vent.
class Meteo():
    """ recup données API meto https://api.meteo-concept.com/ 
    """
    def __init__(self, insee=None):
        """ constructeur """
        self.insee = insee       # code INSEE de la ville à récupérer sous https://public.opendatasoft.com/explore/dataset/correspondance-code-insee-code-postal/table/
        self.forecast = None     # prévisions météo forecast des 12 prochaines heures
        
        #Token à obtenir sur API meto https://api.meteo-concept.com/ 
        self.mytoken = "MON-TOKEN" 
        
        self.URLAPI   = 'https://api.meteo-concept.com/api'
        self.GETHOURS = '/forecast/nextHours'
        self.HOURLY   = '&hourly=true' #GET forecast every  hour
        self.TOKEN  = '?token='+self.mytoken
        self.SEARCH = '&insee='+self.insee
        self.api_meteo_url = self.URLAPI+self.GETHOURS+self.TOKEN+self.HOURLY+self.SEARCH
        
        #--dictionnaire des codes retours API
        self.dic_STATUS= {
            200: "Ok",
            400: "Paramètre manquant ou valeur incorrecte: vérifier le token",
            401: "Échec de l'authentification: token absent ou invalide",
            403: "Action non autorisée",
            404: "URL inconnue",
            500: "Erreur interne au serveur",
            503: "API momentanément indisponible",
            }
        
        #--dictionnaire des bulletins météo de l'api meteo-concept
        self.dic_WEATHER = {
            0: "Soleil",
            1: "Peu nuageux",
            2: "Ciel voilé",
            3: "Nuageux",
            4: "Très nuageux",
            5: "Couvert",
            6: "Brouillard",
            7: "Brouillard givrant",
            10: "Pluie faible",
            11: "Pluie modérée",
            12: "Pluie forte",
            13: "Pluie faible verglaçante",
            14: "Pluie modérée verglaçante",
            15: "Pluie forte verglaçante",
            16: "Bruine",
            20: "Neige faible",
            21: "Neige modérée",
            22: "Neige forte",
            30: "Pluie et neige mêlées faibles",
            31: "Pluie et neige mêlées modérées",
            32: "Pluie et neige mêlées fortes",
            40: "Averses de pluie locales et faibles",
            41: "Averses de pluie locales",
            42: "Averses locales et fortes",
            43: "Averses de pluie faibles",
            44: "Averses de pluie",
            45: "Averses de pluie fortes",
            46: "Averses de pluie faibles et fréquentes",
            47: "Averses de pluie fréquentes",
            48: "Averses de pluie fortes et fréquentes",
            60: "Averses de neige localisées et faibles",
            61: "Averses de neige localisées",
            62: "Averses de neige localisées et fortes",
            63: "Averses de neige faibles",
            64: "Averses de neige",
            65: "Averses de neige fortes",
            66: "Averses de neige faibles et fréquentes",
            67: "Averses de neige fréquentes",
            68: "Averses de neige fortes et fréquentes",
            70: "Averses de pluie et neige mêlées localisées et faibles",
            71: "Averses de pluie et neige mêlées localisées",
            72: "Averses de pluie et neige mêlées localisées et fortes",
            73: "Averses de pluie et neige mêlées faibles",
            74: "Averses de pluie et neige mêlées",
            75: "Averses de pluie et neige mêlées fortes",
            76: "Averses de pluie et neige mêlées faibles et nombreuses",
            77: "Averses de pluie et neige mêlées fréquentes",
            78: "Averses de pluie et neige mêlées fortes et fréquentes",
            100: "Orages faibles et locaux",
            101: "Orages locaux",
            102: "Orages fort et locaux",
            103: "Orages faibles",
            104: "Orages",
            105: "Orages forts",
            106: "Orages faibles et fréquents",
            107: "Orages fréquents",
            108: "Orages forts et fréquents",
            120: "Orages faibles et locaux de neige ou grésil",
            121: "Orages locaux de neige ou grésil",
            122: "Orages locaux de neige ou grésil",
            123: "Orages faibles de neige ou grésil",
            124: "Orages de neige ou grésil",
            125: "Orages de neige ou grésil",
            126: "Orages faibles et fréquents de neige ou grésil",
            127: "Orages fréquents de neige ou grésil",
            128: "Orages fréquents de neige ou grésil",
            130: "Orages faibles et locaux de pluie et neige mêlées ou grésil",
            131: "Orages locaux de pluie et neige mêlées ou grésil",
            132: "Orages fort et locaux de pluie et neige mêlées ou grésil",
            133: "Orages faibles de pluie et neige mêlées ou grésil",
            134: "Orages de pluie et neige mêlées ou grésil",
            135: "Orages forts de pluie et neige mêlées ou grésil",
            136: "Orages faibles et fréquents de pluie et neige mêlées ou grésil",
            137: "Orages fréquents de pluie et neige mêlées ou grésil",
            138: "Orages forts et fréquents de pluie et neige mêlées ou grésil",
            140: "Pluies orageuses",
            141: "Pluie et neige mêlées à caractère orageux",
            142: "Neige à caractère orageux",
            210: "Pluie faible intermittente",
            211: "Pluie modérée intermittente",
            212: "Pluie forte intermittente",
            220: "Neige faible intermittente",
            221: "Neige modérée intermittente",
            222: "Neige forte intermittente",
            230: "Pluie et neige mêlées",
            231: "Pluie et neige mêlées",
            232: "Pluie et neige mêlées",
            235: "Averses de grêle",
            }

    def get_forecast(self):
        ''' appel API: fourni le forecast des 12 prochaines heures sur un code insee
        '''
        #appel API
        self.forecast, status = None, None
        try:
            weather_data = requests.get(self.api_meteo_url)
        except:
            print('Pas de connexion WIFI')
            return status

        #code retour appel API
        status = weather_data.status_code
        if (status != 200):
            print('code retour API:', status, self.dic_STATUS[status])
            print('Pb connexion API: vérifier le token ou la connexion WIFI:')
        else:
            self.forecast = weather_data.json()['forecast']
            self.city = weather_data.json()['city']['name']
        return status
    
    def decode_detail_meteo(self, id_forecast=0):
        ''' décode les données météo des forecast obtenus
            id_forecast=0: heure en cours
        '''
        h, buletin, temp, hum , probarain, wind = None, None, None, None, None, None
        #on vérifie que l'id-forecast ne dépasse pas le nombre de forecast retournés.
        if (id_forecast >= len(self.forecast)):
            id_forecast = 0
        h = self.forecast[id_forecast]['datetime']          # heure prévision
        buletin = self.forecast[id_forecast]['weather']     # code buletin 
        temp = self.forecast[id_forecast]['temp2m']         # température
        hum = self.forecast[id_forecast]['rh2m']            # humidité
        probarain = self.forecast[id_forecast]['probarain'] # probabilité pluie
        wind = self.forecast[id_forecast]['wind10m']        # vitesse moyenne du vent
        return h, buletin, temp, hum, probarain, wind

boucle principale

Il suffit ensuite d’instancier un objet de classe Meteo() avec en paramètre le code INSEE de votre ville. La boucle principale va décoder le retour de l’API et si le status est OK afficher les valeurs météorologiques. Dans cet exemple je recommence toutes les 5 secondes mais avec l’offre basique il faudra veiller à patienter 3mn entre chaque interrogation.

meteo = Meteo(insee='MON CODE INSEE')
while True:
    status = meteo.get_forecast(); # obtention du forecast pour les 12 prochaines heures
    if (status == 200) :           # retour API 200 =  ok
        h, buletin, temp, hum, probarain, wind = meteo.decode_detail_meteo(id_forecast=0)
        print('prévision météo', h, 'à', meteo.city, ':', meteo.dic_WEATHER[buletin], ',',
              temp, '°C, humidité', hum,'%, proba pluie',probarain,'% , vent moyen', wind,'km/h' )
    time.sleep(5)  # wait 5 secondes

Si vous avez suivi mes précédentes aventures vous aurez compris que je fais faire une nouvelle version de mon pimometre avec un ESP32-C3 à la place d’un raspberry pi zero.