ESP32でお天気ステーションを作ろう

Pocket

ESP32でお天気ステーションを作ろう

Wi-Fi接続ができ、ネット上の情報とリンクして様々な動きができる便利なマイコンのESP32。今回はこのマイコンを使って、天気予報と現在の正確な時間を表示するお天気ステーションを作っていきます!

動いている様子

画面上部に常に現在時刻が表示されており、1ページ目には現在の天気が、2ページ目には12時間後の天気が表示されます。寝る前に見ると明日の天気が分かったりして普通に便利です。

前提となる知識・記事

実行環境

ソフトウェア関連

  • windows10 home
  • Arduino 1.8.5
  • ESP32 core for arduino 1.0.0

使用部品

回路

ESP32のピン配置

ソースコード(コピペで動く)

以下のソースコード中の

  • ssid
  • password
  • key

はご自分の環境に合わせて変更してください。

#include "SSD1306Wire.h"
#include "OLEDDisplayUi.h"
#include <stdio.h>
#include <ArduinoJson.h>
#include <HTTPClient.h>
#include <Ticker.h>

Ticker tickerWeatherUpdate;

SSD1306Wire  display(0x3c, 21, 22);
OLEDDisplayUi ui( &display );


const char* ssid = "YOURSSID";
const char* password =  "PASSWORD";

//NTP関連
struct tm timeInfo;
char date[20],hour_minute_sec[20];

//天気関連
String now_weather,now_temp,tom_weather,tom_temp;
const String endpoint = "http://api.openweathermap.org/data/2.5/weather?q=Tokyo,jp&APPID=";
const String endpoint_forecast = "http://api.openweathermap.org/data/2.5/forecast?q=Tokyo,jp&APPID=";
const String key = "YOURKEY";

//12時間後の天気を取得する関数
void get_tomorrow_weather(){
  HTTPClient http;
 
    http.begin(endpoint_forecast + key); //URLを指定
    int httpCode = http.GET();  //GETリクエストを送信
 
    if (httpCode > 0) { //返答がある場合
 
        String payload = http.getString();  //返答(JSON形式)を取得

        //jsonオブジェクトの作成
        DynamicJsonBuffer jsonBuffer;
        String json = payload;
        JsonObject& weatherdata = jsonBuffer.parseObject(json);

        //パースが成功したかどうかを確認
        if(!weatherdata.success()){
          Serial.println("parseObject() failed");
        }
        
        //抜き出すデータの番号
        int data_number = now_hour/3 + 4;
        //12時間後のデータを抜き出し
        const char* weather = weatherdata["list"][data_number]["weather"][0]["main"].as<char*>();
        const double temp = weatherdata["list"][data_number]["main"]["temp"].as<double>();
        //表示用変数に各要素をセット
        tom_weather = weather;
        tom_temp = String(temp-273.15);
      }
 
    else {
      Serial.println("Error on HTTP request");
    }
 
    http.end();
}

//今日の天気を取得する関数
void get_today_weather(){
  HTTPClient http;
 
    http.begin(endpoint + key); //URLを指定
    int httpCode = http.GET();  //GETリクエストを送信
 
    if (httpCode > 0) { //返答がある場合
 
        String payload = http.getString();  //返答(JSON形式)を取得
        Serial.println(httpCode);
        Serial.println(payload);

        //jsonオブジェクトの作成
        DynamicJsonBuffer jsonBuffer;
        String json = payload;
        JsonObject& weatherdata = jsonBuffer.parseObject(json);

        //パースが成功したかどうかを確認
        if(!weatherdata.success()){
          Serial.println("parseObject() failed");
        }

        //各データを抜き出し
        const char* weather = weatherdata["weather"][0]["main"].as<char*>();
        const double temp = weatherdata["main"]["temp"].as<double>();

        //表示用変数に各要素をセット
        now_weather = weather;
        now_temp = String(temp-273.15);
      }
 
    else {
      Serial.println("Error on HTTP request");
    }
 
    http.end(); //Free the resources
}

//ticker用の天気データまとめ関数
void get_weather_data(){
  get_today_weather();
  get_tomorrow_weather();
}

void get_time(){
  getLocalTime(&timeInfo);//tmオブジェクトのtimeInfoに現在時刻を入れ込む
  sprintf(date, " %04d/%02d/%02d ",timeInfo.tm_year + 1900, timeInfo.tm_mon + 1, timeInfo.tm_mday);//日付に変換
  sprintf(hour_minute_sec, "%02d:%02d:%02d",timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec);//時間に変換
}

void ClockOverlay(OLEDDisplay *display, OLEDDisplayUiState* state) {
  display->setTextAlignment(TEXT_ALIGN_LEFT);
  display->setFont(ArialMT_Plain_10);
  display->drawString(25, 0, String(date) + String(hour_minute_sec));
}

void drawFrame1(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) {
  //今日の天気を表示
  display->setTextAlignment(TEXT_ALIGN_LEFT);
  display->setFont(ArialMT_Plain_16);
  display->drawString(10 + x,10 + y,"weather: " + String(now_weather));
  display->setFont(ArialMT_Plain_24);
  display->setTextAlignment(TEXT_ALIGN_CENTER);
  display->drawString(64 + x,25 + y,String(now_temp) + "°C");
}

void drawFrame2(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) {
  //明日の天気を表示
  display->setTextAlignment(TEXT_ALIGN_LEFT);
  display->setFont(ArialMT_Plain_10);
  display->drawString(0 + x,10 + y,"12 hours after");
  display->setFont(ArialMT_Plain_16);
  display->drawString(0+x,22+y,String(tom_weather));
  display->setFont(ArialMT_Plain_24);
  display->drawString(50+x,22+y,String(tom_temp) + "°C");
}

//SSD1306関連
FrameCallback frames[] = { drawFrame1, drawFrame2};
int frameCount = 2;
OverlayCallback overlays[] = { ClockOverlay };
int overlaysCount = 1;

void setup() {
  Serial.begin(115200);
  Serial.println();
  Serial.println();

  //Wi-Fi関連
  WiFi.begin(ssid, password);
  
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi..");
  }
  Serial.println("Connected to the WiFi network");
  
  configTime(9 * 3600L, 0, "ntp.nict.jp", "time.google.com", "ntp.jst.mfeed.ad.jp");//NTPの設定

  //Tickerの設定
  tickerWeatherUpdate.attach(60, get_weather_data);
  get_weather_data();
  
  //ssd1306のui設定
  ui.setTargetFPS(30);

  // You can change this to
  // TOP, LEFT, BOTTOM, RIGHT
  ui.setIndicatorPosition(BOTTOM);

  // Defines where the first frame is located in the bar.
  ui.setIndicatorDirection(LEFT_RIGHT);

  // You can change the transition that is used
  // SLIDE_LEFT, SLIDE_RIGHT, SLIDE_UP, SLIDE_DOWN
  ui.setFrameAnimation(SLIDE_LEFT);

  // Add frames
  ui.setFrames(frames, frameCount);

  // Add overlays
  ui.setOverlays(overlays, overlaysCount);

  // Initialising the UI will init the display too.
  ui.init();

}


void loop() {
  int remainingTimeBudget = ui.update();

  if (remainingTimeBudget > 0) {
    delay(remainingTimeBudget);
    get_time();
  }
}

ソースコード解説

今までの記事で最も長いコードとなりました。しかし、使われている関数・やり方はほぼ以下の3つの記事の組み合わせです。以下の記事をお読みいただければ、関数の使い方は分かるかと思います。

関連記事   ESP32でOpenWeatherMapのAPIを使って現在の天気をLCD「AQM1602A」に表示させる

ここでは上記の記事にはない、12時間後の天気予報を取得するget_tomorrow_weather() について解説します。

天気予報情報をjson形式で取得する

ESP32で現在の天気を取得してみたとほぼ同様の方法で天気予報を取得します。
ただし、getリクエストを送るURLが違います。

これが現在の天気を取得するURL

http://api.openweathermap.org/data/2.5/weather?q=Tokyo,jp&APPID=YOURKEY

これが天気予報を取得するURL

http://api.openweathermap.org/data/2.5/forecast?q=Tokyo,jp&APPID=YOURKEY

変わったのはweatherの部分が予報を意味するforecastになった点です。他は同じですが、当然返ってくるjsonの構造が変わってきます。返ってくるjsonを見てみましょう。

ブラウザに上記のURLを入力してみてください。かなりの分量のjson形式のテキストが返ってくるはずです。

この天気予報APIの情報は公式ページをご覧下さい。

jsonから指定時間後の天気予報を抜き出す

頭が痛くなるようなjsonですが、簡単に書くと以下のような構造になっています。どうでもいい情報に関してはhogeと表しています。

{hoge,"list"[{0時のデータ},{3時のデータ},{},....{5日後の21時のデータ}]}

3時間おきの天気予報データを送ってくれます。

今回の例では、現在時刻の12時間後の天気を取得してみました。この場合、現在の時間(h)をnow_hourとし、選ぶべきデータの番号をdata_numberとすると

data_number = now_hour/3 + 4

となります。例えばnow_hourが2の時は、4番目のデータに当たる12時のデータが取得されます。
よって、このjsonが入ったjsonオブジェクトをjson_objとすると

12_hours_weather = json_obj["list"][data_number]

で12時間後の天気予報が取得できます。

get_tomorrow_weather()を作る

void get_tomorrow_weather(){
  HTTPClient http;
 
    http.begin(endpoint_forecast + key); //URLを指定
    int httpCode = http.GET();  //GETリクエストを送信
 
    if (httpCode > 0) { //返答がある場合
 
        String payload = http.getString();  //返答(JSON形式)を取得

        //jsonオブジェクトの作成
        DynamicJsonBuffer jsonBuffer;
        String json = payload;
        JsonObject& weatherdata = jsonBuffer.parseObject(json);

        //パースが成功したかどうかを確認
        if(!weatherdata.success()){
          Serial.println("parseObject() failed");
        }
        //抜き出すデータの番号
        int data_number = now_hour/3 + 4;
        //12時間後のデータを抜き出し
        const char* weather = weatherdata["list"][data_number]["weather"][0]["main"].as<char*>();
        const double temp = weatherdata["list"][data_number]["main"]["temp"].as<double>();
        //表示用変数に各要素をセット
        tom_weather = weather;
        tom_temp = String(temp-273.15);
      }
 
    else {
      Serial.println("Error on HTTP request");
    }
 
    http.end(); //Free the resources
}

前半部分はコメントを参照してください。
データを抜き出す部分を解説します。

//抜き出すデータの番号
int data_number = now_hour/3 + 4;
//12時間後のデータを抜き出し
const char* weather = weatherdata["list"][data_number]["weather"][0]["main"].as<char*>();
const double temp = weatherdata["list"][data_number]["main"]["temp"].as<double>();
//表示用変数に各要素をセット
tom_weather = weather;
tom_temp = String(temp-273.15);

前述の通り、まず何番目のデータをとればいいかをdata_numberに代入しています。
その番号を使い、

weatherdata["list"][data_number]

でその時間の天気情報が取得できました。ここからはESP32で天気APIから天気情報を取得し、OLEDディスプレイに表示させてみたと同じで、その中の

weatherdata["list"][data_number]["weather"][0]["main"].as<char*>()

に天気(ClearとかRainとか)が入っています。文字列の長さが不明なので、as<char*>を使ってポインタにして代入しています。
同様にその時間の天気は

weatherdata["list"][data_number]["main"]["temp"].as<double>()

で取得します。そのままでは文字列として返ってくるので、.as<double>でdoubleに型変換します。温度を基準に何か動きをさせる、などではない場合、文字列のままでもいいかもしれませんね。

これでtom_weathertom_tempに天気情報が入りました。あとは表示するだけです。

表示するUIを考える

void drawFrame1(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) {
  //今日の天気を表示
  display->setTextAlignment(TEXT_ALIGN_LEFT);
  display->setFont(ArialMT_Plain_16);
  display->drawString(10 + x,10 + y,"weather: " + String(now_weather));
  display->setFont(ArialMT_Plain_24);
  display->setTextAlignment(TEXT_ALIGN_CENTER);
  display->drawString(64 + x,25 + y,String(now_temp) + "°C");
}

void drawFrame2(OLEDDisplay *display, OLEDDisplayUiState* state, int16_t x, int16_t y) {
  //明日の天気を表示
  display->setTextAlignment(TEXT_ALIGN_LEFT);
  display->setFont(ArialMT_Plain_10);
  display->drawString(0 + x,10 + y,"12 hours after");
  display->setFont(ArialMT_Plain_16);
  display->drawString(0+x,22+y,String(tom_weather));
  display->setFont(ArialMT_Plain_24);
  display->drawString(50+x,22+y,String(tom_temp) + "°C");
}

表示部分はここ。これも前回の天気表示のものと変わりません。それぞれ別ページに表示したいので、Frameを2つ作っています。表示する文字列や位置はご自由に変更ください。

正直今のままのUIはダサいのでorz

指定時間ごとに天気を取得する

天気予報取得ですが、ntpと同じようにloop()内に入れ込むことはできません。

void loop() {
  int remainingTimeBudget = ui.update();

  if (remainingTimeBudget > 0) {
    delay(remainingTimeBudget);
    get_time();
  }
}

なぜなら、このループは1秒間に30回回され、APIの制限にかかってしまうからです。

制限にかかると情報が得られなくなります。これを回避するには、「ある時間おきに関数を呼び出す仕組み」が必要となります。それが今回解説するTickerです。

Tickerの使い方

#include <Ticker.h>

Ticker tickerWeatherUpdate;

//ticker用の天気データまとめ関数
void get_weather_data(){
  get_today_weather();
  get_tomorrow_weather();
}

//Tickerの設定
tickerWeatherUpdate.attach(60, get_weather_data);

です。まずTickerのオブジェクトであるtickerWeatherupdateを作成。その後、天気予報を2つとも取得するget_weather_dataを定義します。

あとはtickerWeatherUpdateのattachメソッドに、何秒ごとに呼び出したいかと、呼び出したい関数の名前を入れます。ここでは60秒ごとにセットしました。

これだけで、裏でこの定期処理をしてくれます!かなり便利。APIを使う工作には必須ともいえるかもしれません。

まとめ

今まで上げてきた技術・ノウハウを使って一つの実用的なものを作れました!
まだまだやれていない組み合わせがあり、例えば「ESP32でRGBLEDを使う」とこの天気予報ステーションで、明日の天気を色で表現してくれるものなんかも作れそうです。雨だったら青、晴れだったらオレンジとかよくないですか…??そそられますね。

twitterアカウントの方で最新記事のお知らせや記事にならないような小さな情報を小出ししています。もしよろしければフォローお願いいたしますm(__)m

Pocket

返信を残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です