アクトインディ開発者ブログ

子供とお出かけ情報「いこーよ」を運営する、アクトインディ株式会社の開発者ブログです

天気予報APIを使って天気を表示してみた

新米Webエンジニアのhiroさんです。

ツーリングでもおでかけでも天気の情報は欠かせません。
というわけで自分のアプリに天気予報APIから現在の天気を取得して画面に表示してみます。

要件

天気情報提供サービスの OpenWeatherMap の無料枠を使って実装します。

OpenWeatherMap

  • 天気は画像で表現
  • 画像を画面ヘッダのアプリケーション名の横に表示
  • APIから取得した情報は都道府県単位に weather_forecasts テーブルに保持
  • 前回取得した日時から10分が過ぎている場合はAPIから天気を再取得して weather_forecasts に保存
  • 前回取得した日時から10分以内の場合は weather_forecasts テーブルから天気を取得
  • 都道府県はユーザ情報から取得
  • API Key はgem(dotenv-rails)を使って環境変数に格納・取得
  • APIレスポンスの要素を条件に天気画像を表示(画像は いらすとや さんから)

こちらの情報を参考にレスポンス要素の中から ID, Main, Description を使って画像表示の条件を固める。上から順番に表示条件に合致した画像を表示する。

表示条件 画像(9種類)
Description:
clear sky
or
few clouds
f:id:envgp:20200125130520p:plain
clear_sky.png
Description:
scattered clouds
f:id:envgp:20200125131506p:plain
scattered_cloud.png
Description:
broken clouds
or
overcast clouds
f:id:envgp:20200125131715p:plain
cloud.png
ID:500~504
f:id:envgp:20200125134150p:plain
light_rain.png
ID:511~531
f:id:envgp:20200125134630p:plain
rain.png
Descriptionに"thunderstorm"と"rain"を含む
f:id:envgp:20200125134831p:plain
thunderstorm_rain.png
Descriptionに"thunderstorm"のみ含む
f:id:envgp:20200125222308p:plain
thunderstorm.png
Main:Snow
f:id:envgp:20200125135022p:plain
snow.png
Main:Drizzle
f:id:envgp:20200125135205p:plain
drizzle.png
上記以外 表示なし

ブラウザでAPIレスポンスが返ってくるかテスト

千葉県の情報を取得してみます。

https://api.openweathermap.org/data/2.5/weather?id=2113014&units=metric&appid=XX

データはデフォルトのJSON形式で返ってきました。問題ないようです。 このときの千葉県は曇りがち。

{
  "coord": {
    "lon": 140.12,
    "lat": 35.61
  },
  "weather": [
    {
      "id": 803,
      "main": "Clouds",
      "description": "broken clouds",
      "icon": "04n"
    }
  ],
  "base": "stations",
  "main": {
    "temp": 8.24,
    "feels_like": 4.36,
    "temp_min": 5.56,
    "temp_max": 10.56,
    "pressure": 1028,
    "humidity": 54
  },
  "visibility": 10000,
  "wind": {
    "speed": 2.6,
    "deg": 70
  },
  "clouds": {
    "all": 75
  },
  "dt": 1579953000,
  "sys": {
    "type": 1,
    "id": 7955,
    "country": "JP",
    "sunrise": 1579902283,
    "sunset": 1579939091
  },
  "timezone": 32400,
  "id": 2113014,
  "name": "Chiba",
  "cod": 200
}

実装

API Key を環境変数に格納

  • OpenWeatherMap にユーザ登録して API Key を取得
  • gem 'dotenv-rails'をインストール
  • applicationディレクトリトップに .env ファイルを作成
    OPEN_WEATHER_API_KEY=XXXXXXX...
  • rails console にて ENV['OPEN_WEATHER_API_KEY']で取得できること確認
  • .gitignore に/.envを追加し、gitの追跡ファイルから除外

画像ファイルを保存

app/assets/images/weathers に画像ファイルを格納
clear_sky.png
cloud.png
drizzle.png
light_rain.png
rain.png
scattered_cloud.png
snow.png
thunderstorm.png
thunderstorm_rain.png

テーブル周り

  • prefectures テーブルを新規作成

$ bundle exec rails g model prefecture

class CreatePrefectures < ActiveRecord::Migration[5.2]
  def change
    create_table :prefectures do |t|
      t.string :name, null: false,  comment: "県名"
      t.integer "geonames_id", null: false, comment: "GeoNamesのid"

      t.timestamps
    end
  end
end
  • weather_forecasts テーブルを新規作成

$ bundle exec rails g model weather_forecast

class CreateWeatherForecasts < ActiveRecord::Migration[5.2]
  def change
    create_table :weather_forecasts do |t|
      t.references :prefecture, foreign_key: true, null: false
      t.integer :weather_id, null: false, comment: "天気の状態id"
      t.string :main, null: false, comment: "天気の状態"
      t.string :description, null: false, comment: "天気の詳細"
      t.datetime :acquired_at, null: false, comment: "APIで取得した日時"

      t.timestamps
    end
  end
end
  • user テーブルに prefecture_id を追加

$ bundle exec rails g migration AddPrefectureIdToUsers

class AddPrefectureIdToUsers < ActiveRecord::Migration[5.2]
  def up
    add_reference :users, :prefecture, foreign_key: true
  end
  
  def down
    remove_reference :users, :prefecture
  end
end
  • マイグレーションを実行
    $ bundle exec rails db:migrate

  • モデルの関連付けを宣言

class Prefecture < ApplicationRecord
  has_many :users
  has_many :weather_forecasts
end

class User < ApplicationRecord
  belongs_to :prefecture, optional: true
end

class WeatherForecast < ApplicationRecord
  belongs_to :prefecture
end
  • 初期データ(seed)を作成
    とりあえず以下の1都2県を登録
Prefecture.create!(
  [
    {
      name: '東京都',
      geonames_id: '1850147',
    },
    {
      name: '高知県',
      geonames_id: '1859133',
    },
    {
      name: '沖縄県',
      geonames_id: '1854345',
    },
  ]
)

user.prefecture_id に各県を設定

User.create!(
  [
    {
      name: '太郎(管理者)',
      prefecture_id: '1',
    },
    {
      name: '山田 太郎',
      prefecture_id: '2',
    },
    {
      name: '鈴木 一郎',
      prefecture_id: '3',
    },
  ]
)
  • bundle exec rails db:resetでエラー発生
rails aborted!
ActiveRecord::InvalidForeignKey: PG::ForeignKeyViolation: ERROR:  insert or update on table "users" violates foreign key constraint "fk_rails_15bb13b679"
DETAIL:  Key (prefecture_id)=(1) is not present in table "prefectures".

User よりあとに Prefecture を持ってきていたのが原因でした。 Userの前に Prefecture を入れて解決。

天気の状態を返すサービスオブジェクト(forecast.rb)を作成

他にもいろいろ実装しましたが、核となるこのクラスだけ記載します。 API通信失敗時のエラー処理は見にくくなるため省略しています。

工夫したポイント

  • 表示条件の判定を各メソッドに切り出してわかりやすくした。
  • 表示条件の判定メソッド呼び出しを部分を工夫した。#weather_condition
  • Time.zone.now から forecast.acquired_at を引いて時間差を算出。
    diff = Time.zone.now - forecast.acquired_at

APIドキュメントを参照しながら initialize メソッドでリクエストパラメータを設定。

  • 'id': @prefecture.geonames_id,はcity ID で県を指定しています。
  • 'units': 'metric'は摂氏指定です。

app/services/weather/forecast.rb

module Weather
  class Forecast
    
    attr_accessor :prefecture, :data, :weather_id, :main, :description
    
    OPENWEATHERMAP_URL = 'https://api.openweathermap.org/data/2.5/weather'

    CONDITIONS = [
      'clear_sky',
      'scattered_cloud',
      'cloud',
      'light_rain',
      'rain',
      'thunderstorm_rain',
      'thunderstorm',
      'snow',
      'drizzle',
    ]
    
    USE_API_TIME_SEC = 600

    def initialize(prefecture_id)
      @prefecture = Prefecture.find(prefecture_id)
      @data =
      {
        'id': @prefecture.geonames_id,
        'units': 'metric',
        'appid': ENV['OPEN_WEATHER_API_KEY'],
      }
    end

    def weather_condition
      forecast = WeatherForecast.find_by(prefecture_id: @prefecture.id)

      if forecast.present?
        diff = Time.zone.now - forecast.acquired_at
        if diff.to_i >= USE_API_TIME_SEC
          forecast = weather_forecast_update(api_request())
        end
      else
        forecast = weather_forecast_update(api_request())
      end

      @weather_id = forecast.weather_id
      @main = forecast.main
      @description = forecast.description

      CONDITIONS.each_with_index do |condition, index|
        break condition if send("#{condition}?")
        break nil if CONDITIONS.length == index + 1
      end
    end

    private

    def api_request
      query=@data.to_query
      uri = URI("#{OPENWEATHERMAP_URL}?" + query)
      http = Net::HTTP.new(uri.host, uri.port)
      http.use_ssl = true
      response = Net::HTTP.get_response(uri)
      JSON.parse(response.body, symbolize_names: true)
    end

    def weather_forecast_update(res_data)
      weather = res_data[:weather][0]

      obj = {
        prefecture_id: @prefecture.id,
        weather_id: weather[:id].to_i,
        main: weather[:main],
        description: weather[:description],
        acquired_at: Time.zone.now,
      }

      WeatherForecast.find_or_initialize_by(prefecture_id: obj[:prefecture_id]).update_attributes(obj)
      WeatherForecast.find_by(prefecture_id: obj[:prefecture_id])
    end

    def clear_sky?
      description == 'clear sky' || description == 'few clouds'
    end

    def scattered_cloud?
      description == 'scattered clouds'
    end

    def cloud?
      description == 'broken clouds' || description == 'overcast clouds'
    end

    def light_rain?
      (500..504).include?(weather_id)
    end

    def rain?
      (511..531).include?(weather_id)
    end

    def thunderstorm_rain?
      /thunderstorm/ === description && /rain/ === description
    end

    def thunderstorm?
      /thunderstorm/ === description && !(/rain/ === description)
    end

    def snow?
      main == 'Snow'
    end

    def drizzle?
      main == 'Drizzle'
    end
  end
end

検証

画面に画像が表示されました。
f:id:envgp:20200126233158p:plain

  • logを見てもAPIリクエストに成功しています。
INFO -- : HTTP_LOG:opening connection to api.openweathermap.org:443...
INFO -- : HTTP_LOG:
INFO -- : HTTP_LOG:opened
INFO -- : HTTP_LOG:
INFO -- : HTTP_LOG:starting SSL for api.openweathermap.org:443...
INFO -- : HTTP_LOG:
INFO -- : HTTP_LOG:SSL established, protocol: TLSv1.2, cipher: ECDHE-RSA-AES256-GCM-SHA384
INFO -- : HTTP_LOG:
INFO -- : HTTP_LOG:<-
INFO -- : HTTP_LOG:"GET /data/2.5/weather?appid=XXXXXXXXXXXXXX&id=1850147&units=metric HTTP/1.1\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: api.openweathermap.org\r\n\r\n"
INFO -- : HTTP_LOG:
INFO -- : HTTP_LOG:-> "HTTP/1.1 200 OK\r\n"
INFO -- : HTTP_LOG:-> "Server: openresty\r\n"
INFO -- : HTTP_LOG:-> "Date: Sun, 26 Jan 2020 14:06:43 GMT\r\n"
INFO -- : HTTP_LOG:-> "Content-Type: application/json; charset=utf-8\r\n"
INFO -- : HTTP_LOG:-> "Content-Length: 446\r\n"
INFO -- : HTTP_LOG:-> "Connection: keep-alive\r\n"
INFO -- : HTTP_LOG:-> "X-Cache-Key: /data/2.5/weather?id=1850147&units=metric\r\n"
INFO -- : HTTP_LOG:-> "Access-Control-Allow-Origin: *\r\n"
INFO -- : HTTP_LOG:-> "Access-Control-Allow-Credentials: true\r\n"
INFO -- : HTTP_LOG:-> "Access-Control-Allow-Methods: GET, POST\r\n"
INFO -- : HTTP_LOG:-> "\r\n"
INFO -- : HTTP_LOG:reading 446 bytes...
INFO -- : HTTP_LOG:-> "{\"coord\":{\"lon\":139.69,\"lat\":35.69},\"weather\":[{\"id\":804,\"main\":\"Clouds\",\"description\":\"overcast clouds\",\"icon\":\"04n\"}],\"base\":\"stations\",\"main\":{\"temp\":4.15,\"feels_like\":1.13,\"temp_min\":0,\"temp_max\":7.78,\"pressure\":1025,\"humidity\":90},\"wind\":{\"speed\":2.08,\"deg\":354},\"clouds\":{\"all\":100},\"dt\":1580046764,\"sys\":{\"type\":3,\"id\":2001249,\"country\":\"JP\",\"sunrise\":1579988763,\"sunset\":1580025647},\"timezone\":32400,\"id\":1850147,\"name\":\"Tokyo\",\"cod\":200}"
INFO -- : HTTP_LOG:read 446 bytes
INFO -- : HTTP_LOG:Conn keep-alive
INFO -- : HTTP_LOG:
  • weather_forecasts テーブルに1件データが登録されています。
pry(main)> WeatherForecast.all
=>   WeatherForecast Load (0.7ms)  SELECT "weather_forecasts".* FROM "weather_forecasts"
[#<WeatherForecast:0x0000555a4ce00d70
  id: 9,
  prefecture_id: 1,
  weather_id: 804,
  main: "Clouds",
  description: "overcast clouds",
  acquired_at: Sun, 26 Jan 2020 21:11:46 JST +09:00,
  created_at: Sun, 26 Jan 2020 21:11:46 JST +09:00,
  updated_at: Sun, 26 Jan 2020 21:11:46 JST +09:00>]
  • 10分待って再度画面を更新してみます。 APIリクエストが走り、WeatherForecast テーブルが更新されました。
pry(main)> WeatherForecast.all
=>   WeatherForecast Load (0.7ms)  SELECT "weather_forecasts".* FROM "weather_forecasts"
[#<WeatherForecast:0x000055b59a546780
  id: 9,
  prefecture_id: 1,
  weather_id: 804,
  main: "Clouds",
  description: "overcast clouds",
  acquired_at: Sun, 26 Jan 2020 21:23:04 JST +09:00,
  created_at: Sun, 26 Jan 2020 21:11:46 JST +09:00,
  updated_at: Sun, 26 Jan 2020 21:23:04 JST +09:00>]
  • 10分以内に再度画面を更新するとAPIリクエストは走らず、WeatherForecast テーブルは更新されていません。問題ないようです。
[3] pry(main)> WeatherForecast.all
=>   WeatherForecast Load (0.8ms)  SELECT "weather_forecasts".* FROM "weather_forecasts"
[#<WeatherForecast:0x000055b59a59e430
  id: 9,
  prefecture_id: 1,
  weather_id: 804,
  main: "Clouds",
  description: "overcast clouds",
  acquired_at: Sun, 26 Jan 2020 21:23:04 JST +09:00,
  created_at: Sun, 26 Jan 2020 21:11:46 JST +09:00,
  updated_at: Sun, 26 Jan 2020 21:23:04 JST +09:00>

結果

このように天気の画像を表示できました👏

f:id:envgp:20200126214126p:plain
f:id:envgp:20200126222008p:plain
f:id:envgp:20200126214028p:plain
f:id:envgp:20200126222108p:plain
f:id:envgp:20200126214225p:plain
f:id:envgp:20200126214353p:plain
f:id:envgp:20200126221623p:plain

所感

今回はじめてAPIを叩いて処理するコードを実装しました。なかなか楽しいです😄
次はテストを書いていきたいです💪
調べてみると日本の快晴日は全国平均でも年間一ヶ月ほどしかありません。休日となるともっと少なくなります。快晴休日は予定を変更しておでかけしてみてはいかがでしょうか。