Utiliser le protocole HTTP avec OpenWeather

L’ensemble des sketchs de notre projet peuvent être téléchargés ici.

Grâce à l’API OpenWeather notre station météo pourra publier six prévisions météo pour le lieu que vous préciserez et pour les 16 à 18 heures à venir. Le sketch « openweather_querying.ino » apporte cette amélioration, adaptée du tutoriel situé ici.

Dans les déclarations globales nous avons ajouté les lignes suivantes :

0157 // Macro : est-ce que l'année est bissextile ?
0158 #define LPYR(year) ((((year) % 4 == 0) && ((year) % 100 != 0)) || ((year) % 400 == 0))
0159 
0160 // Nb de jours par mois
0161 static uint8_t RTC_Months[2][12] = {
0162   {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, // Année non-bissextile
0163   {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}  // Année bissextile
0164 };
0165 
0166 // Coordonnées géodésiques de l'emplacement de la station
0167 #define LATITUDE (48.858370) // degrés décimaux
0168 #define LONGITUDE (2.294481) // degrés décimaux
0169 #define ALTITUDE (391) // mètres
0170 
0171 // Structure pour ranger les informations géographiques
0172 struct Location {
0173   float Latitude;
0174   float Longitude;
0175   float Altitude;
0176 };
0177 
0178 struct Location MyLocation = {LATITUDE, LONGITUDE, ALTITUDE};
0179 
0180 // Déclarations pour les requêtes à OpenWeather
0181 
0182 #include <ArduinoJson.h> // Bibliothèque pour le parsing des chaînes JSON
0183 #define OWM_KEY "myOWMkey" // Clef fournie par le site Open Weather
0184 #define OWM_SERVER "api.openweathermap.org" // Serveur OpenWeather
0185 #define OWM_SERVER_PORT (80)
0186 
0187 char OWMKey[] = OWM_KEY;
0188 char owmServer[] = OWM_SERVER;
0189 uint32_t timezone = -1; // Décalage horaire complet (saisonnier + méridien) en s
0190 const uint8_t owmFcst = 6; // Rapporte 6 prévisions météos espacées de 3 heures
0191 
0192 struct Forecast {
0193   char date_time[19];
0194   char description[40];
0195 };

D’après la ligne 182, on aura besoin d’une bibliothèque Arduino supplémentaire, ArduinoJson pour extraire les informations météo des chaînes de caractère au format JSON renvoyées par l’API OpenWeather.

Les lignes 157 à 164 seront utilisées pour convertir en un format lisible les informations de dates – temps en provenance d’OpenWeather, grâce à la fonction GetDateTimeFromUnix, dont le code est reproduit plus loin, copié depuis cette source.

Les lignes 166 à 176 regroupent les informations géographiques pour la localisation de la station (supposée se trouver au sommet de la tour Eiffel pour notre exemple) dans une structure MyLocation renseignée avec des constantes déclarées à l’aide de la directive de préprocesseur #define.

Les lignes 180 à 195 définissent les variables et constantes pour l’interrogation de l’API OpenWeather. Vous trouverez ici les instructions pour obtenir la clef qui doit remplacer « myOWMkey » à la ligne 183.

La variable timezone (ligne 189) contiendra le nombre de secondes à ajouter ou à soustraire au temps UTC pour obtenir l’heure locale correcte incluant les corrections méridienne et saisonnière. Elle est initialisée à la valeur - 1 qui ne pourra en aucun cas provenir d’une réponse de l’API OpenWeather. Ainsi, si n’importe où dans le code, nous lisons par la suite la valeur de timezone et obtenons -1 cela signifiera que l’interrogation de OpenWeather n’a pas eu lieu ou bien a échoué.

Nous n’avons apporté aucune modification à la fonction setup.

Dans la fonction loop, nous avons ajouté la ligne 270, qui appelle la fonction getWeatherForecast :

0260 void loop() {
0261 
0262   if (rtc_mod == SET_RTC_MOD) { // Toutes les SET_RTC_MOD itérations...
0263 
0264     rtc_mod = 0;
0265 
0266     // Re-connexion du module Wi-Fi au réseau spécifié
0267     Connect_WiFi(&Serial);
0268 
0269     // Récupère les informations météo et le temps local avec Open Weather
0270     timezone = getWeatherForecast(&Serial);
0271 
0272     // Acquisition NTP de la date et de l'heure de la mesure, recalage de la RTC
0273     Set_RTC_Time(&Serial);
0274 
0275     // Poste à nouveau les trames MQTT éventuellement sauvegardées sur la carte SD
0276     Post_SD_Records(logfilename, &Serial, MQTT_Payload);
0277 
0278     // Ré-arme l'IDWG
0279     IWatchdog.reload();
0280 
0281   } // Clôture de if (rtc_mod == SET_RTC_MOD)
0282 


0322 
0323   // Mise en veille (mode "STOP") avant l'itération suivante
0324   delay(5);
0325   LowPower.deepSleep(main_loop_delay);
0326 }

Le code pour obtenir les prévisions météo est contenu dans getWeatherForecast :

1082 uint32_t getWeatherForecast(HardwareSerial *serial) {
1083 
1084   uint32_t tzone = 0;
1085 
1086   serial->println("\nConnecting to " + (String)owmServer);
1087 
1088   // Connexion au serveur OpenWeather
1089   if (espClient.connect(owmServer, OWM_SERVER_PORT)) {
1090 
1091     // Requête HTTP au serveur OpenWeather
1092     espClient.print("GET /data/2.5/forecast?");
1093     espClient.print("lat=" + String(MyLocation.Latitude));
1094     espClient.print("&lon=" + String(MyLocation.Longitude));
1095     espClient.print("&appid=" + (String)OWMKey);
1096     espClient.print("&cnt=" + String(owmFcst));
1097     espClient.println("&units=metric HTTP/1.1");
1098     espClient.println("Host: " + (String)owmServer);
1099     espClient.println("Connection: close");
1100     espClient.println();
1101 
1102     // Attend la réponse du serveur
1103     uint32_t _startMillis = millis();
1104     while (!espClient.available() && (millis() - _startMillis < DELAY_5S));
1105 
1106     // Lecture de la réponse du serveur dans l'objet String owmServerResponse
1107     String owmServerResponse = "";
1108     while (espClient.available()) {
1109       owmServerResponse = espClient.readStringUntil('\n');
1110     }
1111 
1112     // Si la réponse du serveur n'est pas nulle...
1113     if (owmServerResponse.length()) {
1114 
1115       // serial->println("Raw meteo data (JSON) : " + owmServerResponse);
1116 
1117       // Crée un buffer pour accueillir des données indexées au format JSON
1118       StaticJsonDocument<5000> JsonDoc;
1119 
1120       // Convertit le contenu de owmServerResponse en JSON
1121       DeserializationError error = deserializeJson(JsonDoc, owmServerResponse);
1122 
1123       // Si conversion OK, extrait de JsonDoc les champs qui nous intéressent
1124       if (!error) {
1125 
1126         // Pression au niveau de la mer
1127         String sealvl = JsonDoc["list"][0]["main"]["sea_level"];
1128         // Agglomération la plus proche
1129         String place = JsonDoc["city"]["name"];
1130         // Pays
1131         String country = JsonDoc["city"]["country"];
1132         // Heure lever de soleil, au format EPOCH
1133         String sunrise = JsonDoc["city"]["sunrise"];
1134         // Heure coucher de soleil, au format EPOCH
1135         String sunset = JsonDoc["city"]["sunset"];
1136 
1137         // time_zone contiendra le nombre de secondes à ajouter ou sosutraire
1138         // au temps UTC pour obtenir l'heure locale correcte incluant
1139         // les corrections méridienne et saisonnière.
1140         String time_zone = JsonDoc["city"]["timezone"];
1141         tzone = time_zone.toInt();
1142 
1143         uint32_t Sunrise_time = sunrise.toInt();
1144         uint32_t Sunset_time = sunset.toInt();
1145 
1146         // Convertit les dates & heures au format UNIX UTC (EPOCH)
1147         //en versions lisibles
1148 
1149         struct DateTime sunrise_date_time;
1150         GetDateTimeFromUnix(&sunrise_date_time, Sunrise_time);
1151         char srdt[18];
1152         Make_TimeStamp(srdt, &sunrise_date_time);
1153 
1154         struct DateTime sunset_date_time;
1155         GetDateTimeFromUnix(&sunset_date_time, Sunset_time);
1156         char ssdt[18];
1157         Make_TimeStamp(ssdt, &sunset_date_time);
1158 
1159         // Affiche les données météo sur le port série
1160         serial->println("\nClosest town : " + place + " (" + country + ")" );
1161         serial->println("Time zone shift, including daylight saving time : "
1162                         + time_zone + "s");
1163         serial->println("Sunrise : " + (String)srdt);
1164         serial->println("Sunset : " + (String)ssdt);
1165         serial->println("Pressure : " + sealvl + " hPa");
1166         serial->println("\nForecasted weather conditions :");
1167 
1168         // Tableau de structures Forecast pour charger les owmFcst prévisions
1169         struct Forecast weather_forecast[owmFcst];
1170 
1171         struct DateTime forecast_date_time;
1172 
1173         for (uint8_t i = 0; i < owmFcst; i++) {
1174 
1175           String nextWeatherTime = JsonDoc["list"][i]["dt"];
1176           String nextWeatherDescription =
1177             JsonDoc["list"][i]["weather"][0]["description"];
1178 
1179           uint32_t nextWeather_time = nextWeatherTime.toInt();
1180           GetDateTimeFromUnix(&forecast_date_time, nextWeather_time);
1181           char sdt[18];
1182           Make_TimeStamp(sdt, &forecast_date_time);
1183           strcpy(weather_forecast[i].date_time, sdt);
1184 
1185           nextWeatherDescription.toCharArray(weather_forecast[i].description, 39);
1186 
1187           serial->println(" On " + (String)weather_forecast[i].date_time
1188                           + " : " + nextWeatherDescription);
1189         }
1190       }
1191       else {
1192         serial->println("deserializeJson() failed");
1193       }
1194     }
1195     else {
1196       serial->println("No response from server");
1197     }
1198   } else {
1199     serial->println("Unable to connect to server");
1200   }
1201   return tzone;
1202 }

L’API OpenWeather est un moyen efficace pour obtenir les corrections d’heure locale, d’origine méridienne et saisonnière. L’information time_zone (ligne 1140) contient le nombre de secondes à ajouter au temps universel coordonné pour calculer l’heure locale. La fonction getWeatherForecast renvoie cette correction (ligne 1201).

La fonction Make_Local_TimeStamp déjà présentée est modifiée en conséquence :

0840 void Make_Local_TimeStamp(char* timestamp, uint32_t UTC_time, HardwareSerial *serial) {
0841 
0842   struct DateTime date_time;
0843 
0844   if (timezone == -1) {  // Si on n'a pas obtenu les infos de OpenWeather
0845     date_time.Year = rtc.getYear();
0846     date_time.Month = rtc.getMonth();
0847     date_time.Day = rtc.getDay();
0848     date_time.Hours = ((rtc.getHours() + (byte)GMT_OFFSET + dst_offset) % 24);
0849     date_time.Minutes = rtc.getMinutes();
0850     date_time.Seconds = rtc.getSeconds();
0851   }
0852   else {  // Si on a obtenu les infos de OpenWeather
0853     uint32_t Local_time = UTC_time + timezone;
0854     GetDateTimeFromUnix(&date_time, Local_time);
0855   }
0856 
0857   Make_TimeStamp(timestamp, &date_time);
0858 }

Si l’interrogation de OpenWeather a réussi (ligne 844) alors on peut calculer le temps local directement en ajoutant timestamp à UTC_time (ligne 853) puis en le convertissant avec GetDateTimeFromUnix. Autrement on retrouve l’algorithme utilisé ici (lignes 844 à 851).

Aux lignes 854, 1150, 1155 et 1180, on fait appel à la fonction GetDateTimeFromUnix qui transforme une date au format Epoch UNIX en une structure DateTime lisible :

1207 int GetDateTimeFromUnix(DateTime* data, uint32_t unix) {
1208 
1209   uint16_t year;
1210 
1211   /* Get seconds from unix */
1212   data->Seconds = unix % 60;
1213   /* Go to minutes */
1214   unix /= 60;
1215   /* Get minutes */
1216   data->Minutes = unix % 60;
1217   /* Go to hours */
1218   unix /= 60;
1219   /* Get hours */
1220   data->Hours = unix % 24;
1221   /* Go to days */
1222   unix /= 24;
1223 
1224   /* Get week day */
1225   /* Monday is day one */
1226   data->WeekDay = (unix + 3) % 7 + 1;
1227 
1228   /* Get year */
1229   year = 1970;
1230   while (true) {
1231     if (LPYR(year)) {
1232       if (unix >= 366) {
1233         unix -= 366;
1234       } else {
1235         break;
1236       }
1237     } else if (unix >= 365) {
1238       unix -= 365;
1239     } else {
1240       break;
1241     }
1242     year++;
1243   }
1244   /* Get year in xx format */
1245   data->Year = (uint8_t) (year - 2000);
1246   /* Get month */
1247   for (data->Month = 0; data->Month < 12; data->Month++) {
1248     if (LPYR(year)) {
1249       if (unix >= (uint32_t)RTC_Months[1][data->Month]) {
1250         unix -= RTC_Months[1][data->Month];
1251       } else {
1252         break;
1253       }
1254     } else if (unix >= (uint32_t)RTC_Months[0][data->Month]) {
1255       unix -= RTC_Months[0][data->Month];
1256     } else {
1257       break;
1258     }
1259   }
1260   /* Get month */
1261   /* Month starts with 1 */
1262   data->Month++;
1263   /* Get date */
1264   /* Date starts with 1 */
1265   data->Day = unix + 1;
1266 
1267   /* Return OK */
1268   return 1;
1269 }

Pour finir, voici une copie des messages envoyés par getWeatherForecast (lignes 1660 à 1188) sur le port série :

Closest town : Neuilly-sur-Seine (FR)
Time zone shift, including daylight saving time : 3600s
Sunrise : 21/01/01 07:44:16
Sunset : 21/01/01 16:04:20
Pressure : 1011 hPa

Forecasted weather conditions :
On 21/01/01 18:00:00 : scattered clouds
On 21/01/01 21:00:00 : clear sky
On 21/01/02 00:00:00 : clear sky
On 21/01/02 03:00:00 : clear sky
On 21/01/02 06:00:00 : clear sky
On 21/01/02 09:00:00 : clear sky

En l’état, la station n’exploite pas ces informations ; une évolution intéressante (et complémentaire du tableau de bord ThingsBoard) consisterait à lui rajouter un écran e-Paper UART de ce type et à réaliser de jolis graphiques afin que les prévisions météo soient accessibles d’un coup d’œil.