11.CoreInkでOpenWeatherMap(2)


11.CoreInkでOpenWeatherMap(2)

説明

無料のワンコールAPI(https://openweathermap.org/api/one-call-api)で
  • 現在の天気
  • 1Hの分予報
  • 48Hの時間予報
  • 7日間の毎日の天気予報
  • 全国気象警報
  • 過去5日間の過去の気象データ
が提供されていますが、今回は現在の天気のみを画面表示します。

以下の課題があります。
  • APIの呼び出し
  • json形式に変換
  • 欲しい項目を抽出
  • UNIX時間(規定時刻からの秒数表示)を変換
  • 日本語文字変数の表示
  • 48x48のjpgアイコンを9個用意

参考 UNIX時間

時刻の戻り値はUNIX時間で、協定世界時UTC 1970/1/1 0:0:0からの秒数です。符号付き32bit整数の最大の値は2,147,483,647で、UNIX時間とすると2038/1/19 3:14:7となります。この時刻を超えると、符号付32bit整数で扱っているシステムが正常動作しなくなります。

参考 構造体tm

構造体tmはtime.hの中で宣言されていて、以下の情報を含みます。

struct tm {      // 構造体 tm
  int tm_year;   // 年 1900からの年数
  int tm_mon;    // 月 0-11
  int tm_mday;   // 日 1-31
  int tm_wday;   // 曜日 0:日 ... 6:土
  int tm_hour;   // 時 0-23
  int tm_min;    // 分 0-59
  int tm_sec;    // 秒 0-61 最大2秒までのうるう秒
  int tm_yday;   // 年内の通し日数 0-365
  int tm_isdst;  // 夏時間正、採用していない時0、この情報が得られないときに負
};

APIの呼出し例

https://api.openweathermap.org/data/2.5/onecall?lat=35.447781&lon=139.642502&units=metric&lang=ja&appid={APIキー}
で呼出し(緯度経度は神奈川県横浜の例)、注釈を入れると以下になります。実際にはjsonに注釈はつけられません。

{
"lat":35.4478,            // 緯度
"lon":139.6425,           // 経度
"timezone":"Asia/Tokyo",  // リクエストされた場所
"timezone_offset":32400,  // UTCから秒単位でシフト

"current":{               // 現在の気象データAPIの応答
    "dt":1615712942,      // 現在の時刻(Unix,UTC)
    "sunrise":1615668847, // 日の出時刻(Unix、UTC) 2021/3/14 5:54:07
    "sunset":1615711647,  // 日没時間(Unix,UTC)
    "temp":13.34,         // 温度
    "feels_like":4.63,    // 体感温度
    "pressure":1008,      // 海面気圧(hPa)
    "humidity":28,        // 湿度(%)
    "dew_point":-4.23,    // 結露気温
    "uvi":0,              // UV指数
    "clouds":20,          // 曇量(%)
    "visibility":10000,   // 可視性(m)
    "wind_speed":8.75,    // 風速(m/s)
    "wind_deg":320,       // 風向(方位角)
    "wind_gust":14.92,    // 利用可能な場合、突風(m/s)
    "weather":[{"id":801, // 詳細気象ID
        "main":"Clouds",  // 概略気象
        "description":"薄い雲", // 詳細気象 日本語
        "icon":"02n"}]},  // 天気アイコンID

"minutely":[              // 分予報の降水確率データAPI応答
    {"dt":1615713000,     // データの時刻(UNIX,UTC) 2021/3/14 18:10:00
    "precipitation":0},   // 降水量(mm)
    {"dt":1615713060,     // データの時刻(UNIX,UTC) 2021/3/14 18:11:00
    "precipitation":0},   // 降水量(mm)
    ・・・(略)
    {"dt":1615716600,"precipitation":0}],  // 19:10:00の61データ

"hourly":[{               // 時間ごとの天気予報データAPI応答
    "dt":1615712400,      // 予測時刻(Unix,UTC) 2021/3/14 18:00:00
    "temp":13.34,               // 温度
    "feels_like":6.3,           // 体感温度
    "pressure":1008,            // 海面気圧(hPa)
    "humidity":28,              // 湿度(%)
    "dew_point":-4.23,          // 結露気温
    "uvi":0,                    // UV指数
    "clouds":20,                // 曇量(%)
    "visibility":10000,         // 可視性(m)
    "wind_speed":6.36,          // 風速(m/s)
    "wind_deg":358,             // 風向(方位角)
    "wind_gust":7.95,           // 利用可能な場合、突風(m/s)
    "weather":[{"id":801,       // 詳細気象ID
        "main":"Clouds",        // 概略気象
        "description":"薄い雲", // 詳細気象
        "icon":"02n"}],         // 天気アイコンID
    "pop":0},                   // 降水確率(%)

    {"dt":1615716000,           // 19:00:00
    "temp":13.67,"feels_like":5.73,"pressure":1009,"humidity":30,"dew_point":-3.16,"uvi":0,"clouds":16,"visibility":10000,"wind_speed":7.83,"wind_deg":357,"wind_gust":10.03,"weather":[{"id":801,"main":"Clouds","description":"薄い雲","icon":"02n"}],"pop":0},
    ・・・(略)
    {"dt":1615881600,  // 2021/3/16 17:00:00までのデータ48個
    "temp":19.79,"feels_like":15.03,"pressure":1008,"humidity":41,"dew_point":5.31,"uvi":0.15,"clouds":0,"visibility":10000,"wind_speed":5.53,"wind_deg":266,"wind_gust":10.95,"weather":[{"id":800,"main":"Clear","description":"晴天","icon":"01d"}],"pop":0}],

"daily":[{  // 毎日の天気予報データAPI応
    "dt":1615687200,  // 予測データの時刻(Unix,UTC) 2021/3/14 11:00:00
    "sunrise":1615668847, // 日の出時刻(Unix,UTC)
    "sunset":1615711647,  // 日没時間(Unix,UTC)
    "temp":{"day":15.83,  // 日中の気温
        "min":11.03,      // 1日の最低気温
        "max":16.95,      // 1日の最高気温
        "night":11.03,    // 夜の気温
        "eve":16.06,      // 夕方の気温
        "morn":12.5},     // 朝の気温
    "feels_like":{        // 体感温度
        "day":10.37,      // 日中の気温
        "night":3.49,     // 夜の気温
        "eve":11.8,       // 夕方の気温
        "morn":7.59},     // 朝の気温
    "pressure":1008,      // 海面気圧(hPa)
    "humidity":38,        // 湿度(%)
    "dew_point":0.66,     // 結露気温
    "wind_speed":5.3,     // 風速(m/s)
    "wind_deg":331,       // 風向(方位角)
    "weather":[{"id":500, // 詳細気象ID
        "main":"Rain",    // 概略気象
        "description":"小雨", // 詳細気象
        "icon":"10d"}],   // 天気アイコンID
    "clouds":3,           // 曇量(%)
    "pop":0.33,           // 降水確率
    "rain":0.45,          // 利用可能な場合、降水量(mm)
    "uvi":4.91},          // 1日のUV指数の最大値

    {"dt":1615773600,     // 2021/3/15 11:00:00
    "sunrise":1615755162,"sunset":1615798097,"temp":{"day":13.44,"min":9.68,"max":15.69,"night":14.28,"eve":14.62,"morn":9.84},"feels_like":{"day":7.67,"night":10.11,"eve":9.06,"morn":4.21},"pressure":1016,"humidity":31,"dew_point":-3.63,"wind_speed":4.78,"wind_deg":354,"weather":[{"id":800,"main":"Clear","description":"晴天","icon":"01d"}],"clouds":0,"pop":0,"uvi":4.54},
    ・・・(略)
    {"dt":1616292000,  // 2021/3/21 11:00:00までのデータ8個
    "sunrise":1616273048,"sunset":1616316793,"temp":{"day":19.93,"min":15.73,"max":19.93,"night":16.18,"eve":18.08,"morn":18.06},"feels_like":{"day":10.61,"night":11.59,"eve":12.06,"morn":10.17},"pressure":1000,"humidity":55,"dew_point":10.42,"wind_speed":13.62,"wind_deg":216,"weather":[{"id":501,"main":"Rain","description":"適度な雨","icon":"10d"}],"clouds":99,"pop":1,"rain":20.28,"uvi":5}]}
その他、 利用可能な場合
  • current.rain.1h:1H前からの雨量(mm)
  • hourly.rain.1h:1H前からの雨量(mm)
  • current.snow.1h:1H前からの積雪量(mm)
  • hourly.snow.1h:1H前からの積雪量(mm)
  • daily.wind_gust:突風(m/s)
  • daily.snow:積雪量(mm)
未確認の気象警報は、
  • alerts.sender_name:場所
  • alerts.start:開始日時(Unix,UTC)
  • alerts.end:終了日時(Unix,UTC)
  • alerts.event:警報名
  • alerts.description:説明
全取得データから変更した部分
  • 概略気象は詳細気象があるので略しました。
  • 詳細気象IDは、詳細気象があるので略しました。
  • 風の方位角だけではなく、計算した8方位(南西など)を追加しました。
  • 表示されていない場合、降水なし、積雪なし、突風なしと追加しました。
  • 天気アイコンは前回同様iconmonstr.comからダウンロードしたものを使用しました。
  • 天気アイコンの左下に都市名を記入しました。

スケッチ

xxxxxは、自分のwifiのSSID、wifiのパスワード、APIキーを入力します。自分で作った天気のアイコン"icon48-9.h"も必要です。

// 天気情報 OpenWeatherMap
// 確認は、https://openweathermap.org/city/1848354
// 関数一覧
// pushSprite   // LovyanGFXの1画面表示お決まり
// getJson      // jsonを得る
// drawCcurrent // 現在の天気の1画面をセット
// inkSet       // coreInkを使う時お決まり
// wifiSet      // wifiを使う時お決まり
// nihonSet     // 日本語を表示する時お決まり
// setup        //
// loop         //
#include "icon48-9.h"  // 天気マーク 同一ホルダに置く
#include <M5CoreInk.h>    // M5Stack CoreInk使用
#include <WiFi.h>         // wifi使用
#include <HTTPClient.h>   // httpと通信
#include <ArduinoJson.h>  // json型式で抽出
#include <efontEnableJaMini.h> // フォント 日本語ミニ
#include <efontFontData.h>     // efontのフォントデータ
#define LGFX_AUTODETECT  // init時に対応機種を自動認識
#include <LovyanGFX.hpp>       // 描画ライブラリ
#include <time.h>
const char* ssid = "xxxxx";  // 自分のwifiのSSID
const char* password = "xxxxx";     // wifiのパスワード
const char* weatherURL = "https://api.openweathermap.org/data/2.5/onecall?lat=35.447781&lon=139.642502&units=metric&lang=ja&appid=xxxxx";
// 神奈川県 横浜 ワンコールAPI
DynamicJsonDocument weatherInfo(20000);  //
Ink_Sprite InkPageSprite(&M5.M5Ink);     // 定義
static LGFX_Sprite sprite;               // 定義
time_t t;  // システム時刻 time()関数によって得られる,<time.h>で定義,数値
struct tm *tm;  // 構造体の型を宣言,タグ名,構造体変数名
static const char *wd[7] = {"(日)", "(月)", "(火)", "(水)", "(木)", "(金)", "(土)"};
int dy = 17, upy = 0;  // 行刻み,上の行
int sheet = 1;  // 1:現在(current) 2:1分毎1Hの降水確率(minutely)
// 3:1H毎2日間(hourly) 4:1日毎7日間(daily)

// LovyanGFXの1画面表示関数
void pushSprite(Ink_Sprite *coreinkSprite, LGFX_Sprite *lgfxSprite) {
    coreinkSprite->clear();
    for (int y = 0; y < 200; y++) {    // 縦方向0-199
    for (int x = 0; x < 200; x++) {  // 横方向0-199
        uint16_t c = lgfxSprite->readPixel(x, y);
        if (c == 0x0000) {
        coreinkSprite->drawPix(x, y, 0);
        }
    }
    }
    coreinkSprite->pushSprite(); // アロー演算子
}

DynamicJsonDocument getJson() {  //
    DynamicJsonDocument doc(20000);  //
    if ((WiFi.status() == WL_CONNECTED)) {  // wifiが接続されていたら
    HTTPClient http;             // 定義
    http.begin(weatherURL);      // アクセスするURLを登録
    Serial.println(weatherURL);  // シリアルモニタに表示
    int httpCode = http.GET();//登録URLに、GETリクエスト送信
    if (httpCode > 0) {//HTTPのリターンコード。エラーの場合は負数。
        String jsonString = http.getString();
        Serial.print("HTTPコード="); Serial.println(httpCode);
        Serial.println(jsonString);        // json用文字列 長い
        deserializeJson(doc, jsonString);
        // 直列化されたデータをDynamicJsonDocumentの形に変換
    } else {
        Serial.println("エラー HTTPリクエスト");  // シリアルモニタに表示
    }
    http.end();                // TCPコネクションを切断しHTTP通信を終了
    }
    return doc;
}

void drawCcurrent(String infoWeather) {  // 1画面セットする
    DynamicJsonDocument doc(20000);        //
    deserializeJson(doc, infoWeather);     //
    
    String dt = doc["dt"]; // 現在の時刻(Unix,UTC)
    t = dt.toInt() + 9 * 3600;
    tm = localtime(&t);
    sprite.setCursor(5, upy);
    sprite.printf("%04d/%02d/%02d%s %02d:%02d現在",
                tm->tm_year + 1900, tm->tm_mon + 1,
                tm->tm_mday, wd[tm->tm_wday],
                tm->tm_hour, tm->tm_min);
    Serial.printf("現在%04d/%02d/%02d%s %02d:%02d",
                tm->tm_year + 1900, tm->tm_mon + 1,
                tm->tm_mday, wd[tm->tm_wday],
                tm->tm_hour, tm->tm_min);

    String sunrise = doc["sunrise"]; // 日の出時刻(Unix,UTC)
    t = sunrise.toInt() + 9 * 3600;
    tm = localtime(&t);
    upy = upy + dy; sprite.setCursor(10, upy);
    sprite.printf("日の出%01d:%02d", tm->tm_hour, tm->tm_min);
    Serial.printf(" 日の出%01d:%02d", tm->tm_hour, tm->tm_min);

    String sunset = doc["sunset"];  // 日没時間(Unix,UTC)
    t = sunset.toInt() + 9 * 3600 ;
    tm = localtime(&t);
    sprite.printf("   日没%02d:%02d", tm->tm_hour, tm->tm_min);
    Serial.printf(" 日没%02d:%02d\n", tm->tm_hour, tm->tm_min);

    String icon = doc["weather"][0]["icon"]; // 天気アイコンID
    upy = upy + dy; sprite.setCursor(10, upy);
    Serial.printf("天気(%s)", icon);
    int x = 70, y = 52, W = 48, H = 48;  // 天気マークの表示位置と大きさ
    if (icon == "01d" || icon == "01n") {
    sprite.pushImage(x, y, W, H, img48hare);    // 晴天(晴れ)
    } else if (icon == "02d" || icon == "02n") {  // or
    sprite.pushImage(x, y, W, H, img48hareKumori); // いくつかの雲:11-25%(晴曇り)
    } else if (icon == "03d" || icon == "03n") {
    sprite.pushImage(x, y, W, H, img48kumori);  // 散在する雲25-50%(曇り)
    } else if (icon == "04d" || icon == "04n") {
    sprite.pushImage(x, y, W, H, img48donyori); // 壊れた雲51-100%(どんより)
    } else if (icon == "09d" || icon == "09n") {
    sprite.pushImage(x, y, W, H, img48kumoriAme); // にわか雨(曇雨)
    } else if (icon == "10d" || icon == "10n") {
    sprite.pushImage(x, y, W, H, img48ame);     // 雨(雨)
    } else if (icon == "11d" || icon == "11n") {
    sprite.pushImage(x, y, W, H, img48kaminari); // 雷雨(雷)
    } else if (icon == "13d" || icon == "13n") {
    sprite.pushImage(x, y, W, H, img48yuki);    // 雪(雪)
    } else if (icon == "50d" || icon == "50n") {
    sprite.pushImage(x, y, W, H, img48moya);    // 靄(もや)
    }

    String main = doc["weather"][0]["main"]; // 概略気象
    Serial.printf("%s\n", main);

    String id = doc["weather"][0]["id"];      // 詳細気象ID
    Serial.printf("詳細(%s)", id);

    String description = doc["weather"][0]["description"]; // 詳細気象 日本語
    sprite.printf("%s", description);
    Serial.printf("%s\n", description);

    String temp = doc["temp"];                   // 温度
    upy = upy + dy; sprite.setCursor(130, upy);
    sprite.printf("温度%2.0f℃", temp.toFloat());  // 文字列→単精度浮動小数点
    Serial.printf("温度%2.0f℃", temp.toFloat());

    String feels_like = doc["feels_like"];        // 体感温度
    upy = upy + dy; sprite.setCursor(130, upy);
    sprite.printf("体感%2.0f℃", feels_like.toFloat());
    Serial.printf(" 体感%2.0f℃", feels_like.toFloat());

    String dew_point = doc["dew_point"];      // 結露気温
    upy = upy + dy; sprite.setCursor(130, upy);
    sprite.printf("結露%2.0f℃", dew_point.toFloat());
    Serial.printf(" 結露%2.0f℃", dew_point.toFloat());

    sprite.setCursor(10, upy);                // 場所
    sprite.print("(横浜)");

    String humidity = doc["humidity"];       // 湿度(%)
    upy = upy + dy; sprite.setCursor(10, upy);
    sprite.printf("湿度%2.0f%%", humidity.toFloat());
    Serial.printf(" 湿度%2.0f%%", humidity.toFloat());

    String pressure = doc["pressure"];      // 海面気圧(hPa)
    sprite.printf("    海面%4.0fhPa", pressure.toFloat());
    Serial.printf(" 海面%4.0fhPa\n", pressure.toFloat());

    String wind_speed = doc["wind_speed"];   // 風速(m/s)
    upy = upy + dy; sprite.setCursor(10, upy);
    sprite.printf("風%2.0fm/s", wind_speed.toFloat());
    Serial.printf("風%2.0fm/s", wind_speed.toFloat());

    String wind_deg = doc["wind_deg"];        // 風向(方位角)
    int wind_degInt = wind_deg.toInt();
    String wind_houi;                          // 8方位
    if (wind_degInt < 22.5) {
    wind_houi = "北";
    } else if (wind_degInt < 67.5) {
    wind_houi = "北東";
    } else if (wind_degInt < 112.5) {
    wind_houi = "東";
    } else if (wind_degInt < 157.5) {
    wind_houi = "南東";
    } else if (wind_degInt < 202.5) {
    wind_houi = "南";
    } else if (wind_degInt < 247.5) {
    wind_houi = "南西";
    } else if (wind_degInt < 292.5) {
    wind_houi = "西";
    } else if (wind_degInt < 337.5) {
    wind_houi = "北西";
    } else {
    wind_houi = "北";
    }
    sprite.printf("(%s%d°)", wind_houi, wind_degInt);
    Serial.printf("(%s%d°)", wind_houi, wind_degInt);

    String clouds = doc["clouds"];                 // 曇量(%)
    sprite.setCursor(140, upy);
    sprite.printf(" 曇%2d%%", clouds.toInt());
    Serial.printf(" 曇量%2d%%\n", clouds.toInt());

    String uvi = doc["uvi"];                        // UV指数
    upy = upy + dy; sprite.setCursor(10, upy);
    sprite.printf("UV指数%4.1f", uvi.toFloat());
    Serial.printf("UV指数%4.1f", uvi.toFloat());

    String visibility = doc["visibility"];        // 可視性(m)
    sprite.printf("  可視%4.1fkm", visibility.toFloat() / 1000);
    Serial.printf(" 可視性%4.1fkm", visibility.toFloat() / 1000);

    String rain_1h = doc["hourly"][0]["rain"][0]["1h"];
    //利用可能な場合、過去1Hの降水量(mm)
    upy = upy + dy; sprite.setCursor(10, upy);
    if (rain_1h = "") {
    sprite.printf("降水なし");
    Serial.printf("降水なし");
    } else {
    sprite.printf("1H雨%.0dmm", rain_1h.toInt());
    Serial.printf("1H降水%.0dmm", rain_1h.toInt());
    }

    String snow_1h = doc["hourly"][0]["snow"][0]["1h"];
    //利用可能な場合、1H前からの積雪量(mm)
    if (snow_1h = "") {
    sprite.printf(" 積雪なし");
    Serial.printf(" 積雪なし");
    } else {
    sprite.printf(" 1H雪%.0dmm\n", snow_1h.toInt());
    Serial.printf(" 1H前からの積雪%.0dmm\n", snow_1h.toInt());
    }

    String wind_gust = doc["wind_gust"]; // 利用可能な場合、突風(m/s)
    upy = upy + dy; sprite.setCursor(10, upy);
    if (wind_gust = "") {
    sprite.printf("突風なし");
    Serial.printf(" 突風なし\n");
    } else {
    sprite.printf("突風%2.0fm/s", wind_gust.toFloat());
    Serial.printf(" 突風%2.0fm/s\n", wind_gust.toFloat());
    }
}

void inkSet() {                       // お決まり
    M5.begin();               // E-Ink,RTC,I2C,ブザーを初期化
    if (!M5.M5Ink.isInit()) {           // 初期化出来なければ
    Serial.println("エラー Ink Init"); // シリアルモニタに表示
    while (1) delay(100);             // ずっと待つ
    }
    M5.M5Ink.clear();                   // 画像クリア
    delay(1000);                        // 1秒待つ
    InkPageSprite.creatSprite(0, 0, 200, 200, false);
    // 画像領域を作成し、画面ドライバーから画像データバフを取得しない
}
void wifiSet() {                           // お決まり
    Serial.print(ssid);                      // シリアルモニタに表示
    Serial.print("に接続中");                 // シリアルモニタに表示
    WiFi.begin(ssid, password);              // ネット設定を初期化
    while (WiFi.status() != WL_CONNECTED) {  // 接続状態が接続完でない時
    delay(500);                            // 0.5秒待つ
    Serial.print(".");                     // シリアルモニタに表示
    }
    Serial.println("ok");                    // シリアルモニタに表示
}
void nihonSet() { // efont日本語16太 お決まり(16or24)
    sprite.setColorDepth(2);        // 2ビットパレットモードに設定
    sprite.createPalette();         // パレットモードにする?
    sprite.createSprite(200, 200);  // 200x200でスプライトを作成
    sprite.clear(TFT_WHITE);        // 全体の背景色を白
    sprite.setFont(&fonts::efontJA_16_b); // efont日本語16ドット太字
    sprite.setTextColor(TFT_BLACK, TFT_WHITE);  // 白地に黒字
}

void setup() {
    inkSet();                          // CoreInkの設定
    wifiSet();                         // wifiの設定
    nihonSet();                        // 日本語設定(efont16太)
    weatherInfo = getJson();           // json取得関数へ
    WiFi.disconnect(true);             // ネットワークから切断
    WiFi.mode(WIFI_OFF);               // wifiの動作モード
    Serial.print("wifi offか=");       // シリアルモニタに表示
    Serial.println(WiFi.status());     // wifiの接続状態
    // 0(WiFi.beginが呼出され3or4になるまで),1(使用可能なSSIDがない)
    // 2(スキャン完了),3(WiFi接続),4(すべての試行で接続失敗)
    // 5(接続が失われた),255(WiFiシールドがない)
    if (sheet == 1) {
    String today = weatherInfo["current"];  // 現在の気象
    drawCcurrent(today);                    // 1画面設定関数へ
    }
    pushSprite(&InkPageSprite, &sprite);    // 1画面表示関数へ
}

void loop() {
    delay(15000);                         // 15秒待つ
}