9
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

M5Stackで天気予報を表示

Last updated at Posted at 2020-05-02

はじめに

うちにはAmazon Echoがあるので「アレクサ、天気予報を教えて」とお願いしますが、降水確率を教えてくれなかったり、明日は今日より暑いのか寒いのか教えてくれなかったりで、イマイチ参考になりません。そこで今回はM5Stackを使って、知りたい情報を常時表示してくれる天気予報専用端末を作りました。

動作の概要

20200502_205523.jpg

  • Yahoo!天気・災害から情報を取得し、今日の天気、明日の天気を交互に表示します。あわせて、気温(最高気温、最低気温)と降水確率も表示します。
  • 仕組みとしては、クラウド側のgoogleスプレッドシートおよびGoogle Apps Scriptと、M5Stackが連携しています。
    • クラウド側では、googleスプレッドシートのimportXML関数を使って、Yahoo!天気の情報をスクレイピングしてシートに格納しています。
    • M5stackは、Google Apps Script経由でgoogleスプレッドシートにアクセスし、天気予報の情報をダウンロードして画面に表示しています。

準備するもの

コーディング

※ソースコード一式はgithubにアップしています。

googleスプレッドシートのコーディング

  • weather.xlsxをダウンロードして、googleドライブにアップロードします。
  • メニューから「ファイル」→「googleスプレッドシートとして保存」を実行し、googleスプレッドシートとして保存します。
  • C1セルで表示したい地域を選択します。表示したい地域がリストにない場合、M列~O列に追記することで追加可能です。

weather_xlsx.jpg

Google Apps Scriptのコーディング

  • googleスプレッドシートのメニューから「ツール」→「スクリプトエディタ」を選択し、スクリプトエディタを実行します。
  • 下記スクリプトを貼り付けます。
  • 「***」の部分には、googleスプレッドシートのidを記載します。(googleスプレッドシートのURL「https://docs.google.com/spreadsheets/d/***/edit 」の「***」の部分)
  • 作成したら、「リソース」→「ウェブアプリケーションとして導入」で公開します。(アクセス権は「Anyone, even anonymous」とします)
  • 更新したら版数(Project Version)を「new」にして改版するのを忘れずに!
weather.gs
function doGet(e) {
  // listデータをjsonに変換
  payload = JSON.stringify(get_weather())
  return ContentService.createTextOutput(payload).setMimeType(ContentService.MimeType.JSON);
}

function get_weather(){
  var id = "***"
  var ss = SpreadsheetApp.openById(id)
  var sheet = ss.getSheetByName("シート1")
  var values = sheet.getRange(1, 2, 4, 9).getValues()

  var res = {}
  res[values[0][0]] = values[0][1];
  res[values[0][2]] = values[0][3];
  for(var i=2; i<4; i++){
    res[values[i][0]] = {};
    for(var j=1; j<8; j++){
      res[values[i][0]][values[1][j]] = values[i][j];
    }
  }
  Logger.log(res)
  return res  
}

M5Stack側のコーディング

  • arduino IDEを使います。
  • M5stackの初期設定をします。(https://docs.m5stack.com/#/en/arduino/arduino_development)
  • arduino IDEのメニューから「ツール」→「ライブラリを管理…」を選択し、「ArduinoJson」のライブラリをダウンロードします。
  • SSIDとpasswordはWi-Fi環境に合わせて入力します。
  • urlには、Google Apps ScriptのURL「Current web app URL:」を入力します。
m5stack_weather.ino
#include <M5Stack.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>

// GAS設定
const String url = "https://script.google.com/macros/s/***/exec";

// 日本語フォント設定
const char* f24 = "genshin-regular-24pt";

// Wi-Fi設定
const char* ssid = "***";
const char* password = "***";

// NTP設定
#define TIMEZONE_JST  (3600 * 9)  // 日本標準時は、UTC(世界標準時)よりも9時間進んでいる。
#define DAYLIGHTOFFSET_JST  (0)   // 日本は、サマータイム(夏時間)はない。
#define NTP_SERVER1   "ntp.nict.jp"  // NTPサーバー
#define NTP_SERVER2   "ntp.jst.mfeed.ad.jp"   // NTPサーバー
static struct tm timeinfo;
int lasthour;
int lastmin;
String disp;

//ArduinoJson
const size_t capacity = JSON_OBJECT_SIZE(3) + 2*JSON_OBJECT_SIZE(10) + 170;
StaticJsonDocument<capacity> doc;
JsonObject weather;

// setup
void setup() {

  //M5初期化
  M5.begin();
  Serial.begin(9600); //M5.begin()の後にSerialの初期化をしている
  M5.Power.begin();
  dacWrite(25, 0); //ノイズ対策

  // フォント
  M5.Lcd.loadFont(f24, SD); // SDカードからフォント読み込み
  M5.Lcd.fillScreen(WHITE);
  M5.Lcd.setTextColor(BLACK, WHITE); 

  // Wifi接続
  M5.Lcd.println(" Wi-Fi APに接続します。");
  WiFi.begin(ssid, password);  //  Wi-Fi APに接続
  M5.Lcd.print("Wi-Fi APに接続しています");
  while (WiFi.status() != WL_CONNECTED) {  //  Wi-Fi AP接続待ち
    M5.Lcd.print(".");
    delay(100);
  }
  M5.Lcd.println(" Wi-Fi APに接続しました。");
  M5.Lcd.print("IP address: "); M5.Lcd.println(WiFi.localIP());
  
  // NTP設定
  configTime( TIMEZONE_JST, DAYLIGHTOFFSET_JST, NTP_SERVER1, NTP_SERVER2 );

  lasthour = -1;
  lastmin = -1;
  disp = "明日";
}

// loop関数
void loop() {
  String filename;
  char buf[40];
  const char* koumoku[] = {"0-6", "6-12", "12-18", "18-24"};

  // LCDをいったんクリア
  M5.Lcd.fillScreen(WHITE);

  // 10分に1回GASから天気予報を取得
  get_time();
  if(lastmin == -1 || (get_min() % 10 == 0 && lastmin != get_min())){
    lastmin = get_min(); lasthour = get_hour();
    //M5.Lcd.unloadFont();
    weather = getWeather(url);
    //M5.Lcd.loadFont(f24, SD); // SDカードからフォント読み込み
  }

  // (今日 or 明日)の天気は…を表示
  M5.Lcd.setCursor(2, 2);
  if(disp == "明日"){
    disp = "今日";
    String message = "今日の" + weather["地域"].as<String>() + "の天気は…";
    M5.Lcd.println(message);
  }else{
    disp = "明日";
    String message = "明日の" + weather["地域"].as<String>() + "の天気は…";
    M5.Lcd.println(message);
  }

  // 天気を表示
  sprintf(buf, "/weather/%d_day.jpg", weather[disp]["天気"].as<int>());
  M5.Lcd.drawJpgFile(SD, buf, 2, 32);

  // 気温を表示
  M5.Lcd.setTextDatum(0);
  M5.Lcd.setTextColor(RED, WHITE); 
  M5.Lcd.drawString(weather[disp]["最高気温"].as<String>(), 200, 64);
  M5.Lcd.setTextColor(BLUE, WHITE); 
  M5.Lcd.drawString(weather[disp]["最低気温"].as<String>(), 200, 100);


  // 降水確率を表示
  M5.Lcd.drawJpgFile(SD, "/weather/table.jpg", 10, 138);
  M5.Lcd.setTextDatum(4);
  M5.Lcd.setTextColor(BLACK, WHITE); 
  for(int i = 0; i < 4; i++){
    M5.Lcd.drawString(weather[disp][koumoku[i]].as<String>(), 48 + 75 * i, 176);
  }

  // 更新時刻を表示
  M5.Lcd.setCursor(152, 214);
  String message = weather["更新時刻"].as<String>() + "更新";
  M5.Lcd.print(message);

  // 15秒で今日と明日を切り替え
  delay(1000*15);
}

//時刻を取得する
void get_time(){
  if (!getLocalTime(&timeinfo)) {
    Serial.println("Failed to obtain time");
    return;
  }
}

// 時(hour)を取得
int get_hour(){
  return timeinfo.tm_hour;
}

// 分(min)を取得
int get_min(){
  return timeinfo.tm_min;
}

// 天気をGASから取得
JsonObject getWeather(String url) { 
  HTTPClient http;

  // Locationヘッダを取得する準備(リダイレクト先の確認用)
  const char* headers[] = {"Location"};
  http.collectHeaders(headers, 1);

  // getメソッドで情報取得(ここでLocationヘッダも取得)
  http.begin(url);
  http.addHeader("Content-Type", "application/json");
  int httpCode = http.GET();//GETメソッドで接続

  // 200OKの場合
  if (httpCode == HTTP_CODE_OK) {
    // ペイロード(JSON)を取得
    String payload = "";
    payload = http.getString();

    // JSONをオブジェクトに格納
    deserializeJson(doc, payload); //HTTPのレスポンス文字列をJSONオブジェクトに変換
    http.end();
    Serial.println(payload);

    // JsonObjectで返却
    return doc.as<JsonObject>();

  // 302 temporaly movedの場合
  }else if(httpCode == 302){
    http.end();

    // LocationヘッダにURLが格納されているのでそちらに再接続
    return getWeather(http.header(headers[0]));

  // その他
  }else{
    http.end();
    Serial.printf("httpCode: %d", httpCode);
    return doc.as<JsonObject>();
  }
}

画像ファイル・フォントの準備

  • microSDカードにフォントと画像ファイル(天気予報アイコン、降水確率表示用テーブル)を格納します。
  • M5Stackで好きなフォントを使う – Watako-Lab.を参考にフォントファイル(vlwファイル)を作成しました。日本語をフルで入れるとメモリが不足するようなので、漢字は使う文字のみに限定しました。(unicodeコードポイントの特定にはUnicode文字ツールを利用させていただきました)
processingソースコード(抜粋)
static final int[] specificUnicodes = {
  0x4ECA, 0x65E5, 0x306E, 0x5929, 0x6C17, 0x306F, 0x2026, 0x660E, 0x65E5, 0x306E, 0x5929, 0x6C17, 0x306F, 
  0x2026, 0x63A5, 0x7D9A, 0x66F4, 0x65B0, 0x540D, 0x53E4, 0x5C4B, 0x2103, 
  0x672D, 0x5E4C, 0x9752, 0x68EE, 0x76DB, 0x5CA1, 0x4ED9, 0x53F0, 0x79CB, 0x7530, 0x5C71, 0x5F62, 0x798F, 
  0x5CF6, 0x6C34, 0x6238, 0x5B87, 0x90FD, 0x5BAE, 0x524D, 0x6A4B, 0x3055, 0x3044, 0x305F, 0x307E, 0x5343, 
  0x8449, 0x6771, 0x4EAC, 0x6A2A, 0x6D5C, 0x65B0, 0x6F5F, 0x5BCC, 0x5C71, 0x91D1, 0x6CA2, 0x798F, 0x4E95, 
  0x7532, 0x5E9C, 0x9577, 0x91CE, 0x5C90, 0x961C, 0x9759, 0x5CA1, 0x540D, 0x53E4, 0x5C4B, 0x6D25, 0x5927, 
  0x6D25, 0x4EAC, 0x90FD, 0x5927, 0x962A, 0x795E, 0x6238, 0x5948, 0x826F, 0x548C, 0x6B4C, 0x5C71, 0x9CE5, 
  0x53D6, 0x677E, 0x6C5F, 0x5CA1, 0x5C71, 0x5E83, 0x5CF6, 0x5C71, 0x53E3, 0x5FB3, 0x5CF6, 0x9AD8, 0x677E, 
  0x677E, 0x5C71, 0x9AD8, 0x77E5, 0x798F, 0x5CA1, 0x4F50, 0x8CC0, 0x9577, 0x5D0E, 0x718A, 0x672C, 0x5927, 
  0x5206, 0x5BAE, 0x5D0E, 0x9E7F, 0x5150, 0x5CF6, 0x90A3, 0x8987, 0x6642
};
  • 画像ファイル(降水確率表示用テーブル)はここからダウンロードしてSDカードに格納してください。
  • 画像ファイル(天気予報アイコン)はいらすとやさんの素材で作成しました(素材の再配布はNGのようなので、各自作成いただければと思います)。作成した画像はSDカードに格納します。

できていないこと

  • importXMLの読み込みができずにうまく表示できない場合があります。importXMLは多すぎると読み込みがうまくいかない場合があるようですが、そんなにたくさん使っていないはずなので、理由がよくわかりません。
  • Yahoo!天気の今日・明日の天気は1日8回更新されるとのことですが、なかなかリアルタイムで取得できません。これもimportXML関数の動作によるものかなとは思いますが、まだ調べ切れていません。

(参考)コードの解説

googleスプレッドシートの動作

Google Apps Scriptの動作

JSON出力
"{今日={12-18=---, 6-12=---, 最低気温=15℃[0], 天気=100, 18-24=0%, 最高気温=30℃[+4], 0-6=---}, 地域=東京, 明日={最低気温=16℃[+1], 天気=114, 18-24=60%, 最高気温=24℃[-6], 0-6=10%, 12-18=20%, 6-12=10%}, 更新時刻=17時00分}"

Google Apps ScriptからのJSONダウンロード

1. HTTPClientのcollectHeadersメソッドで取得したいヘッダ(Locationヘッダ)を指定
2. GETメソッドでHTTPリクエストを実行
3. 302だった場合にはheaderメソッドでLocationヘッダの内容を取得
4. Locationヘッダに書かれたURLを対象にHTTPリクエストを再度実行
(以降、200 OKの応答があるまで繰り返し)

JSONのデシリアライズ

9
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?