新米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
clear_sky.png
Description: scattered clouds
scattered_cloud.png
Description: broken clouds or overcast clouds
cloud.png
ID:500~504
light_rain.png
ID:511~531
rain.png
Descriptionに"thunderstorm"と"rain"を含む
thunderstorm_rain.png
Descriptionに"thunderstorm"のみ含む
thunderstorm.png
Main:Snow
snow.png
Main:Drizzle
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
テーブル周り
$ 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
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
検証
画面に画像が表示されました。
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 "
[
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 "
[
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 "
[
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 >
結果
このように天気の画像を表示できました👏
所感
今回はじめてAPIを叩いて処理するコードを実装しました。なかなか楽しいです😄
次はテストを書いていきたいです💪
調べてみると日本の快晴日は全国平均でも年間一ヶ月ほどしかありません。休日となるともっと少なくなります。快晴休日は予定を変更しておでかけしてみてはいかがでしょうか。